Files
app/src/routes/admin/audit-log.ts
BOHA 9ae49c0d6a refactor: route/schema consistency (parseId, broad invalidation, Czech messages, Zod 4 strictObject)
- OfferDetail: invalidate broad ["offers"] not ["offers","list"] (CLAUDE.md convention)
- trips.ts/sessions.ts: use parseId helper (validated 400) instead of raw parseInt for route ids
- audit-log.ts: z.object().strict() -> z.strictObject() (Zod 4 idiom)
- attendance.ts, orders/offers schemas: English user-facing strings -> Czech
- project-files.ts: unify 'not found' phrasing to the codebase-dominant 'nenalezen' form

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 02:29:04 +02:00

116 lines
4.1 KiB
TypeScript

import { FastifyInstance } from "fastify";
import { z } from "zod";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, paginated, error } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
// DELETE_ALL_CONFIRM must be sent literally in the body to authorize a
// full audit-log wipe. Protects against accidental / automated wipes of
// forensic history.
const DELETE_ALL_CONFIRM = "DELETE_ALL_AUDIT" as const;
const AuditCleanupSchema = z.strictObject({
days: z.number().int().nonnegative().optional(),
confirm: z.string().optional(),
});
export default async function auditLogRoutes(
fastify: FastifyInstance,
): Promise<void> {
fastify.get(
"/",
{ preHandler: requirePermission("settings.audit") },
async (request, reply) => {
const query = request.query as Record<string, unknown>;
const { page, limit, skip, order, search } = parsePagination(query);
const where: Record<string, unknown> = {};
if (query.action) where.action = String(query.action);
if (query.entity_type) where.entity_type = String(query.entity_type);
if (query.user_id) where.user_id = Number(query.user_id);
if (search) where.description = { contains: search };
if (query.date_from || query.date_to) {
const dateFilter: Record<string, Date> = {};
if (query.date_from) dateFilter.gte = new Date(String(query.date_from));
if (query.date_to)
dateFilter.lte = new Date(String(query.date_to) + "T23:59:59");
where.created_at = dateFilter;
}
const [logs, total] = await Promise.all([
prisma.audit_logs.findMany({
where,
skip,
take: limit,
orderBy: { created_at: order },
}),
prisma.audit_logs.count({ where }),
]);
return paginated(reply, logs, buildPaginationMeta(total, page, limit));
},
);
// POST /api/admin/audit-log/cleanup — delete old audit logs
// days=0 with confirm="DELETE_ALL_AUDIT" wipes everything (intentionally
// a two-step path so a stray API call cannot destroy forensic history).
fastify.post(
"/cleanup",
{ preHandler: requirePermission("settings.audit") },
async (request, reply) => {
const parsed = parseBody(AuditCleanupSchema, request.body);
if ("error" in parsed) {
return error(reply, parsed.error, 400);
}
const { days, confirm } = parsed.data;
// Wipe-all path: requires explicit confirmation literal.
if (days === 0) {
if (confirm !== DELETE_ALL_CONFIRM) {
return error(
reply,
`Pro smazání všech audit logů je nutné odeslat confirm: "${DELETE_ALL_CONFIRM}"`,
400,
);
}
const result = await prisma.audit_logs.deleteMany({});
await logAudit({
request,
authData: request.authData,
action: "delete",
entityType: "audit_logs",
description: `Uživatel ${request.authData?.username ?? "unknown"} smazal všechny audit logy, počet: ${result.count}`,
});
return success(reply, null, 200, `Smazáno ${result.count} záznamů`);
}
if (days !== undefined && days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
const result = await prisma.audit_logs.deleteMany({
where: { created_at: { lt: cutoff } },
});
await logAudit({
request,
authData: request.authData,
action: "delete",
entityType: "audit_logs",
description: `Uživatel ${request.authData?.username ?? "unknown"} smazal audit logy starší než ${days} dní, počet: ${result.count}`,
});
return success(
reply,
null,
200,
`Smazáno ${result.count} záznamů starších než ${days} dní`,
);
}
return error(reply, "Zadejte počet dní", 400);
},
);
}