allow admins to delete approvals and activity entries

This commit is contained in:
2026-04-02 22:21:13 +02:00
parent 66b08693b9
commit bc2f972769
3 changed files with 223 additions and 5 deletions

View File

@@ -823,6 +823,15 @@ async function routeRequest(req, res) {
return sendJson(res, 200, { approvals: listApprovalsView() });
}
if (method === "DELETE" && url.pathname.match(/^\/v1\/approvals\/[^/]+$/)) {
const auth = requireAuth(req, res);
if (!auth) return;
if (!hasRole(auth.user, ["admin"])) {
return sendError(res, 403, "auth.forbidden", "Nur Admin");
}
return handleApprovalDelete(res, auth.user, url.pathname);
}
if (method === "GET" && url.pathname === "/v1/activity-log") {
const auth = requireAuth(req, res);
if (!auth) return;
@@ -835,6 +844,15 @@ async function routeRequest(req, res) {
return sendJson(res, 200, { entries });
}
if (method === "DELETE" && url.pathname.match(/^\/v1\/activity-log\/[^/]+$/)) {
const auth = requireAuth(req, res);
if (!auth) return;
if (!hasRole(auth.user, ["admin"])) {
return sendError(res, 403, "auth.forbidden", "Nur Admin");
}
return handleActivityLogDelete(res, auth.user, url.pathname);
}
if (method === "POST" && url.pathname.match(/^\/v1\/approvals\/[^/]+\/(approve|reject)$/)) {
const auth = requireAuth(req, res);
if (!auth) return;
@@ -1567,6 +1585,27 @@ async function handleApprovalDecision(res, actor, pathname) {
return sendJson(res, 200, { ok: true, approval: approvalView, user: sanitizeUser(user) });
}
async function handleApprovalDelete(res, actor, pathname) {
const match = pathname.match(/^\/v1\/approvals\/([^/]+)$/);
if (!match) {
return sendError(res, 404, "approval.not_found", "Freigabe nicht gefunden");
}
const approvalId = decodeURIComponent(match[1]);
const index = runtime.approvalRequests.findIndex((entry) => entry.id === approvalId);
if (index < 0) {
return sendError(res, 404, "approval.not_found", "Freigabe nicht gefunden");
}
const removed = runtime.approvalRequests[index];
runtime.approvalRequests.splice(index, 1);
await saveApprovalRequests();
await appendAudit("approval.delete", actor, {
approvalId,
email: removed && removed.email ? removed.email : null,
status: removed && removed.status ? removed.status : null
});
return sendJson(res, 200, { ok: true, deletedId: approvalId });
}
function listApprovalsView() {
return runtime.approvalRequests.map((approval) => {
const user = runtime.users.find((entry) => entry.id === approval.userId);
@@ -7465,6 +7504,7 @@ async function applyAdminRoles() {
async function appendAudit(action, user, details) {
const entry = {
id: crypto.randomUUID(),
at: new Date().toISOString(),
action,
userId: user ? user.id : null,
@@ -7491,17 +7531,20 @@ async function listStationActivityLog(limit = 200) {
]);
const entries = [];
for (const line of lines) {
let entry;
try {
entry = JSON.parse(line);
} catch {
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const entry = parseAuditLine(line);
if (!entry) {
continue;
}
if (!entry || !interesting.has(entry.action)) {
continue;
}
const id = typeof entry.id === "string" && entry.id.trim()
? entry.id.trim()
: `legacy-${index}`;
entries.push({
id,
at: entry.at,
action: entry.action,
email: entry.email || null,
@@ -7514,6 +7557,57 @@ async function listStationActivityLog(limit = 200) {
return entries.slice(0, limit);
}
async function handleActivityLogDelete(res, actor, pathname) {
const match = pathname.match(/^\/v1\/activity-log\/([^/]+)$/);
if (!match) {
return sendError(res, 404, "activity.not_found", "Aktivitaetseintrag nicht gefunden");
}
const targetId = decodeURIComponent(match[1]);
const raw = await storage.readText(files.audit, "");
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const kept = [];
let removed = null;
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const entry = parseAuditLine(line);
const id = entry && typeof entry.id === "string" && entry.id.trim()
? entry.id.trim()
: `legacy-${index}`;
if (!removed && id === targetId) {
removed = entry || { action: "unknown", at: null };
continue;
}
kept.push(line);
}
if (!removed) {
return sendError(res, 404, "activity.not_found", "Aktivitaetseintrag nicht gefunden");
}
const nextRaw = kept.length ? `${kept.join("\n")}\n` : "";
await storage.writeText(files.audit, nextRaw);
await appendAudit("activity.delete", actor, {
removedId: targetId,
removedAction: removed.action || null,
removedAt: removed.at || null,
removedEmail: removed.email || null
});
return sendJson(res, 200, { ok: true, deletedId: targetId });
}
function parseAuditLine(line) {
try {
const parsed = JSON.parse(line);
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
function activityMessageForEntry(entry) {
const email = entry.email || "unbekannt";
if (entry.action === "auth.request_access") {