Scratch space for learning atproto app development
1import path from 'node:path' 2import { OAuthResolverError } from '@atproto/oauth-client-node' 3import { isValidHandle } from '@atproto/syntax' 4import express from 'express' 5import { createSession, destroySession, getSession } from '#/auth/session' 6import type { AppContext } from '#/config' 7import { home } from '#/pages/home' 8import { login } from '#/pages/login' 9import { page } from '#/view' 10import { handler } from './util' 11 12export const createRouter = (ctx: AppContext) => { 13 const router = express.Router() 14 15 router.use('/public', express.static(path.join(__dirname, '..', 'public'))) 16 17 router.get( 18 '/client-metadata.json', 19 handler((_req, res) => { 20 return res.json(ctx.oauthClient.clientMetadata) 21 }) 22 ) 23 24 router.get( 25 '/oauth/callback', 26 handler(async (req, res) => { 27 const params = new URLSearchParams(req.originalUrl.split('?')[1]) 28 try { 29 const { agent } = await ctx.oauthClient.callback(params) 30 await createSession(req, res, agent.accountDid) 31 } catch (err) { 32 ctx.logger.error({ err }, 'oauth callback failed') 33 return res.redirect('/?error') 34 } 35 return res.redirect('/') 36 }) 37 ) 38 39 router.get( 40 '/login', 41 handler(async (_req, res) => { 42 return res.type('html').send(page(login({}))) 43 }) 44 ) 45 46 router.post( 47 '/login', 48 handler(async (req, res) => { 49 const handle = req.body?.handle 50 if (typeof handle !== 'string' || !isValidHandle(handle)) { 51 return res.type('html').send(page(login({ error: 'invalid handle' }))) 52 } 53 try { 54 const url = await ctx.oauthClient.authorize(handle) 55 return res.redirect(url.toString()) 56 } catch (err) { 57 ctx.logger.error({ err }, 'oauth authorize failed') 58 return res.type('html').send( 59 page( 60 login({ 61 error: 62 err instanceof OAuthResolverError 63 ? err.message 64 : "couldn't initiate login", 65 }) 66 ) 67 ) 68 } 69 }) 70 ) 71 72 router.post( 73 '/logout', 74 handler(async (req, res) => { 75 await destroySession(req, res) 76 return res.redirect('/') 77 }) 78 ) 79 80 router.get( 81 '/', 82 handler(async (req, res) => { 83 const session = await getSession(req, res) 84 const agent = 85 session && 86 (await ctx.oauthClient.restore(session.did).catch(async (err) => { 87 ctx.logger.warn({ err }, 'oauth restore failed') 88 await destroySession(req, res) 89 return null 90 })) 91 const statuses = await ctx.db 92 .selectFrom('status') 93 .selectAll() 94 .orderBy('indexedAt', 'desc') 95 .limit(10) 96 .execute() 97 if (!agent) { 98 return res.type('html').send(page(home({ statuses }))) 99 } 100 const { data: profile } = await agent.getProfile({ actor: session.did }) 101 return res.type('html').send(page(home({ statuses, profile }))) 102 }) 103 ) 104 105 return router 106}