- Extracts Auth, Registration, and Passkey routes into `server/routes/auth.ts`. - Extracts all Admin API endpoints into `server/routes/admin.ts`. - Extracts RPC Connect setup and mTLS listener into `server/rpc.ts`. - Extracts global rate limiters and IP helpers into `server/middleware.ts`. - Reduces `server/main.ts` purely to an entrypoint mounting orchestrator. - Ensures all existing tests and quality gates pass with zero regressions. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
866 lines
26 KiB
TypeScript
866 lines
26 KiB
TypeScript
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,
|
|
isGlobalAdmin,
|
|
requireAdmin,
|
|
} from "../auth-session.ts";
|
|
import { adminRateLimiter, getClientIp } from "../middleware.ts";
|
|
import { computeJwkThumbprint } from "../http_signatures.ts";
|
|
|
|
export const adminRoutes = new Hono();
|
|
|
|
adminRoutes.use("*", requireAdmin);
|
|
adminRoutes.use("*", adminRateLimiter);
|
|
|
|
// ---------------------------------------------------------
|
|
// Global Admin APIs (For the Management Console)
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/audit-logs", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const logs = await sqlWrapper.sql`
|
|
SELECT a.id, a.action, a.resource, a.details, a.ip_address, a.created_at, u.username as user
|
|
FROM audit_records a
|
|
LEFT JOIN users u ON a.user_id = u.id
|
|
ORDER BY a.created_at DESC
|
|
LIMIT 100
|
|
`;
|
|
|
|
return c.json({ logs });
|
|
});
|
|
|
|
adminRoutes.get("/users", 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 });
|
|
});
|
|
|
|
adminRoutes.post("/users/: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 });
|
|
});
|
|
|
|
adminRoutes.post("/users/: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 });
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Admin Application Registry
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/apps", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const apps = await sqlWrapper.sql`
|
|
SELECT a.id, a.name, a.spiffe_id, a.description, a.created_at,
|
|
COUNT(g.id) AS active_grants_count
|
|
FROM apps a
|
|
LEFT JOIN grants g ON a.id = g.app_id
|
|
GROUP BY a.id, a.name, a.spiffe_id, a.description, a.created_at
|
|
ORDER BY a.created_at ASC
|
|
`;
|
|
return c.json({ apps });
|
|
});
|
|
|
|
adminRoutes.post("/apps", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const {
|
|
name,
|
|
spiffeId,
|
|
description,
|
|
domain,
|
|
is_public,
|
|
bypass_paths,
|
|
allowed_cidrs,
|
|
} = await c.req.json();
|
|
if (!name || !spiffeId) {
|
|
return c.json({ error: "Name and SPIFFE ID are required" }, 400);
|
|
}
|
|
|
|
try {
|
|
const newApp = await sqlWrapper.sql`
|
|
INSERT INTO apps (name, spiffe_id, description, domain, is_public, bypass_paths, allowed_cidrs)
|
|
VALUES (${name.trim()}, ${spiffeId.trim()}, ${
|
|
description?.trim() || null
|
|
}, ${domain?.trim() || null}, ${is_public || false}, ${
|
|
bypass_paths || []
|
|
}, ${allowed_cidrs || []})
|
|
RETURNING id, name, spiffe_id, description, created_at
|
|
`.then((res: any) => res[0]);
|
|
|
|
auditWrapper.auditLog(auth.userId, "app_registered", newApp.id, {
|
|
name: newApp.name,
|
|
spiffe_id: newApp.spiffe_id,
|
|
}, getClientIp(c));
|
|
return c.json({ success: true, app: newApp });
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
return c.json({
|
|
error: "An application with this SPIFFE ID already exists",
|
|
}, 409);
|
|
}
|
|
return c.json({ error: "Failed to register application" }, 500);
|
|
}
|
|
});
|
|
|
|
adminRoutes.put("/apps/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const appId = c.req.param("id");
|
|
const {
|
|
name,
|
|
description,
|
|
domain,
|
|
is_public,
|
|
bypass_paths,
|
|
allowed_cidrs,
|
|
} = await c.req.json();
|
|
|
|
if (!name) {
|
|
return c.json({ error: "Application name is required" }, 400);
|
|
}
|
|
|
|
try {
|
|
const updatedApp = await sqlWrapper.sql`
|
|
UPDATE apps
|
|
SET name = ${name.trim()},
|
|
description = ${description?.trim() || null},
|
|
domain = ${domain?.trim() || null},
|
|
is_public = ${is_public || false},
|
|
bypass_paths = ${bypass_paths || []},
|
|
allowed_cidrs = ${allowed_cidrs || []}
|
|
WHERE id = ${appId}
|
|
RETURNING id, name, spiffe_id, description, domain, is_public, bypass_paths, allowed_cidrs
|
|
`.then((res: any) => res[0]);
|
|
|
|
if (!updatedApp) return c.json({ error: "Application not found" }, 404);
|
|
|
|
auditWrapper.auditLog(auth.userId, "app_updated", updatedApp.id, {
|
|
name: updatedApp.name,
|
|
}, getClientIp(c));
|
|
|
|
return c.json({ success: true, app: updatedApp });
|
|
} catch (_err: any) {
|
|
return c.json({ error: "Failed to update application" }, 500);
|
|
}
|
|
});
|
|
|
|
adminRoutes.delete("/apps/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const appId = c.req.param("id");
|
|
const app = await sqlWrapper
|
|
.sql`DELETE FROM apps WHERE id = ${appId} RETURNING id, name`
|
|
.then((res: any) => res[0]);
|
|
if (app) {
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"app_deleted",
|
|
appId,
|
|
{ name: app.name },
|
|
getClientIp(c),
|
|
);
|
|
return c.json({ success: true });
|
|
}
|
|
return c.json({ error: "Application not found" }, 404);
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Admin Role Catalog Management
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/roles", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const appId = c.req.query("appId");
|
|
let roles;
|
|
if (appId) {
|
|
roles = await sqlWrapper.sql`
|
|
SELECT r.id, r.name, r.description, r.app_id, r.created_at,
|
|
a.name AS app_name
|
|
FROM roles r
|
|
LEFT JOIN apps a ON r.app_id = a.id
|
|
WHERE r.app_id IS NULL OR r.app_id = ${appId}
|
|
ORDER BY r.app_id NULLS FIRST, r.name ASC
|
|
`;
|
|
} else {
|
|
roles = await sqlWrapper.sql`
|
|
SELECT r.id, r.name, r.description, r.app_id, r.created_at,
|
|
a.name AS app_name
|
|
FROM roles r
|
|
LEFT JOIN apps a ON r.app_id = a.id
|
|
ORDER BY r.app_id NULLS FIRST, r.name ASC
|
|
`;
|
|
}
|
|
|
|
return c.json({ roles });
|
|
});
|
|
|
|
adminRoutes.post("/roles", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const { name, description, appId } = await c.req.json();
|
|
if (
|
|
!name || typeof name !== "string" || name.trim().length < 2 ||
|
|
name.trim().length > 32
|
|
) {
|
|
return c.json(
|
|
{ error: "Role name must be between 2 and 32 characters" },
|
|
400,
|
|
);
|
|
}
|
|
|
|
const normalizedName = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
|
|
let validatedAppId = null;
|
|
if (appId && typeof appId === "string" && appId.trim()) {
|
|
const uuidRegex =
|
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
if (!uuidRegex.test(appId)) {
|
|
return c.json({ error: "Invalid App UUID" }, 400);
|
|
}
|
|
const appExists = await sqlWrapper
|
|
.sql`SELECT id, name FROM apps WHERE id = ${appId}`
|
|
.then((res: any) => res[0]);
|
|
if (!appExists) {
|
|
return c.json({ error: "Selected application does not exist" }, 404);
|
|
}
|
|
validatedAppId = appId;
|
|
}
|
|
|
|
try {
|
|
const newRole = await sqlWrapper.sql`
|
|
INSERT INTO roles (name, description, app_id)
|
|
VALUES (${normalizedName}, ${
|
|
description?.trim() || null
|
|
}, ${validatedAppId})
|
|
RETURNING id, name, description, app_id, created_at
|
|
`.then((res: any) => res[0]);
|
|
|
|
auditWrapper.auditLog(auth.userId, "role_created", validatedAppId, {
|
|
role_name: newRole.name,
|
|
scope: validatedAppId ? "app-specific" : "global",
|
|
}, getClientIp(c));
|
|
|
|
return c.json({ success: true, role: newRole });
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
return c.json({
|
|
error: "This role already exists for the selected scope",
|
|
}, 409);
|
|
}
|
|
return c.json({ error: "Failed to create role" }, 500);
|
|
}
|
|
});
|
|
|
|
adminRoutes.put("/roles/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const roleId = c.req.param("id");
|
|
const { name, description } = await c.req.json();
|
|
|
|
if (!name || typeof name !== "string" || name.trim().length < 2) {
|
|
return c.json({ error: "Role name must be at least 2 characters" }, 400);
|
|
}
|
|
|
|
const normalizedName = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
|
|
try {
|
|
const updatedRole = await sqlWrapper.sql`
|
|
UPDATE roles
|
|
SET name = ${normalizedName},
|
|
description = ${description?.trim() || null}
|
|
WHERE id = ${roleId}
|
|
RETURNING id, name, description, app_id, created_at
|
|
`.then((res: any) => res[0]);
|
|
|
|
if (!updatedRole) return c.json({ error: "Role not found" }, 404);
|
|
|
|
auditWrapper.auditLog(auth.userId, "role_updated", updatedRole.app_id, {
|
|
role_name: updatedRole.name,
|
|
}, getClientIp(c));
|
|
|
|
return c.json({ success: true, role: updatedRole });
|
|
} catch (_err: any) {
|
|
return c.json({ error: "Failed to update role" }, 500);
|
|
}
|
|
});
|
|
|
|
adminRoutes.delete("/roles/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const roleId = c.req.param("id");
|
|
const role = await sqlWrapper
|
|
.sql`SELECT id, name, app_id FROM roles WHERE id = ${roleId}`.then(
|
|
(res: any) => res[0],
|
|
);
|
|
if (!role) {
|
|
return c.json({ error: "Role not found" }, 404);
|
|
}
|
|
|
|
if (role.name === "admin" && role.app_id === null) {
|
|
return c.json({ error: "The global 'admin' role cannot be deleted" }, 400);
|
|
}
|
|
|
|
await sqlWrapper.sql`DELETE FROM roles WHERE id = ${roleId}`;
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"role_deleted",
|
|
role.app_id,
|
|
{ role_name: role.name },
|
|
getClientIp(c),
|
|
);
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Admin Invites / Registration Tokens
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/invites", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const invites = await sqlWrapper.sql`
|
|
SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at,
|
|
a.name AS app_name, a.id AS app_id,
|
|
u.username AS used_by_username
|
|
FROM invites i
|
|
LEFT JOIN apps a ON i.app_id = a.id
|
|
LEFT JOIN users u ON i.used_by = u.id
|
|
ORDER BY i.created_at DESC
|
|
`;
|
|
return c.json({ invites });
|
|
});
|
|
|
|
adminRoutes.post("/invites/create", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const {
|
|
appId,
|
|
role,
|
|
expiresInDays,
|
|
customCode,
|
|
usageLimitType,
|
|
maxUses,
|
|
autoActivate,
|
|
} = await c.req.json();
|
|
const assignedRole = (typeof role === "string" && role.trim())
|
|
? role.trim()
|
|
: "user";
|
|
|
|
let validatedAppId = null;
|
|
if (appId) {
|
|
const uuidRegex =
|
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
if (typeof appId !== "string" || !uuidRegex.test(appId)) {
|
|
return c.json({ error: "appId must be a valid UUID string" }, 400);
|
|
}
|
|
const appExists = await sqlWrapper
|
|
.sql`SELECT id FROM apps WHERE id = ${appId}`.then(
|
|
(res: any) => res[0],
|
|
);
|
|
if (!appExists) {
|
|
return c.json({ error: "Target application does not exist" }, 404);
|
|
}
|
|
validatedAppId = appId;
|
|
}
|
|
|
|
let parsedMaxUses: number | null = 1;
|
|
if (usageLimitType === "unlimited") {
|
|
parsedMaxUses = null;
|
|
} else if (usageLimitType === "limited") {
|
|
const n = parseInt(maxUses);
|
|
parsedMaxUses = (!isNaN(n) && n > 0) ? n : 5;
|
|
} else {
|
|
parsedMaxUses = 1; // single-use default
|
|
}
|
|
|
|
const shouldAutoActivate = autoActivate !== false;
|
|
|
|
const inviteCode =
|
|
(customCode && typeof customCode === "string" && customCode.trim())
|
|
? customCode.trim()
|
|
: encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));
|
|
|
|
const days = Number(expiresInDays) || 7;
|
|
if (!Number.isInteger(days) || days < 1 || days > 30) {
|
|
return c.json({
|
|
error: "expiresInDays must be an integer between 1 and 30",
|
|
}, 400);
|
|
}
|
|
|
|
const expiresAt = new Date();
|
|
expiresAt.setDate(expiresAt.getDate() + days);
|
|
|
|
try {
|
|
await sqlWrapper.sql`
|
|
INSERT INTO invites (code, app_id, role, created_by, max_uses, uses_count, auto_activate, expires_at)
|
|
VALUES (${inviteCode}, ${validatedAppId}, ${assignedRole}, ${auth.userId}, ${parsedMaxUses}, 0, ${shouldAutoActivate}, ${expiresAt})
|
|
`;
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
return c.json({ error: "An invite with this code already exists" }, 409);
|
|
}
|
|
return c.json({ error: "Failed to generate invite" }, 500);
|
|
}
|
|
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"invite_created",
|
|
validatedAppId,
|
|
{
|
|
code: inviteCode,
|
|
role: assignedRole,
|
|
max_uses: parsedMaxUses,
|
|
auto_activate: shouldAutoActivate,
|
|
expiresInDays: days,
|
|
},
|
|
getClientIp(c),
|
|
);
|
|
|
|
return c.json({
|
|
success: true,
|
|
inviteCode,
|
|
expiresAt,
|
|
maxUses: parsedMaxUses,
|
|
autoActivate: shouldAutoActivate,
|
|
});
|
|
});
|
|
|
|
adminRoutes.get("/invites/:id/redemptions", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const inviteId = c.req.param("id");
|
|
const redemptions = await sqlWrapper.sql`
|
|
SELECT ir.id, ir.redeemed_at, u.id AS user_id, u.username, u.display_name, u.account_status
|
|
FROM invite_redemptions ir
|
|
JOIN users u ON ir.user_id = u.id
|
|
WHERE ir.invite_id = ${inviteId}
|
|
ORDER BY ir.redeemed_at DESC
|
|
`;
|
|
|
|
return c.json({ redemptions });
|
|
});
|
|
|
|
adminRoutes.delete("/invites/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const inviteId = c.req.param("id");
|
|
const invite = await sqlWrapper
|
|
.sql`DELETE FROM invites WHERE id = ${inviteId} RETURNING id, code`
|
|
.then((res: any) => res[0]);
|
|
if (invite) {
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"invite_revoked",
|
|
inviteId,
|
|
{ code: invite.code },
|
|
getClientIp(c),
|
|
);
|
|
return c.json({ success: true });
|
|
}
|
|
return c.json({ error: "Invite not found" }, 404);
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Admin User RBAC Grants Management
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/users/: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 });
|
|
});
|
|
|
|
adminRoutes.post("/users/: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 });
|
|
});
|
|
|
|
adminRoutes.delete("/users/: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);
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Admin AAGUID Management
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/aaguid", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
const allowlist = await sqlWrapper
|
|
.sql`SELECT id, aaguid, description, created_at FROM aaguid_allowlist ORDER BY created_at DESC`;
|
|
return c.json({ allowlist });
|
|
});
|
|
|
|
adminRoutes.post("/aaguid", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
const { aaguid, description } = await c.req.json();
|
|
const uuidRegex =
|
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i;
|
|
if (!aaguid || !uuidRegex.test(aaguid)) {
|
|
return c.json({ error: "Valid AAGUID (UUID) is required" }, 400);
|
|
}
|
|
try {
|
|
await sqlWrapper
|
|
.sql`INSERT INTO aaguid_allowlist (aaguid, description) VALUES (${aaguid.toLowerCase()}, ${
|
|
description || null
|
|
})`;
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"aaguid_added",
|
|
null,
|
|
{ aaguid },
|
|
getClientIp(c),
|
|
);
|
|
return c.json({ success: true });
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
return c.json({ error: "AAGUID already exists" }, 409);
|
|
}
|
|
return c.json({ error: "Internal server error" }, 500);
|
|
}
|
|
});
|
|
|
|
adminRoutes.delete("/aaguid/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
const id = c.req.param("id");
|
|
const record = await sqlWrapper
|
|
.sql`DELETE FROM aaguid_allowlist WHERE id = ${id} RETURNING aaguid`
|
|
.then((res: any) => res[0]);
|
|
if (record) {
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"aaguid_removed",
|
|
null,
|
|
{ aaguid: record.aaguid },
|
|
getClientIp(c),
|
|
);
|
|
}
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Admin HWK (Header Web Key) Management
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.post("/hwk", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const { jwk, name } = await c.req.json();
|
|
if (!jwk || !name || typeof name !== "string") {
|
|
return c.json({ error: "Missing required fields: jwk, name" }, 400);
|
|
}
|
|
|
|
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) {
|
|
return c.json({ error: "Invalid JWK: Must be an Ed25519 OKP key" }, 400);
|
|
}
|
|
|
|
try {
|
|
const fingerprint = await computeJwkThumbprint(jwk);
|
|
|
|
// 1. Dual Storage: PostgreSQL (Durability)
|
|
await sqlWrapper.sql`
|
|
INSERT INTO hwk_keys (fingerprint, public_key, name)
|
|
VALUES (${fingerprint}, ${JSON.stringify(jwk)}, ${name})
|
|
`;
|
|
|
|
// 2. Dual Storage: Valkey (O(1) Verification)
|
|
await valkey.sadd("auth:hwk:fingerprints", fingerprint);
|
|
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"hwk_added",
|
|
null,
|
|
{ fingerprint, name },
|
|
getClientIp(c),
|
|
);
|
|
|
|
return c.json({ success: true, fingerprint }, 201);
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
return c.json({ error: "This key has already been registered" }, 409);
|
|
}
|
|
console.error("Failed to add HWK:", err);
|
|
return c.json({ error: "Internal server error" }, 500);
|
|
}
|
|
});
|
|
|
|
adminRoutes.delete("/hwk/:fingerprint", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const fingerprint = c.req.param("fingerprint");
|
|
|
|
// 1. Remove from PostgreSQL
|
|
const record = await sqlWrapper.sql`
|
|
DELETE FROM hwk_keys WHERE fingerprint = ${fingerprint} RETURNING id, name
|
|
`.then((res: any) => res[0]);
|
|
|
|
if (record) {
|
|
// 2. Remove from Valkey
|
|
try {
|
|
await valkey.srem("auth:hwk:fingerprints", fingerprint);
|
|
} catch (_err) {}
|
|
|
|
auditWrapper.auditLog(auth.userId, "hwk_removed", null, {
|
|
fingerprint,
|
|
name: record.name,
|
|
}, getClientIp(c));
|
|
return c.json({ success: true });
|
|
}
|
|
|
|
return c.json({ error: "Key not found" }, 404);
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Global Session and Device Revocation (Admin)
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.get("/users/: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 });
|
|
});
|
|
|
|
adminRoutes.delete("/sessions/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
const sessionId = c.req.param("id");
|
|
const session = await sqlWrapper
|
|
.sql`DELETE FROM sessions WHERE id = ${sessionId} RETURNING user_id`
|
|
.then((res: any) => res[0]);
|
|
if (session) {
|
|
try {
|
|
await valkey.del(sessionId);
|
|
} catch (_err) {}
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"admin_session_revoked",
|
|
session.user_id,
|
|
{
|
|
revoked_session_id: sessionId,
|
|
},
|
|
getClientIp(c),
|
|
);
|
|
}
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
adminRoutes.delete("/users/: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 });
|
|
});
|
|
|
|
adminRoutes.delete("/users/: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);
|
|
});
|
|
|
|
// ---------------------------------------------------------
|
|
// Out-of-Band Account Recovery (Use Case 12)
|
|
// ---------------------------------------------------------
|
|
|
|
adminRoutes.post("/users/: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 });
|
|
});
|
|
|
|
adminRoutes.get("/check", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const isAdmin = await isGlobalAdmin(auth.userId);
|
|
return c.json({ isAdmin });
|
|
});
|