Files
SnapOtter/apps/api/src/routes/audit-log.ts
T
AshimandGitHub 5a45bcbc8f feat: production-grade RBAC with editor role, custom roles, API key scoping, and audit log (#89)
* feat(rbac): add editor role, 3 new permissions, ownership helper

* feat(rbac): add audit_log table, apiKeys.permissions column, editor role to schema

* feat(rbac): wire requirePermission into all routes, add editor role support

* refactor(rbac): replace ad-hoc role checks with permission-based ownership

* feat(rbac): add audit log DB writes + query endpoint

Dual-write audit events to stdout (existing) and SQLite audit_log table.
Add GET /api/v1/audit-log with pagination, action filter, and date range
filtering, gated behind audit:read permission.

* feat(rbac): add API key permission scoping with ceiling enforcement

* feat(rbac): add escalation prevention and last-admin protection

* feat(rbac): add editor role to UI, API key permission scoping in settings

* test(rbac): add full permission matrix integration test

* test(rbac): add editor role E2E tests

* feat(rbac): add custom roles with CRUD API and DB-backed permission lookup

* feat(rbac): add API key expiration

* feat(rbac): add roles management UI and API key expiration to settings

* feat(rbac): add audit log UI to settings

* fix: remove any cast in API key permission validation

* test(rbac): add unit tests for username validation rules

* test(rbac): add unit tests for effective permissions and ownership

* test(rbac): add comprehensive route permission matrix (all routes × all roles)

* test(rbac): add auth route edge case tests (login failures, session expiry, password side effects)

* test(rbac): add escalation prevention tests (register, update, self-demote, last-admin)

* test(rbac): add ownership enforcement tests (files, pipelines, editor access, cross-user isolation)

* test(rbac): add API key edge cases (name validation, delete behavior, key revocation)

* test(rbac): add audit log edge cases (all events, pagination clamping, structure)

* test(rbac): add custom roles edge case tests (validation, CRUD, functional permissions)

* test(rbac): add comprehensive E2E tests (roles UI, audit log, custom role, API key scoping)
2026-04-22 18:10:04 +08:00

84 lines
2.4 KiB
TypeScript

import { and, desc, eq, gte, lte, sql } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { db, schema } from "../db/index.js";
import { requirePermission } from "../permissions.js";
export async function auditLogRoutes(app: FastifyInstance): Promise<void> {
app.get(
"/api/v1/audit-log",
async (
request: FastifyRequest<{
Querystring: {
page?: string;
limit?: string;
action?: string;
from?: string;
to?: string;
};
}>,
reply: FastifyReply,
) => {
const user = requirePermission("audit:read")(request, reply);
if (!user) return;
const page = Math.max(1, parseInt(request.query.page ?? "1", 10) || 1);
const limit = Math.min(100, Math.max(1, parseInt(request.query.limit ?? "50", 10) || 50));
const offset = (page - 1) * limit;
const conditions = [];
if (request.query.action) {
conditions.push(eq(schema.auditLog.action, request.query.action));
}
if (request.query.from) {
const fromDate = new Date(request.query.from);
if (!Number.isNaN(fromDate.getTime())) {
conditions.push(gte(schema.auditLog.createdAt, fromDate));
}
}
if (request.query.to) {
const toDate = new Date(request.query.to);
if (!Number.isNaN(toDate.getTime())) {
conditions.push(lte(schema.auditLog.createdAt, toDate));
}
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const entries = db
.select()
.from(schema.auditLog)
.where(where)
.orderBy(desc(schema.auditLog.createdAt))
.limit(limit)
.offset(offset)
.all();
const countResult = db
.select({ count: sql<number>`count(*)` })
.from(schema.auditLog)
.where(where)
.get();
return reply.send({
entries: entries.map((e) => ({
id: e.id,
actorId: e.actorId,
actorUsername: e.actorUsername,
action: e.action,
targetType: e.targetType,
targetId: e.targetId,
details: e.details ? JSON.parse(e.details) : null,
ipAddress: e.ipAddress,
createdAt: e.createdAt.toISOString(),
})),
total: countResult?.count ?? 0,
page,
limit,
});
},
);
app.log.info("Audit log routes registered");
}