allow admins to delete approvals and activity entries
This commit is contained in:
104
server/index.js
104
server/index.js
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user