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