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