Files
jlinc-server/backend/http/apiKeys.js
2026-08-20 15:08:32 +00:00

51 lines
1.8 KiB
JavaScript

import { getConfig } from "../common/config.js";
import { fail, sessionRoute } from "../common/http.js";
import {
listKeys as defaultList,
generateKey as defaultGenerate,
revokeKey as defaultRevoke,
} from "../modules/core/apiKey.js";
const requireUser = (fn) => sessionRoute("apikeys", fn);
// Internal modules have no `app` row and issue no billable calls, so no key.
const mintableModules = () =>
Object.entries(getConfig().appModules)
.filter(([, m]) => !m?.internal)
.map(([type]) => type);
export function apiKeyHandlers({
listKeys = defaultList,
generateKey = defaultGenerate,
revokeKey = defaultRevoke,
} = {}) {
return {
whoami: requireUser(async (req, res) => {
res.json({ username: req.user.username, canViewJson: req.user.canViewJson, modules: mintableModules() });
}),
list: requireUser(async (req, res) => {
res.json(await listKeys(req.user.id));
}),
create: requireUser(async (req, res) => {
const { type, label, expiresTs } = req.body || {};
if (!mintableModules().includes(type)) {
return fail(res, 400, "bad_request", "unknown module");
}
if (expiresTs != null && (typeof expiresTs !== "string" || Number.isNaN(Date.parse(expiresTs)))) {
return fail(res, 400, "bad_request", "invalid expiresTs (expected an ISO date string)");
}
// Returns the raw key ONCE — the client must surface it immediately.
res.json(await generateKey(req.user.id, type, { label: label || null, expiresTs: expiresTs || null }));
}),
revoke: requireUser(async (req, res) => {
const id = parseInt(req.params.id, 10);
if (!Number.isInteger(id)) return fail(res, 400, "bad_request", "invalid id");
const ok = await revokeKey(req.user.id, id);
res.status(ok ? 200 : 404).json({ ok });
}),
};
}