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 })