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