forked from
samuel.fm/statusphere-react
the statusphere demo reworked into a vite/react app in a monorepo
1import { OAuthResolverError } from '@atproto/oauth-client-node'
2import { isValidHandle } from '@atproto/syntax'
3import express from 'express'
4
5import { AppContext } from '#/context'
6import { getSession } from '#/session'
7
8export const createRouter = (ctx: AppContext) => {
9 const router = express.Router()
10
11 // OAuth metadata
12 router.get('/oauth-client-metadata.json', (_req, res) => {
13 res.json(ctx.oauthClient.clientMetadata)
14 })
15
16 // OAuth callback to complete session creation
17 router.get('/oauth/callback', async (req, res) => {
18 // Get the query parameters from the URL
19 const params = new URLSearchParams(req.originalUrl.split('?')[1])
20
21 try {
22 const { session } = await ctx.oauthClient.callback(params)
23
24 // Use the common session options
25 const clientSession = await getSession(req, res)
26
27 // Set the DID on the session
28 clientSession.did = session.did
29 await clientSession.save()
30
31 // Get the origin and determine appropriate redirect
32 const host = req.get('host') || ''
33 const protocol = req.protocol || 'http'
34 const baseUrl = `${protocol}://${host}`
35
36 ctx.logger.info(
37 `OAuth callback successful, redirecting to ${baseUrl}/oauth-callback`,
38 )
39
40 // Redirect to the frontend oauth-callback page
41 res.redirect('/oauth-callback')
42 } catch (err) {
43 ctx.logger.error({ err }, 'oauth callback failed')
44
45 // Handle error redirect - stay on same domain
46 res.redirect('/oauth-callback?error=auth')
47 }
48 })
49
50 // Login handler
51 router.post('/oauth/initiate', async (req, res) => {
52 // Validate
53 const handle = req.body?.handle
54 if (
55 typeof handle !== 'string' ||
56 !(isValidHandle(handle) || isValidUrl(handle))
57 ) {
58 res.status(400).json({ error: 'Invalid handle' })
59 return
60 }
61
62 // Initiate the OAuth flow
63 try {
64 const url = await ctx.oauthClient.authorize(handle, {
65 scope: 'atproto transition:generic',
66 })
67 res.json({ redirectUrl: url.toString() })
68 } catch (err) {
69 ctx.logger.error({ err }, 'oauth authorize failed')
70 const errorMsg =
71 err instanceof OAuthResolverError
72 ? err.message
73 : "Couldn't initiate login"
74 res.status(500).json({ error: errorMsg })
75 }
76 })
77
78 // Logout handler
79 router.post('/oauth/logout', async (req, res) => {
80 const session = await getSession(req, res)
81 session.destroy()
82 res.json({ success: true })
83 })
84
85 return router
86}
87
88function isValidUrl(url: string): boolean {
89 try {
90 const urlp = new URL(url)
91 // http or https
92 return urlp.protocol === 'http:' || urlp.protocol === 'https:'
93 } catch (error) {
94 return false
95 }
96}