auth-core-users.ts401 lines · main
1/**
2 * briven-engine users API — list, detail, hold, archive, delete, revoke sessions.
3 *
4 * GET /v1/auth-core/users
5 * GET /v1/auth-core/users/:userId
6 * GET /v1/auth-core/users/:userId/metadata
7 * PUT /v1/auth-core/users/:userId/metadata
8 * POST /v1/auth-core/users/:userId/hold
9 * POST /v1/auth-core/users/:userId/unhold
10 * POST /v1/auth-core/users/:userId/archive
11 * POST /v1/auth-core/users/:userId/unarchive
12 * POST /v1/auth-core/users/:userId/delete
13 * POST /v1/auth-core/users/:userId/sessions/revoke-all
14 * POST /v1/auth-core/users/:userId/sessions/:sessionHandle/revoke
15 */
16
17import { Hono } from 'hono';
18
19import { requireAuthCoreDashboard } from '../middleware/auth-core-guard.js';
20import { BRIVEN_ENGINE_ID, isAuthCoreInitialized } from '../services/auth-core/engine.js';
21import { requireDashboardProjectAdmin } from '../services/auth-core/dashboard-project-auth.js';
22import {
23 archiveBrivenEngineUser,
24 deleteBrivenEngineUser,
25 getBrivenEngineUser,
26 getBrivenEngineUserMetadata,
27 holdBrivenEngineUser,
28 listBrivenEngineUsers,
29 unarchiveBrivenEngineUser,
30 unholdBrivenEngineUser,
31 updateBrivenEngineUserMetadata,
32} from '../services/auth-core/users.js';
33import {
34 revokeAllSessionsForUser,
35 revokeSession,
36} from '../services/auth-core/session.js';
37import type { AppEnv } from '../types/app-env.js';
38
39export const authCoreUsersRouter = new Hono<AppEnv>();
40
41authCoreUsersRouter.use('/v1/auth-core/users', requireAuthCoreDashboard());
42authCoreUsersRouter.use('/v1/auth-core/users/*', requireAuthCoreDashboard());
43
44async function resolveTenantId(
45 projectId: string | undefined,
46 tenantId: string | undefined,
47): Promise<string | undefined> {
48 if (tenantId) return tenantId;
49 if (!projectId) return undefined;
50 try {
51 const { projectIdToTenantId } = await import(
52 '../services/auth-core/project-map.js'
53 );
54 return projectIdToTenantId(projectId);
55 } catch {
56 return undefined;
57 }
58}
59
60authCoreUsersRouter.get('/v1/auth-core/users', async (c) => {
61 if (!isAuthCoreInitialized()) {
62 return c.json(
63 { engine: BRIVEN_ENGINE_ID, code: 'auth_core_sdk_not_ready', users: [] },
64 503,
65 );
66 }
67 const projectGate = await requireDashboardProjectAdmin(
68 c,
69 c.req.query('projectId'),
70 );
71 if (projectGate instanceof Response) return projectGate;
72 const limit = Number(c.req.query('limit') ?? '50');
73 const paginationToken = c.req.query('paginationToken') ?? undefined;
74 const projectId = projectGate.projectId;
75 const tenantId = await resolveTenantId(
76 projectId,
77 c.req.query('tenantId') ?? undefined,
78 );
79 const result = await listBrivenEngineUsers({
80 limit: Number.isFinite(limit) ? limit : 50,
81 paginationToken,
82 tenantId,
83 });
84 return c.json({
85 ...result,
86 projectId,
87 tenantId: tenantId ?? null,
88 });
89});
90
91authCoreUsersRouter.get('/v1/auth-core/users/:userId', async (c) => {
92 if (!isAuthCoreInitialized()) {
93 return c.json({ engine: BRIVEN_ENGINE_ID, code: 'auth_core_sdk_not_ready' }, 503);
94 }
95 const projectGate = await requireDashboardProjectAdmin(
96 c,
97 c.req.query('projectId'),
98 );
99 if (projectGate instanceof Response) return projectGate;
100 const userId = c.req.param('userId');
101 const projectId = projectGate.projectId;
102 const tenantId = await resolveTenantId(
103 projectId,
104 c.req.query('tenantId') ?? undefined,
105 );
106 const user = await getBrivenEngineUser(userId, { tenantId });
107 if (!user) {
108 return c.json(
109 { engine: BRIVEN_ENGINE_ID, code: 'not_found', message: 'user not found' },
110 404,
111 );
112 }
113 return c.json({ engine: BRIVEN_ENGINE_ID, user, projectId });
114});
115
116/** GDPR-style JSON export for one end-user (no password hashes). */
117authCoreUsersRouter.get('/v1/auth-core/users/:userId/export', async (c) => {
118 if (!isAuthCoreInitialized()) {
119 return c.json({ engine: BRIVEN_ENGINE_ID, code: 'auth_core_sdk_not_ready' }, 503);
120 }
121 const projectGate = await requireDashboardProjectAdmin(
122 c,
123 c.req.query('projectId'),
124 );
125 if (projectGate instanceof Response) return projectGate;
126 const userId = c.req.param('userId');
127 const projectId = projectGate.projectId;
128 const tenantId = await resolveTenantId(
129 projectId,
130 c.req.query('tenantId') ?? undefined,
131 );
132 const { exportBrivenEngineUserGdpr } = await import(
133 '../services/auth-core/users.js'
134 );
135 const pack = await exportBrivenEngineUserGdpr(userId, { tenantId });
136 if (!pack) {
137 return c.json(
138 { engine: BRIVEN_ENGINE_ID, code: 'not_found', message: 'user not found' },
139 404,
140 );
141 }
142 return c.json(pack);
143});
144
145authCoreUsersRouter.get('/v1/auth-core/users/:userId/metadata', async (c) => {
146 const projectGate = await requireDashboardProjectAdmin(
147 c,
148 c.req.query('projectId'),
149 );
150 if (projectGate instanceof Response) return projectGate;
151 const userId = c.req.param('userId');
152 const metadata = await getBrivenEngineUserMetadata(userId);
153 if (metadata == null && !isAuthCoreInitialized()) {
154 return c.json({ engine: BRIVEN_ENGINE_ID, code: 'auth_core_sdk_not_ready' }, 503);
155 }
156 return c.json({ engine: BRIVEN_ENGINE_ID, userId, metadata: metadata ?? {} });
157});
158
159authCoreUsersRouter.put('/v1/auth-core/users/:userId/metadata', async (c) => {
160 const projectGate = await requireDashboardProjectAdmin(
161 c,
162 c.req.query('projectId'),
163 );
164 if (projectGate instanceof Response) return projectGate;
165 const userId = c.req.param('userId');
166 let body: Record<string, unknown> = {};
167 try {
168 body = (await c.req.json()) as Record<string, unknown>;
169 } catch {
170 body = {};
171 }
172 const ok = await updateBrivenEngineUserMetadata(userId, body);
173 if (!ok) {
174 return c.json(
175 {
176 engine: BRIVEN_ENGINE_ID,
177 ok: false,
178 code: isAuthCoreInitialized() ? 'update_failed' : 'auth_core_sdk_not_ready',
179 },
180 isAuthCoreInitialized() ? 400 : 503,
181 );
182 }
183 return c.json({ engine: BRIVEN_ENGINE_ID, ok: true, userId });
184});
185
186async function moderationBody(c: {
187 req: { json: () => Promise<unknown> };
188}): Promise<{ reason?: string; projectId?: string; confirm?: string }> {
189 try {
190 const body = (await c.req.json()) as {
191 reason?: string;
192 projectId?: string;
193 confirm?: string;
194 };
195 return body ?? {};
196 } catch {
197 return {};
198 }
199}
200
201authCoreUsersRouter.post('/v1/auth-core/users/:userId/hold', async (c) => {
202 const userId = c.req.param('userId');
203 const body = await moderationBody(c);
204 const projectGate = await requireDashboardProjectAdmin(
205 c,
206 body.projectId ?? c.req.query('projectId'),
207 );
208 if (projectGate instanceof Response) return projectGate;
209 const projectId = projectGate.projectId;
210 const tenantId = await resolveTenantId(projectId, undefined);
211 const ok = await holdBrivenEngineUser(userId, {
212 reason: body.reason,
213 tenantId,
214 });
215 if (!ok) {
216 return c.json(
217 { engine: BRIVEN_ENGINE_ID, ok: false, code: 'hold_failed' },
218 400,
219 );
220 }
221 return c.json({ engine: BRIVEN_ENGINE_ID, ok: true, userId, status: 'held' });
222});
223
224authCoreUsersRouter.post('/v1/auth-core/users/:userId/unhold', async (c) => {
225 const userId = c.req.param('userId');
226 const body = await moderationBody(c);
227 const projectGate = await requireDashboardProjectAdmin(
228 c,
229 body.projectId ?? c.req.query('projectId'),
230 );
231 if (projectGate instanceof Response) return projectGate;
232 const projectId = projectGate.projectId;
233 const tenantId = await resolveTenantId(projectId, undefined);
234 const ok = await unholdBrivenEngineUser(userId, { tenantId });
235 if (!ok) {
236 return c.json(
237 { engine: BRIVEN_ENGINE_ID, ok: false, code: 'unhold_failed' },
238 400,
239 );
240 }
241 return c.json({ engine: BRIVEN_ENGINE_ID, ok: true, userId, status: 'active' });
242});
243
244authCoreUsersRouter.post('/v1/auth-core/users/:userId/archive', async (c) => {
245 const userId = c.req.param('userId');
246 const body = await moderationBody(c);
247 const projectGate = await requireDashboardProjectAdmin(
248 c,
249 body.projectId ?? c.req.query('projectId'),
250 );
251 if (projectGate instanceof Response) return projectGate;
252 const projectId = projectGate.projectId;
253 const tenantId = await resolveTenantId(projectId, undefined);
254 const ok = await archiveBrivenEngineUser(userId, {
255 reason: body.reason,
256 tenantId,
257 });
258 if (!ok) {
259 return c.json(
260 { engine: BRIVEN_ENGINE_ID, ok: false, code: 'archive_failed' },
261 400,
262 );
263 }
264 return c.json({
265 engine: BRIVEN_ENGINE_ID,
266 ok: true,
267 userId,
268 status: 'archived',
269 });
270});
271
272authCoreUsersRouter.post('/v1/auth-core/users/:userId/unarchive', async (c) => {
273 const userId = c.req.param('userId');
274 const body = await moderationBody(c);
275 const projectGate = await requireDashboardProjectAdmin(
276 c,
277 body.projectId ?? c.req.query('projectId'),
278 );
279 if (projectGate instanceof Response) return projectGate;
280 const projectId = projectGate.projectId;
281 const tenantId = await resolveTenantId(projectId, undefined);
282 const ok = await unarchiveBrivenEngineUser(userId, { tenantId });
283 if (!ok) {
284 return c.json(
285 { engine: BRIVEN_ENGINE_ID, ok: false, code: 'unarchive_failed' },
286 400,
287 );
288 }
289 return c.json({ engine: BRIVEN_ENGINE_ID, ok: true, userId, status: 'active' });
290});
291
292authCoreUsersRouter.post('/v1/auth-core/users/:userId/delete', async (c) => {
293 const userId = c.req.param('userId');
294 const body = await moderationBody(c);
295 const projectGate = await requireDashboardProjectAdmin(
296 c,
297 body.projectId ?? c.req.query('projectId'),
298 );
299 if (projectGate instanceof Response) return projectGate;
300 const projectId = projectGate.projectId;
301 const tenantId = await resolveTenantId(projectId, undefined);
302 // Safety: require explicit confirm in body
303 if (body.confirm !== 'delete') {
304 return c.json(
305 {
306 engine: BRIVEN_ENGINE_ID,
307 ok: false,
308 code: 'confirm_required',
309 message: 'send { "confirm": "delete" } to permanently delete',
310 },
311 400,
312 );
313 }
314 const ok = await deleteBrivenEngineUser(userId, { tenantId });
315 if (!ok) {
316 return c.json(
317 { engine: BRIVEN_ENGINE_ID, ok: false, code: 'delete_failed' },
318 400,
319 );
320 }
321 return c.json({ engine: BRIVEN_ENGINE_ID, ok: true, userId, deleted: true });
322});
323
324authCoreUsersRouter.post(
325 '/v1/auth-core/users/:userId/sessions/revoke-all',
326 async (c) => {
327 const userId = c.req.param('userId');
328 const body = await moderationBody(c);
329 const projectGate = await requireDashboardProjectAdmin(
330 c,
331 body.projectId ?? c.req.query('projectId'),
332 );
333 if (projectGate instanceof Response) return projectGate;
334 const projectId = projectGate.projectId;
335 const tenantId = await resolveTenantId(projectId, undefined);
336 // Scope: ensure user belongs to tenant when project provided
337 if (tenantId) {
338 const user = await getBrivenEngineUser(userId, { tenantId });
339 if (!user) {
340 return c.json(
341 { engine: BRIVEN_ENGINE_ID, code: 'not_found' },
342 404,
343 );
344 }
345 }
346 const n = await revokeAllSessionsForUser(userId);
347 return c.json({
348 engine: BRIVEN_ENGINE_ID,
349 ok: true,
350 userId,
351 revoked: n,
352 });
353 },
354);
355
356authCoreUsersRouter.post(
357 '/v1/auth-core/users/:userId/sessions/:sessionHandle/revoke',
358 async (c) => {
359 const userId = c.req.param('userId');
360 const sessionHandle = c.req.param('sessionHandle');
361 const body = await moderationBody(c);
362 const projectGate = await requireDashboardProjectAdmin(
363 c,
364 body.projectId ?? c.req.query('projectId'),
365 );
366 if (projectGate instanceof Response) return projectGate;
367 const projectId = projectGate.projectId;
368 const tenantId = await resolveTenantId(projectId, undefined);
369 if (tenantId) {
370 const user = await getBrivenEngineUser(userId, { tenantId });
371 if (!user) {
372 return c.json(
373 { engine: BRIVEN_ENGINE_ID, code: 'not_found' },
374 404,
375 );
376 }
377 }
378 // Only revoke if session belongs to this user
379 const detail = await getBrivenEngineUser(userId, { tenantId });
380 const owns = detail?.sessions.some((s) => s.handle === sessionHandle);
381 if (!owns) {
382 // still try revoke by handle if list is empty due to race
383 const ok = await revokeSession(sessionHandle);
384 return c.json({
385 engine: BRIVEN_ENGINE_ID,
386 ok,
387 userId,
388 sessionHandle,
389 revoked: ok ? 1 : 0,
390 });
391 }
392 const ok = await revokeSession(sessionHandle);
393 return c.json({
394 engine: BRIVEN_ENGINE_ID,
395 ok,
396 userId,
397 sessionHandle,
398 revoked: ok ? 1 : 0,
399 });
400 },
401);