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.Optional(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 hostingServiceUrl = process.env.HOSTING_SERVICE_URL || `http://localhost:${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(`${hostingServiceUrl}/__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 hostingServiceUrl = process.env.HOSTING_SERVICE_URL || `http://localhost:${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(`${hostingServiceUrl}/__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 hostingServiceUrl = process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}`
211 const response = await fetch(`${hostingServiceUrl}/__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 cache stats (protected)
289 .get('/cache', async ({ cookie, set }) => {
290 const check = requireAdmin({ cookie, set })
291 if (check) return check
292
293 try {
294 const hostingServiceUrl = process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}`
295 const response = await fetch(`${hostingServiceUrl}/__internal__/observability/cache`)
296
297 if (response.ok) {
298 const data = await response.json()
299 return data
300 } else {
301 set.status = 503
302 return {
303 error: 'Failed to fetch cache stats from hosting service',
304 message: 'Hosting service unavailable'
305 }
306 }
307 } catch (error) {
308 set.status = 500
309 return {
310 error: 'Failed to fetch cache stats',
311 message: error instanceof Error ? error.message : String(error)
312 }
313 }
314 }, {
315 cookie: t.Cookie({
316 admin_session: t.Optional(t.String())
317 }, {
318 secrets: cookieSecret,
319 sign: ['admin_session']
320 })
321 })
322
323 // Get sites listing (protected)
324 .get('/sites', async ({ query, cookie, set }) => {
325 const check = requireAdmin({ cookie, set })
326 if (check) return check
327
328 const limit = query.limit ? parseInt(query.limit as string) : 50
329 const offset = query.offset ? parseInt(query.offset as string) : 0
330
331 try {
332 const sites = await db`
333 SELECT
334 s.did,
335 s.rkey,
336 s.display_name,
337 s.created_at,
338 d.domain as subdomain
339 FROM sites s
340 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place'
341 ORDER BY s.created_at DESC
342 LIMIT ${limit} OFFSET ${offset}
343 `
344
345 const customDomains = await db`
346 SELECT
347 domain,
348 did,
349 rkey,
350 verified,
351 created_at
352 FROM custom_domains
353 ORDER BY created_at DESC
354 LIMIT ${limit} OFFSET ${offset}
355 `
356
357 return {
358 sites: sites,
359 customDomains: customDomains
360 }
361 } catch (error) {
362 set.status = 500
363 return {
364 error: 'Failed to fetch sites',
365 message: error instanceof Error ? error.message : String(error)
366 }
367 }
368 }, {
369 cookie: t.Cookie({
370 admin_session: t.Optional(t.String())
371 }, {
372 secrets: cookieSecret,
373 sign: ['admin_session']
374 })
375 })
376
377 // Get system health (protected)
378 .get('/health', ({ cookie, set }) => {
379 const check = requireAdmin({ cookie, set })
380 if (check) return check
381
382 const uptime = process.uptime()
383 const memory = process.memoryUsage()
384
385 return {
386 uptime: Math.floor(uptime),
387 memory: {
388 heapUsed: Math.round(memory.heapUsed / 1024 / 1024), // MB
389 heapTotal: Math.round(memory.heapTotal / 1024 / 1024), // MB
390 rss: Math.round(memory.rss / 1024 / 1024) // MB
391 },
392 timestamp: new Date().toISOString()
393 }
394 }, {
395 cookie: t.Cookie({
396 admin_session: t.Optional(t.String())
397 }, {
398 secrets: cookieSecret,
399 sign: ['admin_session']
400 })
401 })
402