[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

admin auth now inline with atproto spec

see https://atproto.com/specs/xrpc#authentication

+78 -44
+5 -1
services/appview/.env.example
··· 13 13 NODE_ENV=development 14 14 PORT=3000 15 15 PUBLIC_URL=http://localhost:3000 16 - SERVICE_DID=did:web:localhost 16 + SERVICE_DID=did:web:localhost 17 + 18 + # Keys, generate these with openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32 19 + APPVIEW_K256_PRIVATE_KEY_HEX=keyhex 20 + ADMIN_PASSWORD=password
+1
services/appview/compose.dev.yaml
··· 30 30 DB_PASSWORD: mongo 31 31 DB_NAME: dev 32 32 COOKIE_SECRET: "00000000000000000000000000000000" 33 + ADMIN_PASSWORD: "00000000000000000000000000000000" 33 34 ports: 34 35 - "4000:3000" 35 36 volumes:
+71 -42
services/appview/src/auth/middleware.ts
··· 22 22 * 23 23 * @param c - Hono context 24 24 * @param next - Next middleware function 25 - * @param adminRequired - Whether admin privileges are required (checks agent DID against ADMIN_DIDS) 25 + * @param adminRequired - Whether admin privileges are required (checks admin token) 26 26 */ 27 27 export const authMiddleware = async (c: Context, next: Next, adminRequired = false) => { 28 28 const authHeader = c.req.header('Authorization') 29 29 30 - if (!authHeader || !authHeader.startsWith('Bearer ')) { 30 + if (!authHeader) { 31 31 throw new HTTPException(401, { 32 - message: 'Unauthorized: Invalid or missing Authorization header', 32 + message: 'Unauthorized: Missing Authorization header', 33 33 }) 34 34 } 35 35 36 - const jwt = authHeader.replace('Bearer ', '').trim() 36 + try { 37 + if (authHeader.startsWith('Basic ')) { 38 + const base64Credentials = authHeader.replace('Basic ', '').trim() 39 + const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8') 40 + const [username, password] = credentials.split(':') 41 + 42 + console.log('Basic auth attempt:', { username, password, expected: env.ADMIN_PASSWORD }) 43 + 44 + if (username === 'admin' && password === env.ADMIN_PASSWORD) { 45 + c.set('isAdmin', true) 46 + await next() 47 + return 48 + } else { 49 + throw new HTTPException(401, { 50 + message: 'Unauthorized: Invalid admin credentials', 51 + }) 52 + } 53 + } else if (authHeader.startsWith('Bearer ')) { 54 + const jwt = authHeader.replace('Bearer ', '').trim() 37 55 38 - try { 39 - // The service DID and resolver should be passed from app context 40 - const serviceDid = c.get('serviceDid') 41 - const didResolver = c.get('didResolver') as DidResolver 56 + // The service DID and resolver should be passed from app context 57 + const serviceDid = c.get('serviceDid') 58 + const didResolver = c.get('didResolver') as DidResolver 42 59 43 - const parsed = await verifyJwt(jwt, serviceDid, null, async (did: string) => { 44 - return didResolver.resolveAtprotoKey(did) 45 - }) 60 + const parsed = await verifyJwt(jwt, serviceDid, null, async (did: string) => { 61 + return didResolver.resolveAtprotoKey(did) 62 + }) 46 63 47 - // Set auth information in the context for route handlers to access 48 - c.set('did', parsed.iss) 49 - c.set('accessJwt', jwt) 64 + // Set auth information in the context for route handlers to access 65 + c.set('did', parsed.iss) 66 + c.set('accessJwt', jwt) 50 67 51 - // Check for admin status if required 52 - if (adminRequired) { 53 - const isAdmin = env.ADMIN_DIDS.includes(parsed.iss) 54 - if (!isAdmin) { 55 - throw new HTTPException(403, { 56 - message: 'Forbidden: Admin privileges required', 57 - }) 68 + // Check for admin status if required 69 + if (adminRequired) { 70 + if (!c.get('isAdmin')) { 71 + throw new HTTPException(403, { 72 + message: 'Forbidden: Admin privileges required - use Basic auth with admin token', 73 + }) 74 + } 58 75 } 59 - c.set('isAdmin', true) 60 - } 61 76 62 - await next() 77 + await next() 78 + } else { 79 + throw new HTTPException(401, { 80 + message: 'Unauthorized: Authorization header must start with "Basic " or "Bearer "', 81 + }) 82 + } 63 83 } catch (err) { 64 84 if (err instanceof HTTPException) { 65 85 throw err 66 86 } 67 87 throw new HTTPException(401, { 68 - message: 'Unauthorized: Invalid JWT token', 88 + message: 'Unauthorized: Invalid credentials', 69 89 }) 70 90 } 71 91 } ··· 77 97 export const optionalAuthMiddleware = async (c: Context, next: Next) => { 78 98 const authHeader = c.req.header('Authorization') 79 99 80 - if (authHeader && authHeader.startsWith('Bearer ')) { 81 - const jwt = authHeader.replace('Bearer ', '').trim() 100 + if (authHeader) { 101 + if (authHeader.startsWith('Basic ')) { 102 + try { 103 + const base64Credentials = authHeader.replace('Basic ', '').trim() 104 + const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8') 105 + const [username, password] = credentials.split(':') 82 106 83 - try { 84 - const serviceDid = c.get('serviceDid') 85 - const didResolver = c.get('didResolver') as DidResolver 107 + if (username === 'admin' && password === env.ADMIN_PASSWORD) { 108 + c.set('isAdmin', true) 109 + } 110 + } catch (err) { 111 + // On auth failure, just continue without setting admin context 112 + } 113 + } else if (authHeader.startsWith('Bearer ')) { 114 + const jwt = authHeader.replace('Bearer ', '').trim() 86 115 87 - const parsed = await verifyJwt(jwt, serviceDid, null, async (did: string) => { 88 - return didResolver.resolveAtprotoKey(did) 89 - }) 116 + try { 117 + const serviceDid = c.get('serviceDid') 118 + const didResolver = c.get('didResolver') as DidResolver 90 119 91 - // Set auth information if JWT is valid 92 - c.set('did', parsed.iss) 93 - c.set('accessJwt', jwt) 94 - 95 - // Check if user has admin privileges (but don't require it) 96 - if (env.ADMIN_DIDS.includes(parsed.iss)) { 97 - c.set('isAdmin', true) 120 + const parsed = await verifyJwt(jwt, serviceDid, null, async (did: string) => { 121 + return didResolver.resolveAtprotoKey(did) 122 + }) 123 + 124 + // Set auth information if JWT is valid 125 + c.set('did', parsed.iss) 126 + c.set('accessJwt', jwt) 127 + } catch (err) { 128 + // On auth failure, just continue without setting auth context 98 129 } 99 - } catch (err) { 100 - // On auth failure, just continue without setting auth context 101 130 } 102 131 } 103 132
+1 -1
services/appview/src/env.ts
··· 11 11 APPVIEW_K256_PRIVATE_KEY_HEX: envStr('APPVIEW_K256_PRIVATE_KEY_HEX') ?? '', 12 12 SERVICE_DID: envStr('SERVICE_DID') ?? 'did:web:localhost', 13 13 MOD_SERVICE_DID: envStr('MOD_SERVICE_DID') ?? 'did:web:localhost', 14 - ADMIN_DIDS: envList('ADMIN_DIDS') ?? [], 14 + ADMIN_PASSWORD: envStr('ADMIN_PASSWORD') ?? 'admin-token', 15 15 16 16 DB_NAME: envStr('DB_NAME') ?? 'dev', 17 17 DB_HOST: envStr('DB_HOST') ?? 'localhost',