tangled
alpha
login
or
join now
xan.lol
/
wisp.place-monorepo
forked from
nekomimi.pet/wisp.place-monorepo
0
fork
atom
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
0
fork
atom
overview
issues
pulls
pipelines
api docs
nekomimi.pet
2 months ago
8a9517b8
60764ad8
+460
-2
8 changed files
expand all
collapse all
unified
split
apps
main-app
src
routes
admin.ts
auth.ts
domain.ts
site.ts
user.ts
wisp.ts
docs
astro.config.mjs
src
content
docs
reference
main-app-api.md
+49
-1
apps/main-app/src/routes/admin.ts
···
13
13
}
14
14
})
15
15
// Login
16
16
+
/**
17
17
+
* POST /api/admin/login
18
18
+
* Success: { success: true } with admin_session cookie set.
19
19
+
* Failure (401): { error: 'Invalid credentials' }
20
20
+
*/
16
21
.post(
17
22
'/login',
18
23
async ({ body, cookie, set }) => {
···
52
57
)
53
58
54
59
// Logout
60
60
+
/**
61
61
+
* POST /api/admin/logout
62
62
+
* Success: { success: true } and clears admin_session cookie.
63
63
+
*/
55
64
.post('/logout', ({ cookie }) => {
56
65
const sessionId = cookie.admin_session?.value
57
66
if (sessionId && typeof sessionId === 'string') {
···
69
78
})
70
79
71
80
// Check auth status
81
81
+
/**
82
82
+
* GET /api/admin/status
83
83
+
* Authenticated: { authenticated: true, username }
84
84
+
* Not authenticated: { authenticated: false }
85
85
+
*/
72
86
.get('/status', ({ cookie }) => {
73
87
const sessionId = cookie.admin_session?.value
74
88
if (!sessionId || typeof sessionId !== 'string') {
···
94
108
})
95
109
96
110
// Get logs (protected)
111
111
+
/**
112
112
+
* GET /api/admin/logs
113
113
+
* Success: { logs }
114
114
+
* Unauthorized (401): { error: 'Unauthorized' }
115
115
+
*/
97
116
.get('/logs', async ({ query, cookie, set }) => {
98
117
const check = requireAdmin({ cookie, set })
99
118
if (check) return check
···
145
164
})
146
165
147
166
// Get errors (protected)
167
167
+
/**
168
168
+
* GET /api/admin/errors
169
169
+
* Success: { errors }
170
170
+
* Unauthorized (401): { error: 'Unauthorized' }
171
171
+
*/
148
172
.get('/errors', async ({ query, cookie, set }) => {
149
173
const check = requireAdmin({ cookie, set })
150
174
if (check) return check
···
190
214
})
191
215
192
216
// Get metrics (protected)
217
217
+
/**
218
218
+
* GET /api/admin/metrics
219
219
+
* Success: { overall, mainApp, hostingService, timeWindow }
220
220
+
* Unauthorized (401): { error: 'Unauthorized' }
221
221
+
*/
193
222
.get('/metrics', async ({ query, cookie, set }) => {
194
223
const check = requireAdmin({ cookie, set })
195
224
if (check) return check
···
239
268
})
240
269
241
270
// Get database stats (protected)
271
271
+
/**
272
272
+
* GET /api/admin/database
273
273
+
* Success: { stats, recentSites, recentDomains }
274
274
+
* Failure (500): { error, message }
275
275
+
*/
242
276
.get('/database', async ({ cookie, set }) => {
243
277
const check = requireAdmin({ cookie, set })
244
278
if (check) return check
···
292
326
})
293
327
294
328
// Get cache stats (protected)
329
329
+
/**
330
330
+
* GET /api/admin/cache
331
331
+
* Success: hosting service cache stats payload.
332
332
+
* Failure (503|500): { error, message }
333
333
+
*/
295
334
.get('/cache', async ({ cookie, set }) => {
296
335
const check = requireAdmin({ cookie, set })
297
336
if (check) return check
···
327
366
})
328
367
329
368
// Get sites listing (protected)
369
369
+
/**
370
370
+
* GET /api/admin/sites
371
371
+
* Success: { sites, customDomains }
372
372
+
* Failure (500): { error, message }
373
373
+
*/
330
374
.get('/sites', async ({ query, cookie, set }) => {
331
375
const check = requireAdmin({ cookie, set })
332
376
if (check) return check
···
381
425
})
382
426
383
427
// Get system health (protected)
428
428
+
/**
429
429
+
* GET /api/admin/health
430
430
+
* Success: { uptime, memory, timestamp }
431
431
+
* Unauthorized (401): { error: 'Unauthorized' }
432
432
+
*/
384
433
.get('/health', ({ cookie, set }) => {
385
434
const check = requireAdmin({ cookie, set })
386
435
if (check) return check
···
405
454
sign: ['admin_session']
406
455
})
407
456
})
408
408
-
+25
apps/main-app/src/routes/auth.ts
···
13
13
sign: ['did']
14
14
}
15
15
})
16
16
+
/**
17
17
+
* GET /api/auth/login
18
18
+
* 302 redirect to the AT Protocol OAuth authorize URL.
19
19
+
* On error, redirects to /?error=missing_handle or /?error=auth_failed.
20
20
+
*/
16
21
.get('/api/auth/login', async (c) => {
17
22
// GET endpoint for initiating OAuth via atproto.wisp.place entryway
18
23
// Accepts: login_hint (handle) or pds (server)
···
42
47
return c.redirect('/?error=auth_failed')
43
48
}
44
49
})
50
50
+
/**
51
51
+
* POST /api/auth/signin
52
52
+
* Success: { url } where url is the OAuth authorize URL.
53
53
+
* Failure: { error, details }.
54
54
+
*/
45
55
.post('/api/auth/signin', async (c) => {
46
56
let handle = 'unknown'
47
57
try {
···
58
68
return { error: 'Authentication failed', details: err instanceof Error ? err.message : String(err) }
59
69
}
60
70
})
71
71
+
/**
72
72
+
* GET /api/auth/callback
73
73
+
* 302 redirect to /onboarding (new users) or /editor (existing users).
74
74
+
* On error, redirects to /?error=auth_failed.
75
75
+
*/
61
76
.get('/api/auth/callback', async (c) => {
62
77
try {
63
78
const params = new URLSearchParams(c.query)
···
111
126
return c.redirect('/?error=auth_failed')
112
127
}
113
128
})
129
129
+
/**
130
130
+
* POST /api/auth/logout
131
131
+
* Success: { success: true }
132
132
+
* Failure: { error: 'Logout failed' }
133
133
+
*/
114
134
.post('/api/auth/logout', async (c) => {
115
135
try {
116
136
const cookieSession = c.cookie
···
136
156
return { error: 'Logout failed' }
137
157
}
138
158
})
159
159
+
/**
160
160
+
* GET /api/auth/status
161
161
+
* Authenticated: { authenticated: true, did }
162
162
+
* Not authenticated: { authenticated: false }
163
163
+
*/
139
164
.get('/api/auth/status', async (c) => {
140
165
try {
141
166
const auth = await authenticateRequest(client, c.cookie)
+44
-1
apps/main-app/src/routes/domain.ts
···
35
35
}
36
36
})
37
37
// Public endpoints (no auth required)
38
38
+
/**
39
39
+
* GET /api/domain/check
40
40
+
* Success: { available, domain } or { available: false, reason: 'invalid' }.
41
41
+
* Failure: { available: false }.
42
42
+
*/
38
43
.get('/check', async ({ query }) => {
39
44
try {
40
45
const handle = (query.handle || "")
···
60
65
};
61
66
}
62
67
})
68
68
+
/**
69
69
+
* GET /api/domain/registered
70
70
+
* 200: { registered: true, type: 'wisp' | 'custom', domain, did, rkey, verified? }
71
71
+
* 404: { registered: false }
72
72
+
* 400: { error: 'Domain parameter required' }
73
73
+
*/
63
74
.get('/registered', async ({ query, set }) => {
64
75
try {
65
76
const domain = (query.domain || "").trim().toLowerCase();
···
90
101
const auth = await requireAuth(client, cookie)
91
102
return { auth }
92
103
})
104
104
+
/**
105
105
+
* POST /api/domain/claim
106
106
+
* Success: { success: true, domain }
107
107
+
*/
93
108
.post('/claim', async ({ body, auth }) => {
94
109
try {
95
110
const { handle } = body as { handle?: string };
···
133
148
throw new Error(`Failed to claim: ${err instanceof Error ? err.message : 'Unknown error'}`);
134
149
}
135
150
})
151
151
+
/**
152
152
+
* POST /api/domain/update
153
153
+
* Success: { success: true, domain }
154
154
+
*/
136
155
.post('/update', async ({ body, auth }) => {
137
156
try {
138
157
const { handle } = body as { handle?: string };
···
175
194
throw new Error(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`);
176
195
}
177
196
})
197
197
+
/**
198
198
+
* POST /api/domain/custom/add
199
199
+
* Success: { success: true, id, domain, verified: false }
200
200
+
*/
178
201
.post('/custom/add', async ({ body, auth }) => {
179
202
try {
180
203
const { domain } = body as { domain: string };
···
266
289
throw new Error(`Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}`);
267
290
}
268
291
})
292
292
+
/**
293
293
+
* POST /api/domain/custom/verify
294
294
+
* Success: { success: true, verified, error, found }
295
295
+
*/
269
296
.post('/custom/verify', async ({ body, auth }) => {
270
297
try {
271
298
const { id } = body as { id: string };
···
294
321
throw new Error(`Failed to verify domain: ${err instanceof Error ? err.message : 'Unknown error'}`);
295
322
}
296
323
})
324
324
+
/**
325
325
+
* DELETE /api/domain/custom/:id
326
326
+
* Success: { success: true }
327
327
+
*/
297
328
.delete('/custom/:id', async ({ params, auth }) => {
298
329
try {
299
330
const { id } = params;
···
317
348
throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`);
318
349
}
319
350
})
351
351
+
/**
352
352
+
* POST /api/domain/wisp/map-site
353
353
+
* Success: { success: true }
354
354
+
*/
320
355
.post('/wisp/map-site', async ({ body, auth }) => {
321
356
try {
322
357
const { domain, siteRkey } = body as { domain: string; siteRkey: string | null };
···
334
369
throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`);
335
370
}
336
371
})
372
372
+
/**
373
373
+
* DELETE /api/domain/wisp/:domain
374
374
+
* Success: { success: true }
375
375
+
*/
337
376
.delete('/wisp/:domain', async ({ params, auth }) => {
338
377
try {
339
378
const { domain } = params;
···
373
412
throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`);
374
413
}
375
414
})
415
415
+
/**
416
416
+
* POST /api/domain/custom/:id/map-site
417
417
+
* Success: { success: true }
418
418
+
*/
376
419
.post('/custom/:id/map-site', async ({ params, body, auth }) => {
377
420
try {
378
421
const { id } = params;
···
396
439
logger.error('[Domain] Custom domain map error', err);
397
440
throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`);
398
441
}
399
399
-
});
442
442
+
});
+15
apps/main-app/src/routes/site.ts
···
20
20
const auth = await requireAuth(client, cookie)
21
21
return { auth }
22
22
})
23
23
+
/**
24
24
+
* DELETE /api/site/:rkey
25
25
+
* Success: { success: true, message }
26
26
+
* Failure: { success: false, error }
27
27
+
*/
23
28
.delete('/:rkey', async ({ params, auth }) => {
24
29
const { rkey } = params
25
30
···
120
125
}
121
126
}
122
127
})
128
128
+
/**
129
129
+
* GET /api/site/:rkey/settings
130
130
+
* Success: place.wisp.settings record or default settings object.
131
131
+
* Failure: { success: false, error }
132
132
+
*/
123
133
.get('/:rkey/settings', async ({ params, auth }) => {
124
134
const { rkey } = params
125
135
···
171
181
}
172
182
}
173
183
})
184
184
+
/**
185
185
+
* POST /api/site/:rkey/settings
186
186
+
* Success: { success: true, uri, cid }
187
187
+
* Failure: { success: false, error }
188
188
+
*/
174
189
.post('/:rkey/settings', async ({ params, body, auth }) => {
175
190
const { rkey } = params
176
191
+24
apps/main-app/src/routes/user.ts
···
21
21
const auth = await requireAuth(client, cookie)
22
22
return { auth }
23
23
})
24
24
+
/**
25
25
+
* GET /api/user/status
26
26
+
* Success: { did, hasSites, hasDomain, domain, sitesCount }
27
27
+
*/
24
28
.get('/status', async ({ auth }) => {
25
29
try {
26
30
// Check if user has any sites
···
41
45
throw new Error('Failed to get user status')
42
46
}
43
47
})
48
48
+
/**
49
49
+
* GET /api/user/info
50
50
+
* Success: { did, handle }
51
51
+
*/
44
52
.get('/info', async ({ auth }) => {
45
53
try {
46
54
let handle = 'unknown'
···
65
73
throw new Error('Failed to get user info')
66
74
}
67
75
})
76
76
+
/**
77
77
+
* GET /api/user/sites
78
78
+
* Success: { sites }
79
79
+
*/
68
80
.get('/sites', async ({ auth }) => {
69
81
try {
70
82
const sites = await getSitesByDid(auth.did)
···
74
86
throw new Error('Failed to get sites')
75
87
}
76
88
})
89
89
+
/**
90
90
+
* GET /api/user/domains
91
91
+
* Success: { wispDomains: [{ domain, rkey }], customDomains }
92
92
+
*/
77
93
.get('/domains', async ({ auth }) => {
78
94
try {
79
95
// Get all wisp.place subdomains with mappings (up to 3)
···
94
110
throw new Error('Failed to get domains')
95
111
}
96
112
})
113
113
+
/**
114
114
+
* POST /api/user/sync
115
115
+
* Success: { success: true, synced, errors }
116
116
+
*/
97
117
.post('/sync', async ({ auth }) => {
98
118
try {
99
119
logger.debug('[User] Manual sync requested for', { did: auth.did })
···
109
129
throw new Error('Failed to sync sites')
110
130
}
111
131
})
132
132
+
/**
133
133
+
* GET /api/user/site/:rkey/domains
134
134
+
* Success: { rkey, domains }
135
135
+
*/
112
136
.get('/site/:rkey/domains', async ({ auth, params }) => {
113
137
try {
114
138
const { rkey } = params
+12
apps/main-app/src/routes/wisp.ts
···
861
861
const auth = await requireAuth(client, cookie)
862
862
return { auth }
863
863
})
864
864
+
/**
865
865
+
* GET /wisp/upload-progress/:jobId
866
866
+
* SSE stream of upload progress events for the current user.
867
867
+
* 404: { error: 'Job not found' }
868
868
+
* 403: { error: 'Unauthorized' }
869
869
+
*/
864
870
.get(
865
871
'/upload-progress/:jobId',
866
872
async ({ params: { jobId }, auth, set }) => {
···
951
957
return new Response(stream);
952
958
}
953
959
)
960
960
+
/**
961
961
+
* POST /wisp/upload-files
962
962
+
* Success (empty upload): { success: true, uri, cid, fileCount: 0, siteName }
963
963
+
* Success (async upload): { success: true, jobId, message }
964
964
+
* Failure: throws error with message "Failed to upload files: ..."
965
965
+
*/
954
966
.post(
955
967
'/upload-files',
956
968
async ({ body, auth }) => {
+4
docs/astro.config.mjs
···
30
30
{ label: 'Redirects & Rewrites', slug: 'redirects' },
31
31
],
32
32
},
33
33
+
{
34
34
+
label: 'Reference',
35
35
+
autogenerate: { directory: 'reference' },
36
36
+
},
33
37
],
34
38
customCss: ['./src/styles/custom.css'],
35
39
}),
+287
docs/src/content/docs/reference/main-app-api.md
···
1
1
+
---
2
2
+
title: Main App API
3
3
+
description: Expected responses from the main-app Elysia routes.
4
4
+
---
5
5
+
6
6
+
These endpoints power the main wisp.place backend (Bun + Elysia). Responses below are the shapes returned by the handlers in `apps/main-app/src/routes/*`.
7
7
+
8
8
+
Notes:
9
9
+
- Authenticated routes rely on the signed `did` cookie. If authentication fails, the handler throws and Elysia returns an error response.
10
10
+
- Admin routes rely on the signed `admin_session` cookie. Unauthorized requests return `401 { error: 'Unauthorized' }`.
11
11
+
12
12
+
## Auth (`/api/auth/*`)
13
13
+
14
14
+
### `GET /api/auth/login`
15
15
+
Redirects to the AT Protocol OAuth authorize URL.
16
16
+
17
17
+
- **302** → OAuth URL
18
18
+
- **302** → `/?error=missing_handle` if no handle/PDS provided
19
19
+
- **302** → `/?error=auth_failed` on failure
20
20
+
21
21
+
### `POST /api/auth/signin`
22
22
+
```json
23
23
+
{ "url": "https://..." }
24
24
+
```
25
25
+
On failure:
26
26
+
```json
27
27
+
{ "error": "Authentication failed", "details": "..." }
28
28
+
```
29
29
+
30
30
+
### `GET /api/auth/callback`
31
31
+
Redirects after OAuth completes.
32
32
+
33
33
+
- **302** → `/onboarding` (no sites or domain)
34
34
+
- **302** → `/editor` (existing user)
35
35
+
- **302** → `/?error=auth_failed` on failure
36
36
+
37
37
+
### `POST /api/auth/logout`
38
38
+
```json
39
39
+
{ "success": true }
40
40
+
```
41
41
+
On failure:
42
42
+
```json
43
43
+
{ "error": "Logout failed" }
44
44
+
```
45
45
+
46
46
+
### `GET /api/auth/status`
47
47
+
Authenticated:
48
48
+
```json
49
49
+
{ "authenticated": true, "did": "did:plc:..." }
50
50
+
```
51
51
+
Not authenticated:
52
52
+
```json
53
53
+
{ "authenticated": false }
54
54
+
```
55
55
+
56
56
+
## User (`/api/user/*`)
57
57
+
58
58
+
### `GET /api/user/status`
59
59
+
```json
60
60
+
{
61
61
+
"did": "did:plc:...",
62
62
+
"hasSites": true,
63
63
+
"hasDomain": false,
64
64
+
"domain": null,
65
65
+
"sitesCount": 3
66
66
+
}
67
67
+
```
68
68
+
69
69
+
### `GET /api/user/info`
70
70
+
```json
71
71
+
{ "did": "did:plc:...", "handle": "user.bsky.social" }
72
72
+
```
73
73
+
74
74
+
### `GET /api/user/sites`
75
75
+
```json
76
76
+
{ "sites": [/* site rows */] }
77
77
+
```
78
78
+
79
79
+
### `GET /api/user/domains`
80
80
+
```json
81
81
+
{
82
82
+
"wispDomains": [{ "domain": "name.wisp.place", "rkey": "site-rkey" }],
83
83
+
"customDomains": [/* custom domain rows */]
84
84
+
}
85
85
+
```
86
86
+
87
87
+
### `POST /api/user/sync`
88
88
+
```json
89
89
+
{ "success": true, "synced": 2, "errors": [] }
90
90
+
```
91
91
+
92
92
+
### `GET /api/user/site/:rkey/domains`
93
93
+
```json
94
94
+
{ "rkey": "site-rkey", "domains": [/* domain rows */] }
95
95
+
```
96
96
+
97
97
+
## Domain (`/api/domain/*`)
98
98
+
99
99
+
### `GET /api/domain/check`
100
100
+
```json
101
101
+
{ "available": true, "domain": "name.wisp.place" }
102
102
+
```
103
103
+
Invalid handle:
104
104
+
```json
105
105
+
{ "available": false, "reason": "invalid" }
106
106
+
```
107
107
+
108
108
+
### `GET /api/domain/registered`
109
109
+
Registered:
110
110
+
```json
111
111
+
{ "registered": true, "type": "wisp", "domain": "name.wisp.place", "did": "did:plc:...", "rkey": "site-rkey" }
112
112
+
```
113
113
+
Custom domain:
114
114
+
```json
115
115
+
{ "registered": true, "type": "custom", "domain": "example.com", "did": "did:plc:...", "rkey": "site-rkey", "verified": true }
116
116
+
```
117
117
+
Unregistered:
118
118
+
```json
119
119
+
{ "registered": false }
120
120
+
```
121
121
+
Missing domain:
122
122
+
```json
123
123
+
{ "error": "Domain parameter required" }
124
124
+
```
125
125
+
126
126
+
### `POST /api/domain/claim`
127
127
+
```json
128
128
+
{ "success": true, "domain": "name.wisp.place" }
129
129
+
```
130
130
+
131
131
+
### `POST /api/domain/update`
132
132
+
```json
133
133
+
{ "success": true, "domain": "name.wisp.place" }
134
134
+
```
135
135
+
136
136
+
### `POST /api/domain/custom/add`
137
137
+
```json
138
138
+
{ "success": true, "id": "abcdef1234567890", "domain": "example.com", "verified": false }
139
139
+
```
140
140
+
141
141
+
### `POST /api/domain/custom/verify`
142
142
+
```json
143
143
+
{ "success": true, "verified": true, "error": null, "found": true }
144
144
+
```
145
145
+
146
146
+
### `DELETE /api/domain/custom/:id`
147
147
+
```json
148
148
+
{ "success": true }
149
149
+
```
150
150
+
151
151
+
### `POST /api/domain/wisp/map-site`
152
152
+
```json
153
153
+
{ "success": true }
154
154
+
```
155
155
+
156
156
+
### `DELETE /api/domain/wisp/:domain`
157
157
+
```json
158
158
+
{ "success": true }
159
159
+
```
160
160
+
161
161
+
### `POST /api/domain/custom/:id/map-site`
162
162
+
```json
163
163
+
{ "success": true }
164
164
+
```
165
165
+
166
166
+
## Site (`/api/site/*`)
167
167
+
168
168
+
### `DELETE /api/site/:rkey`
169
169
+
```json
170
170
+
{ "success": true, "message": "Site deleted successfully" }
171
171
+
```
172
172
+
On failure:
173
173
+
```json
174
174
+
{ "success": false, "error": "..." }
175
175
+
```
176
176
+
177
177
+
### `GET /api/site/:rkey/settings`
178
178
+
Returns the `place.wisp.settings` record when present, otherwise defaults:
179
179
+
```json
180
180
+
{ "indexFiles": ["index.html"], "cleanUrls": false, "directoryListing": false }
181
181
+
```
182
182
+
On failure:
183
183
+
```json
184
184
+
{ "success": false, "error": "..." }
185
185
+
```
186
186
+
187
187
+
### `POST /api/site/:rkey/settings`
188
188
+
```json
189
189
+
{ "success": true, "uri": "at://...", "cid": "bafy..." }
190
190
+
```
191
191
+
On validation failure:
192
192
+
```json
193
193
+
{ "success": false, "error": "Only one of spaMode, directoryListing, or custom404 can be enabled" }
194
194
+
```
195
195
+
196
196
+
## Wisp Uploads (`/wisp/*`)
197
197
+
198
198
+
### `GET /wisp/upload-progress/:jobId`
199
199
+
Server-sent events stream for upload progress.
200
200
+
201
201
+
- **event:** `progress` → `{ status, progress, result, error }`
202
202
+
- **event:** `done` → `result`
203
203
+
- **event:** `error` → `{ error }`
204
204
+
205
205
+
Errors:
206
206
+
```json
207
207
+
{ "error": "Job not found" }
208
208
+
```
209
209
+
```json
210
210
+
{ "error": "Unauthorized" }
211
211
+
```
212
212
+
213
213
+
### `POST /wisp/upload-files`
214
214
+
Empty upload (no files):
215
215
+
```json
216
216
+
{ "success": true, "uri": "at://...", "cid": "bafy...", "fileCount": 0, "siteName": "my-site" }
217
217
+
```
218
218
+
Async upload:
219
219
+
```json
220
220
+
{ "success": true, "jobId": "...", "message": "Upload started. Connect to /wisp/upload-progress/..." }
221
221
+
```
222
222
+
223
223
+
## Admin (`/api/admin/*`)
224
224
+
225
225
+
### `POST /api/admin/login`
226
226
+
```json
227
227
+
{ "success": true }
228
228
+
```
229
229
+
Invalid credentials (401):
230
230
+
```json
231
231
+
{ "error": "Invalid credentials" }
232
232
+
```
233
233
+
234
234
+
### `POST /api/admin/logout`
235
235
+
```json
236
236
+
{ "success": true }
237
237
+
```
238
238
+
239
239
+
### `GET /api/admin/status`
240
240
+
Authenticated:
241
241
+
```json
242
242
+
{ "authenticated": true, "username": "admin" }
243
243
+
```
244
244
+
Not authenticated:
245
245
+
```json
246
246
+
{ "authenticated": false }
247
247
+
```
248
248
+
249
249
+
### `GET /api/admin/logs`
250
250
+
```json
251
251
+
{ "logs": [/* combined logs */] }
252
252
+
```
253
253
+
254
254
+
### `GET /api/admin/errors`
255
255
+
```json
256
256
+
{ "errors": [/* combined errors */] }
257
257
+
```
258
258
+
259
259
+
### `GET /api/admin/metrics`
260
260
+
```json
261
261
+
{ "overall": {}, "mainApp": {}, "hostingService": {}, "timeWindow": 3600000 }
262
262
+
```
263
263
+
264
264
+
### `GET /api/admin/database`
265
265
+
```json
266
266
+
{ "stats": {}, "recentSites": [], "recentDomains": [] }
267
267
+
```
268
268
+
269
269
+
### `GET /api/admin/cache`
270
270
+
Returns the hosting service cache stats payload or:
271
271
+
```json
272
272
+
{ "error": "Failed to fetch cache stats from hosting service", "message": "Hosting service unavailable" }
273
273
+
```
274
274
+
275
275
+
### `GET /api/admin/sites`
276
276
+
```json
277
277
+
{ "sites": [/* sites */], "customDomains": [/* domains */] }
278
278
+
```
279
279
+
280
280
+
### `GET /api/admin/health`
281
281
+
```json
282
282
+
{
283
283
+
"uptime": 12345,
284
284
+
"memory": { "heapUsed": 123, "heapTotal": 456, "rss": 789 },
285
285
+
"timestamp": "2026-01-22T00:00:00.000Z"
286
286
+
}
287
287
+
```