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