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: err instanceof OAuthResolverError ? err.message : "couldn't initiate login", 62 }), 63 ), 64 ) 65 } 66 }), 67 ) 68 69 router.post( 70 '/logout', 71 handler(async (req, res) => { 72 await destroySession(req, res) 73 return res.redirect('/') 74 }), 75 ) 76 77 router.get( 78 '/', 79 handler(async (req, res) => { 80 const session = await getSession(req, res) 81 const agent = 82 session && 83 (await ctx.oauthClient.restore(session.did).catch(async (err) => { 84 ctx.logger.warn({ err }, 'oauth restore failed') 85 await destroySession(req, res) 86 return null 87 })) 88 const posts = await ctx.db.selectFrom('post').selectAll().orderBy('indexedAt', 'desc').limit(10).execute() 89 if (!agent) { 90 return res.type('html').send(page(home({ posts }))) 91 } 92 const { data: profile } = await agent.getProfile({ actor: session.did }) 93 return res.type('html').send(page(home({ posts, profile }))) 94 }), 95 ) 96 97 return router 98}