Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
96
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 13cfcbbb8726fcec6c161f046120016bc32cbea6 367 lines 10 kB view raw
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