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