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