From d146a68174dcebdf9fbd0258847c13e044ca453b Mon Sep 17 00:00:00 2001 From: "Dennis (via Claude+Gemma)" Date: Sat, 23 May 2026 05:33:03 +0200 Subject: [PATCH] feat(audit-log): Audit-Log Tabelle + Page (admin-only) [tsc:fail] --- .phase6-state.json | 6 +- GENERATION_LOG.md | 18 ++++++ apps/api/src/db/schema.ts | 10 ++++ apps/api/src/routes/audit-log.ts | 32 +++++++++++ apps/web/src/pages/AuditLog.tsx | 98 ++++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/routes/audit-log.ts create mode 100644 apps/web/src/pages/AuditLog.tsx diff --git a/.phase6-state.json b/.phase6-state.json index 08e5d00..5eb0837 100644 --- a/.phase6-state.json +++ b/.phase6-state.json @@ -1,5 +1,7 @@ { - "completed_features": [], - "current_feature": "password-change", + "completed_features": [ + "password-change" + ], + "current_feature": "audit-log", "started_at": "2026-05-23T05:30:16.203066" } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 8aa29e5..3efb7e5 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -660,3 +660,21 @@ Admin user already exists - `05:32:00` **INFO** wrote 7584 chars in 61.9s (attempt 1) - `05:32:00` **INFO** Running tsc --noEmit on api… - `05:32:02` **INFO** tsc clean ✓ +- `05:32:02` **INFO** Committed feature password-change +- `05:32:02` **INFO** Pushed: rc=0 + +## Phase-3 Feature: audit-log (2026-05-23 05:32:02) + +- `05:32:02` **INFO** Description: Audit-Log Tabelle + Page (admin-only) +- `05:32:02` **INFO** Generating apps/api/src/db/schema.ts (ERWEITERT — behalte alle Tabellen. Füge `auditLog` (pgTable 'audit_log…) +- `05:32:20` **INFO** wrote 2167 chars in 18.5s (attempt 1) +- `05:32:20` **INFO** Generating apps/api/src/routes/audit-log.ts (Fastify-Plugin /api/audit-log. GET / (admin only, returns last 100 ent…) +- `05:32:28` **INFO** wrote 848 chars in 8.0s (attempt 1) +- `05:32:28` **INFO** Generating apps/web/src/pages/AuditLog.tsx (AuditLog-Page (admin-only). Tabelle: When / User / Action / Resource. …) +- `05:33:01` **INFO** wrote 3903 chars in 33.0s (attempt 1) +- `05:33:01` **INFO** Running tsc --noEmit on api… +- `05:33:03` **WARN** tsc errors: +src/routes/audit-log.ts(3,10): error TS2724: '"../db/schema"' has no exported member named 'auditLogs'. Did you mean 'auditLog'? +undefined +/home/dark/Developer/EmberClone/apps/api: + ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL  Command failed with exit code 2: tsc --noEmit -p tsconfig.json diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 35872f1..7eb7071 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -40,4 +40,14 @@ export const appSettings = pgTable("app_settings", { defaultBillable: boolean("default_billable").notNull().default(true), weekStart: integer("week_start").notNull().default(1), updatedAt: timestamp("updated_at").notNull().defaultNow() +}) + +export const auditLog = pgTable("audit_log", { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id").references(() => users.id), + action: text("action").notNull(), + resourceType: text("resource_type"), + resourceId: text("resource_id"), + metadata: text("metadata"), + createdAt: timestamp("created_at").notNull().defaultNow() }) \ No newline at end of file diff --git a/apps/api/src/routes/audit-log.ts b/apps/api/src/routes/audit-log.ts new file mode 100644 index 0000000..9f977a9 --- /dev/null +++ b/apps/api/src/routes/audit-log.ts @@ -0,0 +1,32 @@ +import { FastifyInstance } from "fastify" +import { db } from "../db" +import { auditLogs } from "../db/schema" +import { desc } from "drizzle-orm" + +export default async function auditLogRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + const isAdmin = (request: any) => { + return (request.user as { sub: string, role: string })?.role === "admin" + } + + fastify.get("/", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + const logs = await db + .select() + .from(auditLogs) + .orderBy(desc(auditLogs.createdAt)) + .limit(100) + + return logs + }) +} \ No newline at end of file diff --git a/apps/web/src/pages/AuditLog.tsx b/apps/web/src/pages/AuditLog.tsx new file mode 100644 index 0000000..e671653 --- /dev/null +++ b/apps/web/src/pages/AuditLog.tsx @@ -0,0 +1,98 @@ +import { useState } from "react" +import { useQuery } from "@tanstack/react-query" +import { api } from "../lib/api" +import type { AuditLogEntry } from "@emberclone/shared" + +export default function AuditLog() { + const [me, setMe] = useState<{ role: string } | null>(null) + const [isMeLoading, setIsMeLoading] = useState(true) + + const { data: currentUser } = useQuery({ + queryKey: ["me"], + queryFn: () => api.getMe(), + onSuccess: (data) => { + setMe(data) + setIsMeLoading(false) + } + }) + + const { data: logs, isLoading, isError } = useQuery({ + queryKey: ["auditLogs"], + queryFn: () => api.listAuditLog(), + enabled: !!currentUser && currentUser.role === "admin" + }) + + if (isMeLoading || !currentUser) { + return
Loading permissions...
+ } + + if (currentUser.role !== "admin") { + return ( +
+
+

Forbidden

+

You do not have administrative privileges to access the audit logs.

+
+
+ ) + } + + if (isLoading) return
Loading audit logs...
+ if (isError) return
Error loading audit logs.
+ + return ( +
+
+

Audit Log

+

System-wide activity and security events

+
+ +
+
+ + + + + + + + + + + {logs && logs.length > 0 ? ( + logs.map((log: AuditLogEntry) => ( + + + + + + + )) + ) : ( + + + + )} + +
WhenUserActionResource
+ {new Date(log.createdAt).toLocaleString()} + + {log.userId} + + + {log.action} + + + {log.resourceId} +
+ No audit logs found. +
+
+
+
+ ) +} \ No newline at end of file