Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1// Admin API routes
2import { Elysia, t } from 'elysia'
3import { adminAuth, requireAdmin } from '../lib/admin-auth'
4import { logCollector, errorTracker, metricsCollector } from '../lib/observability'
5import { db } from '../lib/db'
6
7export const adminRoutes = (cookieSecret: string) =>
8 new Elysia({ prefix: '/api/admin' })
9 // Login
10 .post(
11 '/login',
12 async ({ body, cookie, set }) => {
13 const { username, password } = body
14
15 const valid = await adminAuth.verify(username, password)
16 if (!valid) {
17 set.status = 401
18 return { error: 'Invalid credentials' }
19 }
20
21 const sessionId = adminAuth.createSession(username)
22
23 // Set cookie
24 cookie.admin_session.set({
25 value: sessionId,
26 httpOnly: true,
27 secure: process.env.NODE_ENV === 'production',
28 sameSite: 'lax',
29 maxAge: 24 * 60 * 60 // 24 hours
30 })
31
32 return { success: true }
33 },
34 {
35 body: t.Object({
36 username: t.String(),
37 password: t.String()
38 }),
39 cookie: t.Cookie({
40 admin_session: t.String()
41 }, {
42 secrets: cookieSecret,
43 sign: ['admin_session']
44 })
45 }
46 )
47
48 // Logout
49 .post('/logout', ({ cookie }) => {
50 const sessionId = cookie.admin_session?.value
51 if (sessionId && typeof sessionId === 'string') {
52 adminAuth.deleteSession(sessionId)
53 }
54 cookie.admin_session.remove()
55 return { success: true }
56 }, {
57 cookie: t.Cookie({
58 admin_session: t.Optional(t.String())
59 }, {
60 secrets: cookieSecret,
61 sign: ['admin_session']
62 })
63 })
64
65 // Check auth status
66 .get('/status', ({ cookie }) => {
67 const sessionId = cookie.admin_session?.value
68 if (!sessionId || typeof sessionId !== 'string') {
69 return { authenticated: false }
70 }
71
72 const session = adminAuth.verifySession(sessionId)
73 if (!session) {
74 return { authenticated: false }
75 }
76
77 return {
78 authenticated: true,
79 username: session.username
80 }
81 }, {
82 cookie: t.Cookie({
83 admin_session: t.Optional(t.String())
84 }, {
85 secrets: cookieSecret,
86 sign: ['admin_session']
87 })
88 })
89
90 // Get logs (protected)
91 .get('/logs', async ({ query, cookie, set }) => {
92 const check = requireAdmin({ cookie, set })
93 if (check) return check
94
95 const filter: any = {}
96
97 if (query.level) filter.level = query.level
98 if (query.service) filter.service = query.service
99 if (query.search) filter.search = query.search
100 if (query.eventType) filter.eventType = query.eventType
101 if (query.limit) filter.limit = parseInt(query.limit as string)
102
103 // Get logs from main app
104 const mainLogs = logCollector.getLogs(filter)
105
106 // Get logs from hosting service
107 let hostingLogs: any[] = []
108 try {
109 const hostingPort = process.env.HOSTING_PORT || '3001'
110 const params = new URLSearchParams()
111 if (query.level) params.append('level', query.level as string)
112 if (query.service) params.append('service', query.service as string)
113 if (query.search) params.append('search', query.search as string)
114 if (query.eventType) params.append('eventType', query.eventType as string)
115 params.append('limit', String(filter.limit || 100))
116
117 const response = await fetch(`http://localhost:${hostingPort}/__internal__/observability/logs?${params}`)
118 if (response.ok) {
119 const data = await response.json()
120 hostingLogs = data.logs
121 }
122 } catch (err) {
123 // Hosting service might not be running
124 }
125
126 // Merge and sort by timestamp
127 const allLogs = [...mainLogs, ...hostingLogs].sort((a, b) =>
128 new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
129 )
130
131 return { logs: allLogs.slice(0, filter.limit || 100) }
132 }, {
133 cookie: t.Cookie({
134 admin_session: t.Optional(t.String())
135 }, {
136 secrets: cookieSecret,
137 sign: ['admin_session']
138 })
139 })
140
141 // Get errors (protected)
142 .get('/errors', async ({ query, cookie, set }) => {
143 const check = requireAdmin({ cookie, set })
144 if (check) return check
145
146 const filter: any = {}
147
148 if (query.service) filter.service = query.service
149 if (query.limit) filter.limit = parseInt(query.limit as string)
150
151 // Get errors from main app
152 const mainErrors = errorTracker.getErrors(filter)
153
154 // Get errors from hosting service
155 let hostingErrors: any[] = []
156 try {
157 const hostingPort = process.env.HOSTING_PORT || '3001'
158 const params = new URLSearchParams()
159 if (query.service) params.append('service', query.service as string)
160 params.append('limit', String(filter.limit || 100))
161
162 const response = await fetch(`http://localhost:${hostingPort}/__internal__/observability/errors?${params}`)
163 if (response.ok) {
164 const data = await response.json()
165 hostingErrors = data.errors
166 }
167 } catch (err) {
168 // Hosting service might not be running
169 }
170
171 // Merge and sort by last seen
172 const allErrors = [...mainErrors, ...hostingErrors].sort((a, b) =>
173 new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime()
174 )
175
176 return { errors: allErrors.slice(0, filter.limit || 100) }
177 }, {
178 cookie: t.Cookie({
179 admin_session: t.Optional(t.String())
180 }, {
181 secrets: cookieSecret,
182 sign: ['admin_session']
183 })
184 })
185
186 // Get metrics (protected)
187 .get('/metrics', async ({ query, cookie, set }) => {
188 const check = requireAdmin({ cookie, set })
189 if (check) return check
190
191 const timeWindow = query.timeWindow
192 ? parseInt(query.timeWindow as string)
193 : 3600000 // 1 hour default
194
195 const mainAppStats = metricsCollector.getStats('main-app', timeWindow)
196 const overallStats = metricsCollector.getStats(undefined, timeWindow)
197
198 // Get hosting service stats from its own endpoint
199 let hostingServiceStats = {
200 totalRequests: 0,
201 avgDuration: 0,
202 p50Duration: 0,
203 p95Duration: 0,
204 p99Duration: 0,
205 errorRate: 0,
206 requestsPerMinute: 0
207 }
208
209 try {
210 const hostingPort = process.env.HOSTING_PORT || '3001'
211 const response = await fetch(`http://localhost:${hostingPort}/__internal__/observability/metrics?timeWindow=${timeWindow}`)
212 if (response.ok) {
213 const data = await response.json()
214 hostingServiceStats = data.stats
215 }
216 } catch (err) {
217 // Hosting service might not be running
218 }
219
220 return {
221 overall: overallStats,
222 mainApp: mainAppStats,
223 hostingService: hostingServiceStats,
224 timeWindow
225 }
226 }, {
227 cookie: t.Cookie({
228 admin_session: t.Optional(t.String())
229 }, {
230 secrets: cookieSecret,
231 sign: ['admin_session']
232 })
233 })
234
235 // Get database stats (protected)
236 .get('/database', async ({ cookie, set }) => {
237 const check = requireAdmin({ cookie, set })
238 if (check) return check
239
240 try {
241 // Get total counts
242 const allSitesResult = await db`SELECT COUNT(*) as count FROM sites`
243 const wispSubdomainsResult = await db`SELECT COUNT(*) as count FROM domains WHERE domain LIKE '%.wisp.place'`
244 const customDomainsResult = await db`SELECT COUNT(*) as count FROM custom_domains WHERE verified = true`
245
246 // Get recent sites (including those without domains)
247 const recentSites = await db`
248 SELECT
249 s.did,
250 s.rkey,
251 s.display_name,
252 s.created_at,
253 d.domain as subdomain
254 FROM sites s
255 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place'
256 ORDER BY s.created_at DESC
257 LIMIT 10
258 `
259
260 // Get recent domains
261 const recentDomains = await db`SELECT domain, did, rkey, verified, created_at FROM custom_domains ORDER BY created_at DESC LIMIT 10`
262
263 return {
264 stats: {
265 totalSites: allSitesResult[0].count,
266 totalWispSubdomains: wispSubdomainsResult[0].count,
267 totalCustomDomains: customDomainsResult[0].count
268 },
269 recentSites: recentSites,
270 recentDomains: recentDomains
271 }
272 } catch (error) {
273 set.status = 500
274 return {
275 error: 'Failed to fetch database stats',
276 message: error instanceof Error ? error.message : String(error)
277 }
278 }
279 }, {
280 cookie: t.Cookie({
281 admin_session: t.Optional(t.String())
282 }, {
283 secrets: cookieSecret,
284 sign: ['admin_session']
285 })
286 })
287
288 // Get sites listing (protected)
289 .get('/sites', async ({ query, cookie, set }) => {
290 const check = requireAdmin({ cookie, set })
291 if (check) return check
292
293 const limit = query.limit ? parseInt(query.limit as string) : 50
294 const offset = query.offset ? parseInt(query.offset as string) : 0
295
296 try {
297 const sites = await db`
298 SELECT
299 s.did,
300 s.rkey,
301 s.display_name,
302 s.created_at,
303 d.domain as subdomain
304 FROM sites s
305 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place'
306 ORDER BY s.created_at DESC
307 LIMIT ${limit} OFFSET ${offset}
308 `
309
310 const customDomains = await db`
311 SELECT
312 domain,
313 did,
314 rkey,
315 verified,
316 created_at
317 FROM custom_domains
318 ORDER BY created_at DESC
319 LIMIT ${limit} OFFSET ${offset}
320 `
321
322 return {
323 sites: sites,
324 customDomains: customDomains
325 }
326 } catch (error) {
327 set.status = 500
328 return {
329 error: 'Failed to fetch sites',
330 message: error instanceof Error ? error.message : String(error)
331 }
332 }
333 }, {
334 cookie: t.Cookie({
335 admin_session: t.Optional(t.String())
336 }, {
337 secrets: cookieSecret,
338 sign: ['admin_session']
339 })
340 })
341
342 // Get system health (protected)
343 .get('/health', ({ cookie, set }) => {
344 const check = requireAdmin({ cookie, set })
345 if (check) return check
346
347 const uptime = process.uptime()
348 const memory = process.memoryUsage()
349
350 return {
351 uptime: Math.floor(uptime),
352 memory: {
353 heapUsed: Math.round(memory.heapUsed / 1024 / 1024), // MB
354 heapTotal: Math.round(memory.heapTotal / 1024 / 1024), // MB
355 rss: Math.round(memory.rss / 1024 / 1024) // MB
356 },
357 timestamp: new Date().toISOString()
358 }
359 }, {
360 cookie: t.Cookie({
361 admin_session: t.Optional(t.String())
362 }, {
363 secrets: cookieSecret,
364 sign: ['admin_session']
365 })
366 })
367