mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(auth): user-management audit log (table + record sites + /api/audit endpoint)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ceb24595e6
commit
6af0e97f51
11 files changed
+256
-8
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { createDatabase, type BotDatabase } from "./database.js";
|
||||||
|
import { createAuditStore, type AuditStore } from "./audit.js";
|
||||||
|
|
||||||
|
describe("AuditStore", () => {
|
||||||
|
let botDb: BotDatabase;
|
||||||
|
let audit: AuditStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
botDb = createDatabase(":memory:");
|
||||||
|
audit = createAuditStore(botDb.db);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => botDb.close());
|
||||||
|
|
||||||
|
it("records and lists entries newest-first", async () => {
|
||||||
|
audit.record({
|
||||||
|
actorId: "a1", actorUsername: "alice",
|
||||||
|
targetUserId: "b1", targetUsername: "bob",
|
||||||
|
action: "user.created",
|
||||||
|
});
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
audit.record({
|
||||||
|
actorId: "a1", actorUsername: "alice",
|
||||||
|
targetUserId: "b1", targetUsername: "bob",
|
||||||
|
action: "user.deleted",
|
||||||
|
});
|
||||||
|
const list = audit.list(10, 0);
|
||||||
|
expect(list).toHaveLength(2);
|
||||||
|
expect(list[0].action).toBe("user.deleted");
|
||||||
|
expect(list[1].action).toBe("user.created");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports limit and offset", () => {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
audit.record({
|
||||||
|
actorId: "a1", actorUsername: "alice",
|
||||||
|
targetUserId: null, targetUsername: null,
|
||||||
|
action: "user.password_changed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect(audit.list(2, 0)).toHaveLength(2);
|
||||||
|
expect(audit.list(2, 4)).toHaveLength(1);
|
||||||
|
expect(audit.list(10, 10)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores nullable fields correctly", () => {
|
||||||
|
audit.record({
|
||||||
|
actorId: null, actorUsername: null,
|
||||||
|
targetUserId: "x", targetUsername: "deleted-user",
|
||||||
|
action: "admin.first_created",
|
||||||
|
});
|
||||||
|
const e = audit.list(1, 0)[0];
|
||||||
|
expect(e.actorId).toBeNull();
|
||||||
|
expect(e.actorUsername).toBeNull();
|
||||||
|
expect(e.targetUserId).toBe("x");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type Database from "better-sqlite3";
|
||||||
|
|
||||||
|
export type AuditAction =
|
||||||
|
| "admin.first_created"
|
||||||
|
| "user.created"
|
||||||
|
| "user.deleted"
|
||||||
|
| "user.password_reset"
|
||||||
|
| "user.password_changed";
|
||||||
|
|
||||||
|
export interface AuditEntry {
|
||||||
|
id: number;
|
||||||
|
timestamp: number;
|
||||||
|
actorId: string | null;
|
||||||
|
actorUsername: string | null;
|
||||||
|
targetUserId: string | null;
|
||||||
|
targetUsername: string | null;
|
||||||
|
action: AuditAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditRecordInput {
|
||||||
|
actorId: string | null;
|
||||||
|
actorUsername: string | null;
|
||||||
|
targetUserId: string | null;
|
||||||
|
targetUsername: string | null;
|
||||||
|
action: AuditAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditStore {
|
||||||
|
record(input: AuditRecordInput): void;
|
||||||
|
list(limit: number, offset: number): AuditEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAuditStore(db: Database.Database): AuditStore {
|
||||||
|
const insertStmt = db.prepare(
|
||||||
|
"INSERT INTO user_audit (timestamp, actorId, actorUsername, targetUserId, targetUsername, action) VALUES (?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
const listStmt = db.prepare(
|
||||||
|
"SELECT id, timestamp, actorId, actorUsername, targetUserId, targetUsername, action FROM user_audit ORDER BY timestamp DESC, id DESC LIMIT ? OFFSET ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
record(input) {
|
||||||
|
insertStmt.run(
|
||||||
|
Date.now(),
|
||||||
|
input.actorId,
|
||||||
|
input.actorUsername,
|
||||||
|
input.targetUserId,
|
||||||
|
input.targetUsername,
|
||||||
|
input.action
|
||||||
|
);
|
||||||
|
},
|
||||||
|
list(limit, offset) {
|
||||||
|
return listStmt.all(limit, offset) as AuditEntry[];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -40,6 +40,13 @@ describe("database", () => {
|
|||||||
expect(sessionColNames).toEqual(["createdAt", "expiresAt", "id", "lastSeenAt", "userId"]);
|
expect(sessionColNames).toEqual(["createdAt", "expiresAt", "id", "lastSeenAt", "userId"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates user_audit table on init", () => {
|
||||||
|
const tables = botDb.db
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||||
|
.all() as Array<{ name: string }>;
|
||||||
|
expect(tables.map((t) => t.name)).toContain("user_audit");
|
||||||
|
});
|
||||||
|
|
||||||
it("records and retrieves play history", () => {
|
it("records and retrieves play history", () => {
|
||||||
botDb.addPlayHistory({
|
botDb.addPlayHistory({
|
||||||
botId: "bot1",
|
botId: "bot1",
|
||||||
|
|||||||
@@ -147,6 +147,17 @@ function initTables(db: Database.Database): void {
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_userId ON sessions(userId);
|
CREATE INDEX IF NOT EXISTS idx_sessions_userId ON sessions(userId);
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_expiresAt ON sessions(expiresAt);
|
CREATE INDEX IF NOT EXISTS idx_sessions_expiresAt ON sessions(expiresAt);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_audit (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
actorId TEXT,
|
||||||
|
actorUsername TEXT,
|
||||||
|
targetUserId TEXT,
|
||||||
|
targetUsername TEXT,
|
||||||
|
action TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_audit_timestamp ON user_audit(timestamp DESC);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import express from "express";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
import request from "supertest";
|
||||||
|
import { createDatabase, type BotDatabase } from "../../data/database.js";
|
||||||
|
import { createUserStore } from "../../data/users.js";
|
||||||
|
import { createSessionStore } from "../../data/sessions.js";
|
||||||
|
import { createAuditStore } from "../../data/audit.js";
|
||||||
|
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||||
|
import { createAuditRouter } from "./audit.js";
|
||||||
|
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||||
|
|
||||||
|
describe("audit router", () => {
|
||||||
|
let botDb: BotDatabase;
|
||||||
|
let app: express.Express;
|
||||||
|
let cookie: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
botDb = createDatabase(":memory:");
|
||||||
|
const users = createUserStore(botDb.db);
|
||||||
|
const sessions = createSessionStore(botDb.db);
|
||||||
|
const audit = createAuditStore(botDb.db);
|
||||||
|
const alice = await users.createUser("alice", "pw-alice");
|
||||||
|
cookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
audit.record({
|
||||||
|
actorId: alice.id, actorUsername: "alice",
|
||||||
|
targetUserId: "x", targetUsername: "x",
|
||||||
|
action: "user.created",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use("/api", createRequireAuth(sessions));
|
||||||
|
app.use("/api/audit", createAuditRouter(audit));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => botDb.close());
|
||||||
|
|
||||||
|
it("requires auth", async () => {
|
||||||
|
const res = await request(app).get("/api/audit");
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns entries newest-first", async () => {
|
||||||
|
const res = await request(app).get("/api/audit").set("Cookie", cookie);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.entries).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honors limit query param", async () => {
|
||||||
|
const res = await request(app).get("/api/audit?limit=1").set("Cookie", cookie);
|
||||||
|
expect(res.body.entries).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import type { AuditStore } from "../../data/audit.js";
|
||||||
|
|
||||||
|
export function createAuditRouter(audit: AuditStore): Router {
|
||||||
|
const router = Router();
|
||||||
|
router.get("/", (req, res) => {
|
||||||
|
const limit = clampInt(req.query.limit, 1, 500, 100);
|
||||||
|
const offset = clampInt(req.query.offset, 0, 100_000, 0);
|
||||||
|
res.json({ entries: audit.list(limit, offset) });
|
||||||
|
});
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampInt(v: unknown, min: number, max: number, def: number): number {
|
||||||
|
const n = typeof v === "string" ? parseInt(v, 10) : NaN;
|
||||||
|
if (!Number.isFinite(n)) return def;
|
||||||
|
return Math.min(Math.max(n, min), max);
|
||||||
|
}
|
||||||
@@ -6,14 +6,16 @@ import pino from "pino";
|
|||||||
import { createDatabase, type BotDatabase } from "../../data/database.js";
|
import { createDatabase, type BotDatabase } from "../../data/database.js";
|
||||||
import { createUserStore, type UserStore } from "../../data/users.js";
|
import { createUserStore, type UserStore } from "../../data/users.js";
|
||||||
import { createSessionStore, type SessionStore } from "../../data/sessions.js";
|
import { createSessionStore, type SessionStore } from "../../data/sessions.js";
|
||||||
|
import { createAuditStore } from "../../data/audit.js";
|
||||||
import { createSessionRouter } from "./session.js";
|
import { createSessionRouter } from "./session.js";
|
||||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||||
|
|
||||||
function makeApp(users: UserStore, sessions: SessionStore) {
|
function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use("/api/session", createSessionRouter(users, sessions, pino({ level: "silent" })));
|
const audit = createAuditStore(botDb.db);
|
||||||
|
app.use("/api/session", createSessionRouter(users, sessions, audit, pino({ level: "silent" })));
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ describe("session router", () => {
|
|||||||
botDb = createDatabase(":memory:");
|
botDb = createDatabase(":memory:");
|
||||||
users = createUserStore(botDb.db);
|
users = createUserStore(botDb.db);
|
||||||
sessions = createSessionStore(botDb.db);
|
sessions = createSessionStore(botDb.db);
|
||||||
app = makeApp(users, sessions);
|
app = makeApp(botDb, users, sessions);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => botDb.close());
|
afterEach(() => botDb.close());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Request, Response, NextFunction } from "express";
|
|||||||
import type { Logger } from "../../logger.js";
|
import type { Logger } from "../../logger.js";
|
||||||
import type { UserStore } from "../../data/users.js";
|
import type { UserStore } from "../../data/users.js";
|
||||||
import type { SessionStore } from "../../data/sessions.js";
|
import type { SessionStore } from "../../data/sessions.js";
|
||||||
|
import type { AuditStore } from "../../data/audit.js";
|
||||||
import { SESSION_TTL_MS } from "../../data/sessions.js";
|
import { SESSION_TTL_MS } from "../../data/sessions.js";
|
||||||
import { SESSION_COOKIE_NAME, validateSessionFromHeaders, extractSessionToken } from "../auth/validateSession.js";
|
import { SESSION_COOKIE_NAME, validateSessionFromHeaders, extractSessionToken } from "../auth/validateSession.js";
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ function parseTokenFromCookie(cookieHeader: string | undefined): string | null {
|
|||||||
export function createSessionRouter(
|
export function createSessionRouter(
|
||||||
users: UserStore,
|
users: UserStore,
|
||||||
sessions: SessionStore,
|
sessions: SessionStore,
|
||||||
|
audit: AuditStore,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
): Router {
|
): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -86,6 +88,11 @@ export function createSessionRouter(
|
|||||||
}
|
}
|
||||||
const { token } = sessions.createSession(user.id);
|
const { token } = sessions.createSession(user.id);
|
||||||
setSessionCookie(res, token);
|
setSessionCookie(res, token);
|
||||||
|
audit.record({
|
||||||
|
actorId: user.id, actorUsername: user.username,
|
||||||
|
targetUserId: user.id, targetUsername: user.username,
|
||||||
|
action: "admin.first_created",
|
||||||
|
});
|
||||||
logger.info({ userId: user.id, username }, "First admin created");
|
logger.info({ userId: user.id, username }, "First admin created");
|
||||||
res.json({ id: user.id, username: user.username });
|
res.json({ id: user.id, username: user.username });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -144,6 +151,11 @@ export function createSessionRouter(
|
|||||||
await users.changePassword(u.id, newPassword);
|
await users.changePassword(u.id, newPassword);
|
||||||
const currentToken = parseTokenFromCookie(req.headers.cookie);
|
const currentToken = parseTokenFromCookie(req.headers.cookie);
|
||||||
sessions.deleteAllForUser(u.id, currentToken ?? undefined);
|
sessions.deleteAllForUser(u.id, currentToken ?? undefined);
|
||||||
|
audit.record({
|
||||||
|
actorId: u.id, actorUsername: u.username,
|
||||||
|
targetUserId: u.id, targetUsername: u.username,
|
||||||
|
action: "user.password_changed",
|
||||||
|
});
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ import pino from "pino";
|
|||||||
import { createDatabase, type BotDatabase } from "../../data/database.js";
|
import { createDatabase, type BotDatabase } from "../../data/database.js";
|
||||||
import { createUserStore, type UserStore } from "../../data/users.js";
|
import { createUserStore, type UserStore } from "../../data/users.js";
|
||||||
import { createSessionStore, type SessionStore } from "../../data/sessions.js";
|
import { createSessionStore, type SessionStore } from "../../data/sessions.js";
|
||||||
|
import { createAuditStore } from "../../data/audit.js";
|
||||||
import { createRequireAuth } from "../middleware/requireAuth.js";
|
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||||
import { createUsersRouter } from "./users.js";
|
import { createUsersRouter } from "./users.js";
|
||||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||||
|
|
||||||
function makeApp(users: UserStore, sessions: SessionStore) {
|
function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
const requireAuth = createRequireAuth(sessions);
|
const requireAuth = createRequireAuth(sessions);
|
||||||
|
const audit = createAuditStore(botDb.db);
|
||||||
app.use("/api", requireAuth);
|
app.use("/api", requireAuth);
|
||||||
app.use("/api/users", createUsersRouter(users, sessions, pino({ level: "silent" })));
|
app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" })));
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ describe("users router", () => {
|
|||||||
botDb = createDatabase(":memory:");
|
botDb = createDatabase(":memory:");
|
||||||
users = createUserStore(botDb.db);
|
users = createUserStore(botDb.db);
|
||||||
sessions = createSessionStore(botDb.db);
|
sessions = createSessionStore(botDb.db);
|
||||||
app = makeApp(users, sessions);
|
app = makeApp(botDb, users, sessions);
|
||||||
const alice = await users.createUser("alice", "pw-alice");
|
const alice = await users.createUser("alice", "pw-alice");
|
||||||
aliceId = alice.id;
|
aliceId = alice.id;
|
||||||
aliceCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`;
|
aliceCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Logger } from "../../logger.js";
|
|||||||
import type { UserStore } from "../../data/users.js";
|
import type { UserStore } from "../../data/users.js";
|
||||||
import { UsernameTakenError } from "../../data/users.js";
|
import { UsernameTakenError } from "../../data/users.js";
|
||||||
import type { SessionStore } from "../../data/sessions.js";
|
import type { SessionStore } from "../../data/sessions.js";
|
||||||
|
import type { AuditStore } from "../../data/audit.js";
|
||||||
|
|
||||||
function isValidUsername(v: unknown): v is string {
|
function isValidUsername(v: unknown): v is string {
|
||||||
return typeof v === "string" && /^[A-Za-z0-9_\-.]{3,32}$/.test(v);
|
return typeof v === "string" && /^[A-Za-z0-9_\-.]{3,32}$/.test(v);
|
||||||
@@ -15,6 +16,7 @@ function isValidPassword(v: unknown): v is string {
|
|||||||
export function createUsersRouter(
|
export function createUsersRouter(
|
||||||
users: UserStore,
|
users: UserStore,
|
||||||
sessions: SessionStore,
|
sessions: SessionStore,
|
||||||
|
audit: AuditStore,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
): Router {
|
): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -31,6 +33,11 @@ export function createUsersRouter(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const u = await users.createUser(username, password);
|
const u = await users.createUser(username, password);
|
||||||
|
audit.record({
|
||||||
|
actorId: req.user!.id, actorUsername: req.user!.username,
|
||||||
|
targetUserId: u.id, targetUsername: u.username,
|
||||||
|
action: "user.created",
|
||||||
|
});
|
||||||
logger.info({ createdBy: req.user!.id, newUserId: u.id, username }, "User created");
|
logger.info({ createdBy: req.user!.id, newUserId: u.id, username }, "User created");
|
||||||
res.status(201).json({ id: u.id, username: u.username });
|
res.status(201).json({ id: u.id, username: u.username });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -45,6 +52,11 @@ export function createUsersRouter(
|
|||||||
|
|
||||||
router.delete("/:id", (req, res) => {
|
router.delete("/:id", (req, res) => {
|
||||||
const targetId = req.params.id;
|
const targetId = req.params.id;
|
||||||
|
const target = users.findById(targetId);
|
||||||
|
if (!target) {
|
||||||
|
res.status(404).json({ error: "not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (targetId === req.user!.id) {
|
if (targetId === req.user!.id) {
|
||||||
res.status(400).json({ error: "cannot delete self" });
|
res.status(400).json({ error: "cannot delete self" });
|
||||||
return;
|
return;
|
||||||
@@ -55,6 +67,11 @@ export function createUsersRouter(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessions.deleteAllForUser(targetId);
|
sessions.deleteAllForUser(targetId);
|
||||||
|
audit.record({
|
||||||
|
actorId: req.user!.id, actorUsername: req.user!.username,
|
||||||
|
targetUserId: target.id, targetUsername: target.username,
|
||||||
|
action: "user.deleted",
|
||||||
|
});
|
||||||
logger.info({ deletedBy: req.user!.id, deletedUserId: targetId }, "User deleted");
|
logger.info({ deletedBy: req.user!.id, deletedUserId: targetId }, "User deleted");
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
});
|
});
|
||||||
@@ -75,6 +92,11 @@ export function createUsersRouter(
|
|||||||
// Invalidate all sessions for the target user (except current actor's if it's the same user)
|
// Invalidate all sessions for the target user (except current actor's if it's the same user)
|
||||||
const exceptToken = targetId === req.user!.id ? undefined : undefined;
|
const exceptToken = targetId === req.user!.id ? undefined : undefined;
|
||||||
sessions.deleteAllForUser(targetId, exceptToken);
|
sessions.deleteAllForUser(targetId, exceptToken);
|
||||||
|
audit.record({
|
||||||
|
actorId: req.user!.id, actorUsername: req.user!.username,
|
||||||
|
targetUserId: target.id, targetUsername: target.username,
|
||||||
|
action: "user.password_reset",
|
||||||
|
});
|
||||||
logger.info({ resetBy: req.user!.id, targetUserId: targetId }, "Password reset");
|
logger.info({ resetBy: req.user!.id, targetUserId: targetId }, "Password reset");
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-2
@@ -16,6 +16,8 @@ import { createPlayerRouter } from "./api/player.js";
|
|||||||
import { createAuthRouter } from "./api/auth.js";
|
import { createAuthRouter } from "./api/auth.js";
|
||||||
import { createSessionRouter } from "./api/session.js";
|
import { createSessionRouter } from "./api/session.js";
|
||||||
import { createUsersRouter } from "./api/users.js";
|
import { createUsersRouter } from "./api/users.js";
|
||||||
|
import { createAuditStore } from "../data/audit.js";
|
||||||
|
import { createAuditRouter } from "./api/audit.js";
|
||||||
import { setupWebSocket } from "./websocket.js";
|
import { setupWebSocket } from "./websocket.js";
|
||||||
import { createUserStore } from "../data/users.js";
|
import { createUserStore } from "../data/users.js";
|
||||||
import { createSessionStore } from "../data/sessions.js";
|
import { createSessionStore } from "../data/sessions.js";
|
||||||
@@ -59,6 +61,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||||||
|
|
||||||
const users = createUserStore(options.database.db);
|
const users = createUserStore(options.database.db);
|
||||||
const sessions = createSessionStore(options.database.db);
|
const sessions = createSessionStore(options.database.db);
|
||||||
|
const audit = createAuditStore(options.database.db);
|
||||||
|
|
||||||
// ─── Public routes (no auth, no CSRF) ───────────────────────────────────
|
// ─── Public routes (no auth, no CSRF) ───────────────────────────────────
|
||||||
app.get("/api/health", (_req, res) => {
|
app.get("/api/health", (_req, res) => {
|
||||||
@@ -70,7 +73,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||||||
res.json({ publicUrl: raw ? raw.replace(/\/+$/, "") : null });
|
res.json({ publicUrl: raw ? raw.replace(/\/+$/, "") : null });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use("/api/session", createSessionRouter(users, sessions, logger));
|
app.use("/api/session", createSessionRouter(users, sessions, audit, logger));
|
||||||
|
|
||||||
// ─── Gates for everything else under /api ───────────────────────────────
|
// ─── Gates for everything else under /api ───────────────────────────────
|
||||||
const requireAuth = createRequireAuth(sessions);
|
const requireAuth = createRequireAuth(sessions);
|
||||||
@@ -101,7 +104,8 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||||||
"/api/auth",
|
"/api/auth",
|
||||||
createAuthRouter(options.neteaseProvider, options.qqProvider, options.bilibiliProvider, logger, options.cookieStore)
|
createAuthRouter(options.neteaseProvider, options.qqProvider, options.bilibiliProvider, logger, options.cookieStore)
|
||||||
);
|
);
|
||||||
app.use("/api/users", createUsersRouter(users, sessions, logger));
|
app.use("/api/users", createUsersRouter(users, sessions, audit, logger));
|
||||||
|
app.use("/api/audit", createAuditRouter(audit));
|
||||||
|
|
||||||
// ─── Static SPA (public) ────────────────────────────────────────────────
|
// ─── Static SPA (public) ────────────────────────────────────────────────
|
||||||
if (options.staticDir) {
|
if (options.staticDir) {
|
||||||
|
|||||||
Reference in new issue
Block a user