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