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 = () => 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 } 40 ) 41 42 // Logout 43 .post('/logout', ({ cookie }) => { 44 const sessionId = cookie.admin_session?.value 45 if (sessionId && typeof sessionId === 'string') { 46 adminAuth.deleteSession(sessionId) 47 } 48 cookie.admin_session.remove() 49 return { success: true } 50 }) 51 52 // Check auth status 53 .get('/status', ({ cookie }) => { 54 const sessionId = cookie.admin_session?.value 55 if (!sessionId || typeof sessionId !== 'string') { 56 return { authenticated: false } 57 } 58 59 const session = adminAuth.verifySession(sessionId) 60 if (!session) { 61 return { authenticated: false } 62 } 63 64 return { 65 authenticated: true, 66 username: session.username 67 } 68 }) 69 70 // Get logs (protected) 71 .get('/logs', async ({ query, cookie, set }) => { 72 const check = requireAdmin({ cookie, set }) 73 if (check) return check 74 75 const filter: any = {} 76 77 if (query.level) filter.level = query.level 78 if (query.service) filter.service = query.service 79 if (query.search) filter.search = query.search 80 if (query.eventType) filter.eventType = query.eventType 81 if (query.limit) filter.limit = parseInt(query.limit as string) 82 83 // Get logs from main app 84 const mainLogs = logCollector.getLogs(filter) 85 86 // Get logs from hosting service 87 let hostingLogs: any[] = [] 88 try { 89 const hostingPort = process.env.HOSTING_PORT || '3001' 90 const params = new URLSearchParams() 91 if (query.level) params.append('level', query.level as string) 92 if (query.service) params.append('service', query.service as string) 93 if (query.search) params.append('search', query.search as string) 94 if (query.eventType) params.append('eventType', query.eventType as string) 95 params.append('limit', String(filter.limit || 100)) 96 97 const response = await fetch(`http://localhost:${hostingPort}/__internal__/observability/logs?${params}`) 98 if (response.ok) { 99 const data = await response.json() 100 hostingLogs = data.logs 101 } 102 } catch (err) { 103 // Hosting service might not be running 104 } 105 106 // Merge and sort by timestamp 107 const allLogs = [...mainLogs, ...hostingLogs].sort((a, b) => 108 new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() 109 ) 110 111 return { logs: allLogs.slice(0, filter.limit || 100) } 112 }) 113 114 // Get errors (protected) 115 .get('/errors', async ({ query, cookie, set }) => { 116 const check = requireAdmin({ cookie, set }) 117 if (check) return check 118 119 const filter: any = {} 120 121 if (query.service) filter.service = query.service 122 if (query.limit) filter.limit = parseInt(query.limit as string) 123 124 // Get errors from main app 125 const mainErrors = errorTracker.getErrors(filter) 126 127 // Get errors from hosting service 128 let hostingErrors: any[] = [] 129 try { 130 const hostingPort = process.env.HOSTING_PORT || '3001' 131 const params = new URLSearchParams() 132 if (query.service) params.append('service', query.service as string) 133 params.append('limit', String(filter.limit || 100)) 134 135 const response = await fetch(`http://localhost:${hostingPort}/__internal__/observability/errors?${params}`) 136 if (response.ok) { 137 const data = await response.json() 138 hostingErrors = data.errors 139 } 140 } catch (err) { 141 // Hosting service might not be running 142 } 143 144 // Merge and sort by last seen 145 const allErrors = [...mainErrors, ...hostingErrors].sort((a, b) => 146 new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime() 147 ) 148 149 return { errors: allErrors.slice(0, filter.limit || 100) } 150 }) 151 152 // Get metrics (protected) 153 .get('/metrics', async ({ query, cookie, set }) => { 154 const check = requireAdmin({ cookie, set }) 155 if (check) return check 156 157 const timeWindow = query.timeWindow 158 ? parseInt(query.timeWindow as string) 159 : 3600000 // 1 hour default 160 161 const mainAppStats = metricsCollector.getStats('main-app', timeWindow) 162 const overallStats = metricsCollector.getStats(undefined, timeWindow) 163 164 // Get hosting service stats from its own endpoint 165 let hostingServiceStats = { 166 totalRequests: 0, 167 avgDuration: 0, 168 p50Duration: 0, 169 p95Duration: 0, 170 p99Duration: 0, 171 errorRate: 0, 172 requestsPerMinute: 0 173 } 174 175 try { 176 const hostingPort = process.env.HOSTING_PORT || '3001' 177 const response = await fetch(`http://localhost:${hostingPort}/__internal__/observability/metrics?timeWindow=${timeWindow}`) 178 if (response.ok) { 179 const data = await response.json() 180 hostingServiceStats = data.stats 181 } 182 } catch (err) { 183 // Hosting service might not be running 184 } 185 186 return { 187 overall: overallStats, 188 mainApp: mainAppStats, 189 hostingService: hostingServiceStats, 190 timeWindow 191 } 192 }) 193 194 // Get database stats (protected) 195 .get('/database', async ({ cookie, set }) => { 196 const check = requireAdmin({ cookie, set }) 197 if (check) return check 198 199 try { 200 // Get total counts 201 const allSitesResult = await db`SELECT COUNT(*) as count FROM sites` 202 const wispSubdomainsResult = await db`SELECT COUNT(*) as count FROM domains WHERE domain LIKE '%.wisp.place'` 203 const customDomainsResult = await db`SELECT COUNT(*) as count FROM custom_domains WHERE verified = true` 204 205 // Get recent sites (including those without domains) 206 const recentSites = await db` 207 SELECT 208 s.did, 209 s.rkey, 210 s.display_name, 211 s.created_at, 212 d.domain as subdomain 213 FROM sites s 214 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place' 215 ORDER BY s.created_at DESC 216 LIMIT 10 217 ` 218 219 // Get recent domains 220 const recentDomains = await db`SELECT domain, did, rkey, verified, created_at FROM custom_domains ORDER BY created_at DESC LIMIT 10` 221 222 return { 223 stats: { 224 totalSites: allSitesResult[0].count, 225 totalWispSubdomains: wispSubdomainsResult[0].count, 226 totalCustomDomains: customDomainsResult[0].count 227 }, 228 recentSites: recentSites, 229 recentDomains: recentDomains 230 } 231 } catch (error) { 232 set.status = 500 233 return { 234 error: 'Failed to fetch database stats', 235 message: error instanceof Error ? error.message : String(error) 236 } 237 } 238 }) 239 240 // Get sites listing (protected) 241 .get('/sites', async ({ query, cookie, set }) => { 242 const check = requireAdmin({ cookie, set }) 243 if (check) return check 244 245 const limit = query.limit ? parseInt(query.limit as string) : 50 246 const offset = query.offset ? parseInt(query.offset as string) : 0 247 248 try { 249 const sites = await db` 250 SELECT 251 s.did, 252 s.rkey, 253 s.display_name, 254 s.created_at, 255 d.domain as subdomain 256 FROM sites s 257 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place' 258 ORDER BY s.created_at DESC 259 LIMIT ${limit} OFFSET ${offset} 260 ` 261 262 const customDomains = await db` 263 SELECT 264 domain, 265 did, 266 rkey, 267 verified, 268 created_at 269 FROM custom_domains 270 ORDER BY created_at DESC 271 LIMIT ${limit} OFFSET ${offset} 272 ` 273 274 return { 275 sites: sites, 276 customDomains: customDomains 277 } 278 } catch (error) { 279 set.status = 500 280 return { 281 error: 'Failed to fetch sites', 282 message: error instanceof Error ? error.message : String(error) 283 } 284 } 285 }) 286 287 // Get system health (protected) 288 .get('/health', ({ cookie, set }) => { 289 const check = requireAdmin({ cookie, set }) 290 if (check) return check 291 292 const uptime = process.uptime() 293 const memory = process.memoryUsage() 294 295 return { 296 uptime: Math.floor(uptime), 297 memory: { 298 heapUsed: Math.round(memory.heapUsed / 1024 / 1024), // MB 299 heapTotal: Math.round(memory.heapTotal / 1024 / 1024), // MB 300 rss: Math.round(memory.rss / 1024 / 1024) // MB 301 }, 302 timestamp: new Date().toISOString() 303 } 304 }) 305