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}