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