From 250a00ff3cfc39d155ed803e199ea677bae47ec5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 19 Apr 2026 16:58:04 +0200 Subject: [PATCH] =?UTF-8?q?fix(migration):=20correct=20app=5Fusers=20?= =?UTF-8?q?=E2=86=92=20users=20table=20references=20in=20V46/V47?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AppUser entity is mapped to the 'users' table (not 'app_users'). V46 had a broken REFERENCES clause and hardcoded role in REVOKE; V47 and the native query in AuditLogQueryRepository had the same wrong table name. Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/AuditLogQueryRepository.java | 88 +++++++++++++++++++ .../db/migration/V46__add_audit_log.sql | 4 +- .../db/migration/V47__add_user_color.sql | 2 +- 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepository.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepository.java new file mode 100644 index 00000000..83d9a627 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepository.java @@ -0,0 +1,88 @@ +package org.raddatz.familienarchiv.dashboard; + +import org.raddatz.familienarchiv.audit.AuditLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface AuditLogQueryRepository extends JpaRepository { + + @Query(value = """ + SELECT a.document_id + FROM audit_log a + WHERE a.kind = 'TEXT_SAVED' + AND a.actor_id = :userId + AND a.document_id IS NOT NULL + ORDER BY a.happened_at DESC + LIMIT 1 + """, nativeQuery = true) + Optional findMostRecentDocumentIdByActor(@Param("userId") UUID userId); + + @Query(value = """ + SELECT * FROM ( + SELECT DISTINCT ON (a.actor_id, a.document_id, a.kind, date_trunc('hour', a.happened_at)) + a.kind AS kind, + a.actor_id AS actorId, + CASE + WHEN u.first_name IS NOT NULL AND u.last_name IS NOT NULL + THEN UPPER(LEFT(u.first_name, 1)) || UPPER(LEFT(u.last_name, 1)) + WHEN u.first_name IS NOT NULL THEN UPPER(LEFT(u.first_name, 1)) + WHEN u.last_name IS NOT NULL THEN UPPER(LEFT(u.last_name, 1)) + ELSE '?' + END AS actorInitials, + COALESCE(u.color, '') AS actorColor, + CONCAT_WS(' ', u.first_name, u.last_name) AS actorName, + a.document_id AS documentId, + a.happened_at AS happenedAt, + (a.kind = 'MENTION_CREATED' + AND a.payload->>'mentionedUserId' = :currentUserId) AS youMentioned + FROM audit_log a + LEFT JOIN users u ON u.id = a.actor_id + WHERE a.kind IN ('TEXT_SAVED','FILE_UPLOADED','ANNOTATION_CREATED','COMMENT_ADDED','MENTION_CREATED') + AND a.document_id IS NOT NULL + ORDER BY a.actor_id, a.document_id, a.kind, + date_trunc('hour', a.happened_at), a.happened_at DESC + ) deduped + ORDER BY happened_at DESC + LIMIT :limit + """, nativeQuery = true) + List findDedupedActivityFeed( + @Param("currentUserId") String currentUserId, + @Param("limit") int limit); + + @Query(value = """ + SELECT + COUNT(DISTINCT (a.document_id::text || '|' || (a.payload->>'pageNumber'))) AS pages, + COUNT(*) FILTER (WHERE a.kind = 'ANNOTATION_CREATED') AS annotated, + COUNT(DISTINCT a.payload->>'blockId') FILTER (WHERE a.kind = 'TEXT_SAVED') AS transcribed, + COUNT(DISTINCT a.document_id) FILTER (WHERE a.kind = 'FILE_UPLOADED') AS uploaded, + COUNT(DISTINCT (a.document_id::text || '|' || (a.payload->>'pageNumber'))) + FILTER (WHERE (a.kind = 'ANNOTATION_CREATED' OR a.kind = 'TEXT_SAVED') + AND a.actor_id::text = :userId) AS yourPages + FROM audit_log a + WHERE a.happened_at >= :weekStart + AND a.kind IN ('ANNOTATION_CREATED','TEXT_SAVED','FILE_UPLOADED') + """, nativeQuery = true) + PulseStatsRow getPulseStats( + @Param("weekStart") OffsetDateTime weekStart, + @Param("userId") String userId); + + @Query(value = """ + SELECT DISTINCT ON (a.document_id) + a.document_id AS documentId, + a.actor_id AS actorId + FROM audit_log a + WHERE a.kind = :kind + AND a.document_id IN :documentIds + AND a.actor_id IS NOT NULL + ORDER BY a.document_id, a.happened_at DESC + """, nativeQuery = true) + List findMostRecentActorPerDocument( + @Param("documentIds") List documentIds, + @Param("kind") String kind); +} diff --git a/backend/src/main/resources/db/migration/V46__add_audit_log.sql b/backend/src/main/resources/db/migration/V46__add_audit_log.sql index 2a01126a..645e1d43 100644 --- a/backend/src/main/resources/db/migration/V46__add_audit_log.sql +++ b/backend/src/main/resources/db/migration/V46__add_audit_log.sql @@ -6,7 +6,7 @@ CREATE TABLE audit_log ( happened_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- ON DELETE SET NULL is by design: GDPR right-to-erasure. Deleted users' events -- retain their timestamp and kind but lose actor attribution. - actor_id UUID REFERENCES app_users(id) ON DELETE SET NULL, + actor_id UUID REFERENCES users(id) ON DELETE SET NULL, kind VARCHAR(50) NOT NULL, document_id UUID REFERENCES documents(id) ON DELETE CASCADE, payload JSONB @@ -19,4 +19,4 @@ CREATE INDEX idx_audit_log_kind ON audit_log (kind); -- Enforce append-only at the database layer: the application role may INSERT -- but must not UPDATE or DELETE audit rows. -REVOKE UPDATE, DELETE ON audit_log FROM app_user; +REVOKE UPDATE, DELETE ON audit_log FROM CURRENT_USER; diff --git a/backend/src/main/resources/db/migration/V47__add_user_color.sql b/backend/src/main/resources/db/migration/V47__add_user_color.sql index cca60905..ac6317f7 100644 --- a/backend/src/main/resources/db/migration/V47__add_user_color.sql +++ b/backend/src/main/resources/db/migration/V47__add_user_color.sql @@ -2,7 +2,7 @@ -- Assigned at application layer (AppUser.java) from a fixed 8-colour palette. -- Also corrects V46's REVOKE which hardcoded 'app_user' instead of CURRENT_USER. -ALTER TABLE app_users ADD COLUMN color VARCHAR(20) NOT NULL DEFAULT ''; +ALTER TABLE users ADD COLUMN color VARCHAR(20) NOT NULL DEFAULT ''; -- Fix V46 append-only enforcement for the actual application role. REVOKE UPDATE, DELETE ON audit_log FROM CURRENT_USER;