feat(security): audit-log session termination (single + all-others)

The two session-revocation endpoints wrote no audit entry. Added a 'session' EntityType (+ Czech label 'Relace') and logAudit on both deletes, so revoking sessions is recorded in the forensic trail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 02:40:41 +02:00
parent 1f05ef6a55
commit 744baec2ec
3 changed files with 20 additions and 2 deletions

View File

@@ -36,4 +36,5 @@ export const ENTITY_TYPE_LABELS: Record<string, string> = {
work_plan_entry: "Plán prací záznam", work_plan_entry: "Plán prací záznam",
work_plan_override: "Plán prací přepsání dne", work_plan_override: "Plán prací přepsání dne",
plan_category: "Plán prací kategorie", plan_category: "Plán prací kategorie",
session: "Relace",
}; };

View File

@@ -3,6 +3,7 @@ import prisma from "../../config/database";
import { requireAuth } from "../../middleware/auth"; import { requireAuth } from "../../middleware/auth";
import { success, error, parseId } from "../../utils/response"; import { success, error, parseId } from "../../utils/response";
import { hashToken } from "../../services/auth"; import { hashToken } from "../../services/auth";
import { logAudit } from "../../services/audit";
/** Parse user-agent string into browser, OS, and device icon */ /** Parse user-agent string into browser, OS, and device icon */
function parseUserAgent(ua: string | null): { function parseUserAgent(ua: string | null): {
@@ -94,6 +95,14 @@ export default async function sessionsRoutes(
where: { id }, where: { id },
data: { replaced_at: new Date() }, data: { replaced_at: new Date() },
}); });
await logAudit({
request,
authData,
action: "delete",
entityType: "session",
entityId: id,
description: `Ukončena relace #${id} (${session.ip_address ?? "?"})`,
});
return success(reply, null, 200, "Relace ukončena"); return success(reply, null, 200, "Relace ukončena");
}, },
); );
@@ -112,7 +121,7 @@ export default async function sessionsRoutes(
return error(reply, "Nelze identifikovat aktuální relaci", 400); return error(reply, "Nelze identifikovat aktuální relaci", 400);
} }
await prisma.refresh_tokens.updateMany({ const terminated = await prisma.refresh_tokens.updateMany({
where: { where: {
user_id: authData.userId, user_id: authData.userId,
replaced_at: null, replaced_at: null,
@@ -120,6 +129,13 @@ export default async function sessionsRoutes(
}, },
data: { replaced_at: new Date() }, data: { replaced_at: new Date() },
}); });
await logAudit({
request,
authData,
action: "delete",
entityType: "session",
description: `Ukončeny všechny ostatní relace (${terminated.count})`,
});
return success(reply, null, 200, "Všechny ostatní relace ukončeny"); return success(reply, null, 200, "Všechny ostatní relace ukončeny");
} }

View File

@@ -146,4 +146,5 @@ export type EntityType =
| "warehouse_receipt_attachment" | "warehouse_receipt_attachment"
| "work_plan_entry" | "work_plan_entry"
| "work_plan_override" | "work_plan_override"
| "plan_category"; | "plan_category"
| "session";