mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-09 16:32:49 +08:00
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:
1 parent
2ea02f54d9
commit
aab8a004ae
16 files changed
+1272
-7
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import { Router } from "express";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import type { ApiKeyStore } from "../../data/api-keys.js";
|
||||
import { MAX_API_KEYS_PER_USER } from "../../data/api-keys.js";
|
||||
import type { AuditStore } from "../../data/audit.js";
|
||||
import type { Logger } from "../../logger.js";
|
||||
|
||||
/**
|
||||
* API-key management (list / create / revoke), mounted at /api/keys.
|
||||
* Only interactive sessions may manage keys: a leaked key must never be able
|
||||
* to mint its own replacements.
|
||||
*/
|
||||
export function createApiKeysRouter(apiKeys: ApiKeyStore, audit: AuditStore, logger: Logger): Router {
|
||||
const router = Router();
|
||||
|
||||
const rejectApiKeyAuth = (req: Request, res: Response, next: NextFunction): void => {
|
||||
if (req.authMethod === "api-key") {
|
||||
res.status(403).json({ error: "API keys cannot manage API keys — log in to the WebUI" });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
router.use(rejectApiKeyAuth);
|
||||
|
||||
// GET /api/keys — the caller's keys; admins may pass ?all=1 for every user's.
|
||||
router.get("/", (req, res) => {
|
||||
const user = req.user!;
|
||||
if (req.query.all === "1" && user.role === "admin") {
|
||||
res.json({ keys: apiKeys.listAll() });
|
||||
return;
|
||||
}
|
||||
res.json({ keys: apiKeys.listForUser(user.id) });
|
||||
});
|
||||
|
||||
// POST /api/keys — create a key; the plaintext is returned exactly once.
|
||||
router.post("/", (req, res) => {
|
||||
const user = req.user!;
|
||||
const name = typeof req.body?.name === "string" ? req.body.name.trim() : "";
|
||||
if (!name || name.length > 64) {
|
||||
res.status(400).json({ error: "name is required (1-64 characters)" });
|
||||
return;
|
||||
}
|
||||
const created = apiKeys.create(user.id, name);
|
||||
if (!created) {
|
||||
res.status(409).json({ error: `每个用户最多创建 ${MAX_API_KEYS_PER_USER} 个 API Key` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
audit.record({
|
||||
actorId: user.id,
|
||||
actorUsername: user.username,
|
||||
targetUserId: user.id,
|
||||
targetUsername: user.username,
|
||||
action: "api_key.created",
|
||||
});
|
||||
} catch (auditErr) {
|
||||
logger.warn({ err: auditErr, action: "api_key.created" }, "audit insert failed");
|
||||
}
|
||||
logger.info({ userId: user.id, keyId: created.key.id }, "API key created");
|
||||
res.status(201).json(created);
|
||||
});
|
||||
|
||||
// DELETE /api/keys/:id — revoke; members only their own, admins any.
|
||||
router.delete("/:id", (req, res) => {
|
||||
const user = req.user!;
|
||||
const ok =
|
||||
user.role === "admin"
|
||||
? apiKeys.delete(req.params.id)
|
||||
: apiKeys.delete(req.params.id, user.id);
|
||||
if (!ok) {
|
||||
res.status(404).json({ error: "API key not found" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
audit.record({
|
||||
actorId: user.id,
|
||||
actorUsername: user.username,
|
||||
targetUserId: user.id,
|
||||
targetUsername: user.username,
|
||||
action: "api_key.deleted",
|
||||
});
|
||||
} catch (auditErr) {
|
||||
logger.warn({ err: auditErr, action: "api_key.deleted" }, "audit insert failed");
|
||||
}
|
||||
logger.info({ userId: user.id, keyId: req.params.id }, "API key deleted");
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in new issue
Block a user