import { Hono } from "jsr:@hono/hono@4"; import { encodeBase64Url } from "jsr:@std/encoding@1/base64url"; import { sqlWrapper } from "../../db.ts"; import { valkey } from "../../valkey.ts"; import { auditWrapper } from "../../audit.ts"; import { getAuthenticatedUser } from "../../auth-session.ts"; import { getClientIp } from "../../middleware.ts"; export const usersAdminRoutes = new Hono(); usersAdminRoutes.get("/", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const users = await sqlWrapper.sql` SELECT id, username, display_name, account_status FROM users ORDER BY username ASC `; return c.json({ users }); }); usersAdminRoutes.post("/:id/status", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const { status } = await c.req.json(); if (!["active", "pending", "suspended"].includes(status)) { return c.json({ error: "Invalid status" }, 400); } const targetUser = await sqlWrapper .sql`UPDATE users SET account_status = ${status} WHERE id = ${targetUserId} RETURNING id` .then((res: any) => res[0]); if (!targetUser) { return c.json({ error: "User not found" }, 404); } auditWrapper.auditLog(auth.userId, "user_status_changed", targetUserId, { newStatus: status, }, getClientIp(c)); return c.json({ success: true }); }); usersAdminRoutes.post("/:id/profile", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const { displayName } = await c.req.json(); const targetUser = await sqlWrapper .sql`UPDATE users SET display_name = ${ displayName?.trim() || null } WHERE id = ${targetUserId} RETURNING id, username, display_name` .then((res: any) => res[0]); if (!targetUser) return c.json({ error: "User not found" }, 404); auditWrapper.auditLog(auth.userId, "user_profile_updated", targetUserId, { display_name: targetUser.display_name, }, getClientIp(c)); return c.json({ success: true, user: targetUser }); }); usersAdminRoutes.get("/:id/grants", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const grants = await sqlWrapper.sql` SELECT g.id, g.app_id, g.role, g.created_at, a.name AS app_name, a.spiffe_id FROM grants g JOIN apps a ON g.app_id = a.id WHERE g.user_id = ${targetUserId} ORDER BY a.name ASC `; return c.json({ grants }); }); usersAdminRoutes.post("/:id/grants", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const { appId, role } = await c.req.json(); if (!appId || !role) { return c.json({ error: "appId and role are required" }, 400); } const app = await sqlWrapper .sql`SELECT id, name FROM apps WHERE id = ${appId}`.then( (res: any) => res[0], ); if (!app) return c.json({ error: "Application not found" }, 404); const targetUser = await sqlWrapper .sql`SELECT id, username FROM users WHERE id = ${targetUserId}`.then( (res: any) => res[0], ); if (!targetUser) return c.json({ error: "User not found" }, 404); await sqlWrapper.sql` INSERT INTO grants (user_id, app_id, role) VALUES (${targetUserId}, ${appId}, ${role}) ON CONFLICT (user_id, app_id) DO UPDATE SET role = ${role} `; auditWrapper.auditLog( auth.userId, "user_grant_assigned", targetUserId, { app_id: appId, app_name: app.name, role }, getClientIp(c), ); return c.json({ success: true }); }); usersAdminRoutes.delete("/:id/grants/:appId", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const { id: targetUserId, appId } = c.req.param(); const grant = await sqlWrapper.sql` DELETE FROM grants WHERE user_id = ${targetUserId} AND app_id = ${appId} RETURNING id `.then((res: any) => res[0]); if (grant) { auditWrapper.auditLog( auth.userId, "user_grant_revoked", targetUserId, { app_id: appId }, getClientIp(c), ); return c.json({ success: true }); } return c.json({ error: "Grant not found" }, 404); }); usersAdminRoutes.get("/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const user = await sqlWrapper .sql`SELECT id, username, display_name, account_status FROM users WHERE id = ${targetUserId}` .then((res: any) => res[0]); if (!user) return c.json({ error: "User not found" }, 404); const sessions = await sqlWrapper .sql`SELECT id, created_at, expires_at FROM sessions WHERE user_id = ${targetUserId} ORDER BY created_at DESC`; const passkeys = await sqlWrapper .sql`SELECT id, credential_id, counter FROM passkeys WHERE user_id = ${targetUserId}`; return c.json({ user, sessions, passkeys }); }); usersAdminRoutes.delete("/:id/sessions", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const sessions = await sqlWrapper .sql`DELETE FROM sessions WHERE user_id = ${targetUserId} RETURNING id`; for (const session of sessions) { try { await valkey.del(session.id); } catch (_err) {} } auditWrapper.auditLog( auth.userId, "admin_all_sessions_revoked", targetUserId, null, getClientIp(c), ); return c.json({ success: true }); }); usersAdminRoutes.delete("/:userId/passkeys/:passkeyId", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const { userId, passkeyId } = c.req.param(); const passkey = await sqlWrapper .sql`DELETE FROM passkeys WHERE id = ${passkeyId} AND user_id = ${userId} RETURNING id` .then((res: any) => res[0]); if (passkey) { auditWrapper.auditLog(auth.userId, "admin_passkey_revoked", userId, { passkey_id: passkey.id, }, getClientIp(c)); return c.json({ success: true }); } return c.json({ error: "Passkey not found" }, 404); }); usersAdminRoutes.post("/:id/recovery", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetUserId = c.req.param("id"); const targetUser = await sqlWrapper .sql`SELECT id FROM users WHERE id = ${targetUserId}` .then((res: any) => res[0]); if (!targetUser) return c.json({ error: "User not found" }, 404); const recoveryCode = encodeBase64Url( crypto.getRandomValues(new Uint8Array(24)), ); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 1); await sqlWrapper .sql`INSERT INTO recovery_links (code, user_id, created_by, expires_at) VALUES (${recoveryCode}, ${targetUserId}, ${auth.userId}, ${expiresAt})`; auditWrapper.auditLog( auth.userId, "recovery_link_created", targetUserId, null, getClientIp(c), ); return c.json({ success: true, recoveryCode, expiresAt }); });