Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1import { Elysia } 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 } from '../lib/db'
6import { syncSitesFromPDS } from '../lib/sync-sites'
7import { logger } from '../lib/logger'
8
9export const userRoutes = (client: NodeOAuthClient) =>
10 new Elysia({ prefix: '/api/user' })
11 .derive(async ({ cookie }) => {
12 const auth = await requireAuth(client, cookie)
13 return { auth }
14 })
15 .get('/status', async ({ auth }) => {
16 try {
17 // Check if user has any sites
18 const sites = await getSitesByDid(auth.did)
19
20 // Check if user has claimed a domain
21 const domain = await getDomainByDid(auth.did)
22
23 return {
24 did: auth.did,
25 hasSites: sites.length > 0,
26 hasDomain: !!domain,
27 domain: domain || null,
28 sitesCount: sites.length
29 }
30 } catch (err) {
31 logger.error('[User] Status error', err)
32 throw new Error('Failed to get user status')
33 }
34 })
35 .get('/info', async ({ auth }) => {
36 try {
37 // Get user's handle from AT Protocol
38 const agent = new Agent((url, init) => auth.session.fetchHandler(url, init))
39
40 let handle = 'unknown'
41 try {
42 const profile = await agent.getProfile({ actor: auth.did })
43 handle = profile.data.handle
44 } catch (err) {
45 logger.error('[User] Failed to fetch profile', err)
46 }
47
48 return {
49 did: auth.did,
50 handle
51 }
52 } catch (err) {
53 logger.error('[User] Info error', err)
54 throw new Error('Failed to get user info')
55 }
56 })
57 .get('/sites', async ({ auth }) => {
58 try {
59 const sites = await getSitesByDid(auth.did)
60 return { sites }
61 } catch (err) {
62 logger.error('[User] Sites error', err)
63 throw new Error('Failed to get sites')
64 }
65 })
66 .get('/domains', async ({ auth }) => {
67 try {
68 // Get wisp.place subdomain with mapping
69 const wispDomainInfo = await getWispDomainInfo(auth.did)
70
71 // Get custom domains
72 const customDomains = await getCustomDomainsByDid(auth.did)
73
74 return {
75 wispDomain: wispDomainInfo ? {
76 domain: wispDomainInfo.domain,
77 rkey: wispDomainInfo.rkey || null
78 } : null,
79 customDomains
80 }
81 } catch (err) {
82 logger.error('[User] Domains error', err)
83 throw new Error('Failed to get domains')
84 }
85 })
86 .post('/sync', async ({ auth }) => {
87 try {
88 logger.debug('[User] Manual sync requested for', auth.did)
89 const result = await syncSitesFromPDS(auth.did, auth.session)
90
91 return {
92 success: true,
93 synced: result.synced,
94 errors: result.errors
95 }
96 } catch (err) {
97 logger.error('[User] Sync error', err)
98 throw new Error('Failed to sync sites')
99 }
100 })
101 .get('/site/:rkey/domains', async ({ auth, params }) => {
102 try {
103 const { rkey } = params
104 const domains = await getDomainsBySite(auth.did, rkey)
105
106 return {
107 rkey,
108 domains
109 }
110 } catch (err) {
111 logger.error('[User] Site domains error', err)
112 throw new Error('Failed to get domains for site')
113 }
114 })