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((url, init) => auth.session.fetchHandler(url, init))
45
46 let handle = 'unknown'
47 try {
48 const profile = await agent.getProfile({ actor: auth.did })
49 handle = profile.data.handle
50 } catch (err) {
51 logger.error('[User] Failed to fetch profile', err)
52 }
53
54 return {
55 did: auth.did,
56 handle
57 }
58 } catch (err) {
59 logger.error('[User] Info error', err)
60 throw new Error('Failed to get user info')
61 }
62 })
63 .get('/sites', async ({ auth }) => {
64 try {
65 const sites = await getSitesByDid(auth.did)
66 return { sites }
67 } catch (err) {
68 logger.error('[User] Sites error', err)
69 throw new Error('Failed to get sites')
70 }
71 })
72 .get('/domains', async ({ auth }) => {
73 try {
74 // Get all wisp.place subdomains with mappings (up to 3)
75 const wispDomains = await getAllWispDomains(auth.did)
76
77 // Get custom domains
78 const customDomains = await getCustomDomainsByDid(auth.did)
79
80 return {
81 wispDomains: wispDomains.map(d => ({
82 domain: d.domain,
83 rkey: d.rkey || null
84 })),
85 customDomains
86 }
87 } catch (err) {
88 logger.error('[User] Domains error', err)
89 throw new Error('Failed to get domains')
90 }
91 })
92 .post('/sync', async ({ auth }) => {
93 try {
94 logger.debug('[User] Manual sync requested for', auth.did)
95 const result = await syncSitesFromPDS(auth.did, auth.session)
96
97 return {
98 success: true,
99 synced: result.synced,
100 errors: result.errors
101 }
102 } catch (err) {
103 logger.error('[User] Sync error', err)
104 throw new Error('Failed to sync sites')
105 }
106 })
107 .get('/site/:rkey/domains', async ({ auth, params }) => {
108 try {
109 const { rkey } = params
110 const domains = await getDomainsBySite(auth.did, rkey)
111
112 return {
113 rkey,
114 domains
115 }
116 } catch (err) {
117 logger.error('[User] Site domains error', err)
118 throw new Error('Failed to get domains for site')
119 }
120 })