Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 11 kB view raw
1// Admin API routes 2import { Elysia, t } from 'elysia' 3import { adminAuth, requireAdmin } from '../lib/admin-auth' 4import { logCollector, errorTracker, metricsCollector } from '@wisp/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