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}