forked from
nekomimi.pet/wisp.place-monorepo
Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
1import { Elysia } from 'elysia'
2import { NodeOAuthClient } from '@atproto/oauth-client-node'
3import { getSitesByDid, getDomainByDid } from '../lib/db'
4import { syncSitesFromPDS } from '../lib/sync-sites'
5import { authenticateRequest } from '../lib/wisp-auth'
6
7export const authRoutes = (client: NodeOAuthClient) => new Elysia()
8 .post('/api/auth/signin', async (c) => {
9 try {
10 const { handle } = await c.request.json()
11 const state = crypto.randomUUID()
12 const url = await client.authorize(handle, { state })
13 return { url: url.toString() }
14 } catch (err) {
15 console.error('Signin error', err)
16 return { error: 'Authentication failed' }
17 }
18 })
19 .get('/api/auth/callback', async (c) => {
20 try {
21 const params = new URLSearchParams(c.query)
22
23 // client.callback() validates the state parameter internally
24 // It will throw an error if state validation fails (CSRF protection)
25 const { session } = await client.callback(params)
26
27 if (!session) {
28 console.error('[Auth] OAuth callback failed: no session returned')
29 return c.redirect('/?error=auth_failed')
30 }
31
32 const cookieSession = c.cookie
33 cookieSession.did.value = session.did
34
35 // Sync sites from PDS to database cache
36 console.log('[Auth] Syncing sites from PDS for', session.did)
37 try {
38 const syncResult = await syncSitesFromPDS(session.did, session)
39 console.log(`[Auth] Sync complete: ${syncResult.synced} sites synced`)
40 if (syncResult.errors.length > 0) {
41 console.warn('[Auth] Sync errors:', syncResult.errors)
42 }
43 } catch (err) {
44 console.error('[Auth] Failed to sync sites:', err)
45 // Don't fail auth if sync fails, just log it
46 }
47
48 // Check if user has any sites or domain
49 const sites = await getSitesByDid(session.did)
50 const domain = await getDomainByDid(session.did)
51
52 // If no sites and no domain, redirect to onboarding
53 if (sites.length === 0 && !domain) {
54 return c.redirect('/onboarding')
55 }
56
57 return c.redirect('/editor')
58 } catch (err) {
59 // This catches state validation failures and other OAuth errors
60 console.error('[Auth] OAuth callback error:', err)
61 return c.redirect('/?error=auth_failed')
62 }
63 })
64 .post('/api/auth/logout', async (c) => {
65 try {
66 const cookieSession = c.cookie
67 const did = cookieSession.did?.value
68
69 // Clear the session cookie
70 cookieSession.did.value = ''
71 cookieSession.did.maxAge = 0
72
73 // If we have a DID, try to revoke the OAuth session
74 if (did && typeof did === 'string') {
75 try {
76 await client.revoke(did)
77 console.log('[Auth] Revoked OAuth session for', did)
78 } catch (err) {
79 console.error('[Auth] Failed to revoke session:', err)
80 // Continue with logout even if revoke fails
81 }
82 }
83
84 return { success: true }
85 } catch (err) {
86 console.error('[Auth] Logout error:', err)
87 return { error: 'Logout failed' }
88 }
89 })
90 .get('/api/auth/status', async (c) => {
91 try {
92 const auth = await authenticateRequest(client, c.cookie)
93
94 if (!auth) {
95 return { authenticated: false }
96 }
97
98 return {
99 authenticated: true,
100 did: auth.did
101 }
102 } catch (err) {
103 console.error('[Auth] Status check error:', err)
104 return { authenticated: false }
105 }
106 })