This commit is contained in:
k
2026-09-21 15:57:10 +02:00
parent 2dfcb944d1
commit 133a08b7ed
8 changed files with 2167 additions and 5 deletions
+78
View File
@@ -43,6 +43,16 @@ import {
updateUser,
} from "./db.ts";
import type { ChatMessage } from "./drivers/types.ts";
import {
consumeTotpChallenge,
createTotpChallenge,
getTotpSecret,
initTotpTables,
newTotpSecret,
setTotpSecret,
totpAuthUrl,
verifyTotp,
} from "./totp.ts";
import {
addAccessMessage,
createAccessRequest,
@@ -83,6 +93,7 @@ const registry = new DriverRegistry();
const ollamaDriver = registry.get("ollama") as OllamaDriver;
await tools.init();
initAccessTables();
initTotpTables();
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
@@ -297,10 +308,35 @@ const server = Bun.serve({
return json({ error: "invalid credentials" }, 401);
}
clearAttempts(ip);
// Second factor: correct key but TOTP enabled -> short-lived challenge,
// the session is only issued after POST /api/auth/totp verifies a code.
if (getTotpSecret(user.id)) {
const totpToken = createTotpChallenge(user.id);
return json({ totpRequired: true, totpToken, username: user.username });
}
const { token, expiresAt } = issueToken(user);
return json({ token, expiresAt, user });
}
// --- auth: TOTP second step (rate limited, challenge is single-use) ---
if (path === "/api/auth/totp" && req.method === "POST") {
const ip = clientIp(req, server);
if (isRateLimited(ip)) return json({ error: "too many attempts, try again later" }, 429);
const { ok, body } = await readJson(req);
const userId =
ok && typeof body.totpToken === "string" ? consumeTotpChallenge(body.totpToken) : null;
const code = ok && typeof body.code === "string" ? body.code : "";
const target = userId ? getUserById(userId) : null;
if (!target || !verifyTotp(getTotpSecret(target.id), code)) {
recordAttempt(ip);
await Bun.sleep(400);
return json({ error: "invalid code" }, 401);
}
clearAttempts(ip);
const { token, expiresAt } = issueToken(toPublicUser(target));
return json({ token, expiresAt, user: toPublicUser(target) });
}
if (path === "/api/auth/verify" && req.method === "GET") {
const user = requireAuth(req);
return user ? json({ ok: true, user }) : json({ error: "unauthorized" }, 401);
@@ -506,6 +542,48 @@ const server = Bun.serve({
return json({ user: toPublicUser(updated) });
}
// --- self-service security: rotate access key (old sessions revoked) ---
if (path === "/api/me/key/rotate" && req.method === "POST") {
const key = newAccessKey();
updateUser(user.id, { keyHash: hashSecret(key) });
deleteUserSessions(user.id);
return json({ key });
}
// --- self-service: delete your own account (chats, shares, sessions gone) ---
if (path === "/api/me" && req.method === "DELETE") {
if (user.username === "admin") return json({ error: "the admin account cannot be deleted" }, 403);
deleteUser(user.id);
getDb().query("DELETE FROM totp_challenges WHERE user_id = ?").run(user.id);
return json({ ok: true });
}
// --- self-service security: TOTP two-factor ---
if (path === "/api/me/totp" && req.method === "GET") {
return json({ enabled: getTotpSecret(user.id).length > 0 });
}
if (path === "/api/me/totp/setup" && req.method === "POST") {
if (getTotpSecret(user.id)) return json({ error: "TOTP already enabled — disable it first" }, 409);
const secret = newTotpSecret();
return json({ secret, otpauthUrl: totpAuthUrl(secret, user.username) });
}
if (path === "/api/me/totp/verify" && req.method === "POST") {
const { ok, body } = await readJson(req);
const secret = ok && typeof body.secret === "string" ? body.secret : "";
const code = ok && typeof body.code === "string" ? body.code : "";
if (!/^[A-Z2-7]{16,64}$/.test(secret.trim().toUpperCase())) return json({ error: "invalid secret" }, 400);
if (!verifyTotp(secret, code)) return json({ error: "invalid code" }, 401);
setTotpSecret(user.id, secret.trim().toUpperCase());
return json({ enabled: true });
}
if (path === "/api/me/totp" && req.method === "DELETE") {
const { ok, body } = await readJson(req);
const code = ok && typeof body.code === "string" ? body.code : "";
if (!verifyTotp(getTotpSecret(user.id), code)) return json({ error: "invalid code" }, 401);
setTotpSecret(user.id, "");
return json({ enabled: false });
}
// --- admin: user management (No-KYC accounts, keys issued by admin) ---
if (path === "/api/admin/users" && req.method === "GET") {
if (!admin) return json({ error: "forbidden" }, 403);