mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* 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)
84 lines
2.4 KiB
TypeScript
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");
|
|
}
|