feat(web): add API-key authentication for the REST API

- api_keys table + hashed key store (src/data/api-keys.ts), tsmb_-prefixed
  plaintext shown once, per-user cap of 20, lastUsedAt tracking
- requireAuth accepts Authorization: Bearer / X-API-Key headers as an
  alternative to the session cookie; key inherits the owner user's
  role/capabilities/bot scope
- csrf origin check skipped for key-only requests (no ambient credentials);
  requests that also carry the session cookie stay gated
- /api/keys management endpoints (session-only, guests excluded, keys
  themselves rejected) with audit logging
- user deletion / password reset cascade-revoke the user's keys
- Settings page: API key management section (create/copy-once/revoke)
- docs: README section + full endpoint reference in docs/API.md
This commit is contained in:
senlinjun committed 2026-09-29 21:51:43 +08:00
1 parent 2ea02f54d9
commit aab8a004ae
16 files changed
+1272 -7

No files matched your search

+7 -1
View File
@@ -3,6 +3,7 @@ import type { Logger } from "../../logger.js";
import type { UserStore } from "../../data/users.js";
import { UsernameTakenError, GUEST_USER_ID } from "../../data/users.js";
import type { SessionStore } from "../../data/sessions.js";
import type { ApiKeyStore } from "../../data/api-keys.js";
import type { AuditStore } from "../../data/audit.js";
import { isCapability, BASIC_TIER_CAPABILITIES, type PermissionStore } from "../../data/permissions.js";
import { extractSessionToken } from "../auth/validateSession.js";
@@ -20,7 +21,8 @@ export function createUsersRouter(
sessions: SessionStore,
audit: AuditStore,
logger: Logger,
permissions: PermissionStore
permissions: PermissionStore,
apiKeys?: ApiKeyStore
): Router {
const router = Router();
@@ -85,6 +87,7 @@ export function createUsersRouter(
}
// FK CASCADE removes sessions; explicit call is belt-and-suspenders
sessions.deleteAllForUser(targetId);
apiKeys?.deleteAllForUser(targetId);
try {
audit.record({
actorId: req.user!.id, actorUsername: req.user!.username,
@@ -117,6 +120,9 @@ export function createUsersRouter(
? (extractSessionToken(req.headers.cookie) ?? undefined)
: undefined;
sessions.deleteAllForUser(targetId, exceptToken);
// A password reset must also kill the target's API keys — they are
// long-lived credentials that otherwise survive credential rotation.
apiKeys?.deleteAllForUser(targetId);
try {
audit.record({
actorId: req.user!.id, actorUsername: req.user!.username,