Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { Elysia, t } from 'elysia' 2import { requireAuth } from '../lib/wisp-auth' 3import { NodeOAuthClient } from '@atproto/oauth-client-node' 4import { Agent } from '@atproto/api' 5import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo, getDomainsBySite, getAllWispDomains } from '../lib/db' 6import { syncSitesFromPDS } from '../lib/sync-sites' 7import { createLogger } from '@wisp/observability' 8 9const logger = createLogger('main-app') 10 11export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => 12 new Elysia({ 13 prefix: '/api/user', 14 cookie: { 15 secrets: cookieSecret, 16 sign: ['did'] 17 } 18 }) 19 .derive(async ({ cookie }) => { 20 const auth = await requireAuth(client, cookie) 21 return { auth } 22 }) 23 .get('/status', async ({ auth }) => { 24 try { 25 // Check if user has any sites 26 const sites = await getSitesByDid(auth.did) 27 28 // Check if user has claimed a domain 29 const domain = await getDomainByDid(auth.did) 30 31 return { 32 did: auth.did, 33 hasSites: sites.length > 0, 34 hasDomain: !!domain, 35 domain: domain || null, 36 sitesCount: sites.length 37 } 38 } catch (err) { 39 logger.error('[User] Status error', err) 40 throw new Error('Failed to get user status') 41 } 42 }) 43 .get('/info', async ({ auth }) => { 44 try { 45 // Get user's handle from AT Protocol 46 const agent = new Agent(auth.session) 47 48 let handle = 'unknown' 49 try { 50 console.log('[User] Attempting to fetch profile for DID:', auth.did) 51 const profile = await agent.getProfile({ actor: auth.did }) 52 console.log('[User] Profile fetched successfully:', profile.data.handle) 53 handle = profile.data.handle 54 } catch (err) { 55 console.error('[User] Failed to fetch profile - Full error:', err) 56 console.error('[User] Error message:', err instanceof Error ? err.message : String(err)) 57 console.error('[User] Error stack:', err instanceof Error ? err.stack : 'No stack') 58 logger.error('[User] Failed to fetch profile', err) 59 } 60 61 return { 62 did: auth.did, 63 handle 64 } 65 } catch (err) { 66 logger.error('[User] Info error', err) 67 throw new Error('Failed to get user info') 68 } 69 }) 70 .get('/sites', async ({ auth }) => { 71 try { 72 const sites = await getSitesByDid(auth.did) 73 return { sites } 74 } catch (err) { 75 logger.error('[User] Sites error', err) 76 throw new Error('Failed to get sites') 77 } 78 }) 79 .get('/domains', async ({ auth }) => { 80 try { 81 // Get all wisp.place subdomains with mappings (up to 3) 82 const wispDomains = await getAllWispDomains(auth.did) 83 84 // Get custom domains 85 const customDomains = await getCustomDomainsByDid(auth.did) 86 87 return { 88 wispDomains: wispDomains.map(d => ({ 89 domain: d.domain, 90 rkey: d.rkey || null 91 })), 92 customDomains 93 } 94 } catch (err) { 95 logger.error('[User] Domains error', err) 96 throw new Error('Failed to get domains') 97 } 98 }) 99 .post('/sync', async ({ auth }) => { 100 try { 101 logger.debug('[User] Manual sync requested for', { did: auth.did }) 102 const result = await syncSitesFromPDS(auth.did, auth.session) 103 104 return { 105 success: true, 106 synced: result.synced, 107 errors: result.errors 108 } 109 } catch (err) { 110 logger.error('[User] Sync error', err) 111 throw new Error('Failed to sync sites') 112 } 113 }) 114 .get('/site/:rkey/domains', async ({ auth, params }) => { 115 try { 116 const { rkey } = params 117 const domains = await getDomainsBySite(auth.did, rkey) 118 119 return { 120 rkey, 121 domains 122 } 123 } catch (err) { 124 logger.error('[User] Site domains error', err) 125 throw new Error('Failed to get domains for site') 126 } 127 })