diff --git a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java index 8984d465..3a645d86 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditLogQueryRepository.java @@ -104,7 +104,7 @@ public interface AuditLogQueryRepository extends JpaRepository { ag.happened_at_until AS happenedAtUntil, (ag.payload->>'commentId')::uuid AS commentId FROM aggregated ag - LEFT JOIN users u ON u.id = ag.actor_id + LEFT JOIN app_users u ON u.id = ag.actor_id ORDER BY ag.happened_at DESC LIMIT :limit """, nativeQuery = true) @@ -157,7 +157,7 @@ public interface AuditLogQueryRepository extends JpaRepository { COALESCE(u.color, '') AS actorColor, CONCAT_WS(' ', u.first_name, u.last_name) AS actorName FROM audit_log a - LEFT JOIN users u ON u.id = a.actor_id + LEFT JOIN app_users u ON u.id = a.actor_id WHERE a.kind IN ('ANNOTATION_CREATED', 'TEXT_SAVED', 'BLOCK_REVIEWED') AND a.document_id IN :documentIds AND a.actor_id IS NOT NULL @@ -189,7 +189,7 @@ public interface AuditLogQueryRepository extends JpaRepository { ORDER BY MAX(a.happened_at) DESC ) AS rn FROM audit_log a - LEFT JOIN users u ON u.id = a.actor_id + LEFT JOIN app_users u ON u.id = a.actor_id WHERE a.kind IN ('ANNOTATION_CREATED', 'TEXT_SAVED', 'BLOCK_REVIEWED') AND a.document_id IN :documentIds AND a.actor_id IS NOT NULL diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/AppUser.java b/backend/src/main/java/org/raddatz/familienarchiv/model/AppUser.java index 6b2b8419..b69d71bb 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/AppUser.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/AppUser.java @@ -24,7 +24,7 @@ import jakarta.persistence.PrePersist; import jakarta.persistence.PreUpdate; @Entity -@Table(name = "users") +@Table(name = "app_users") @Data @NoArgsConstructor @AllArgsConstructor @@ -69,7 +69,7 @@ public class AppUser { private boolean notifyOnMention = false; @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "users_groups", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "group_id")) + @JoinTable(name = "app_users_groups", joinColumns = @JoinColumn(name = "app_user_id"), inverseJoinColumns = @JoinColumn(name = "group_id")) @Builder.Default @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private Set groups = new HashSet<>(); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/DocumentComment.java b/backend/src/main/java/org/raddatz/familienarchiv/model/DocumentComment.java index d64941ae..2277d2a5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/DocumentComment.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/DocumentComment.java @@ -71,7 +71,7 @@ public class DocumentComment { @JoinTable( name = "comment_mentions", joinColumns = @JoinColumn(name = "comment_id"), - inverseJoinColumns = @JoinColumn(name = "user_id") + inverseJoinColumns = @JoinColumn(name = "app_user_id") ) @JsonIgnore @Builder.Default diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/PasswordResetToken.java b/backend/src/main/java/org/raddatz/familienarchiv/model/PasswordResetToken.java index 7e4d956d..686b9f48 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/PasswordResetToken.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/PasswordResetToken.java @@ -30,7 +30,7 @@ public class PasswordResetToken { private UUID id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "app_user_id", nullable = false) private AppUser user; @Column(nullable = false, unique = true, length = 64) diff --git a/backend/src/main/resources/db/migration/V60__rename_users_to_app_users.sql b/backend/src/main/resources/db/migration/V60__rename_users_to_app_users.sql new file mode 100644 index 00000000..49216249 --- /dev/null +++ b/backend/src/main/resources/db/migration/V60__rename_users_to_app_users.sql @@ -0,0 +1,21 @@ +-- Align the auth-account table name with the AppUser entity (issue #418). +-- The historical mismatch (table 'users' alongside table 'persons') misleads +-- schema-first readers into assuming the two are related. Renaming the table to +-- 'app_users' makes the deliberate split between auth accounts and historical +-- persons explicit at the schema layer. +-- +-- Scope: the table itself, the users_groups join table, and the three FK +-- columns whose name is literally 'user_id'. Semantic FK columns +-- (audit_log.actor_id, notifications.recipient_id, document_versions.editor_id, +-- document_comments.author_id, transcription_blocks.created_by/updated_by, +-- transcription_block_versions.changed_by, document_annotations.created_by, +-- ocr_training_runs.triggered_by, invite_tokens.created_by, geschichten.author_id) +-- keep their names — the role they describe is the documentation, not the type. + +ALTER TABLE users RENAME TO app_users; + +ALTER TABLE users_groups RENAME TO app_users_groups; +ALTER TABLE app_users_groups RENAME COLUMN user_id TO app_user_id; + +ALTER TABLE comment_mentions RENAME COLUMN user_id TO app_user_id; +ALTER TABLE password_reset_tokens RENAME COLUMN user_id TO app_user_id; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryContributorsTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryContributorsTest.java index 1126b447..f9b9ad5d 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryContributorsTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryContributorsTest.java @@ -32,7 +32,7 @@ class AuditLogQueryRepositoryContributorsTest { @Test @Sql(statements = { - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#f00')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#f00')", "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test', 'test.pdf', 'PLACEHOLDER')", "INSERT INTO audit_log (kind, actor_id, document_id) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-000000000001', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')" }) @@ -47,11 +47,11 @@ class AuditLogQueryRepositoryContributorsTest { @Test @Sql(statements = { - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#aaa')", - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000002', true, 'b@test.com', 'pw', 'Ben', 'Wolf', '#bbb')", - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000003', true, 'c@test.com', 'pw', 'Clara', 'Zorn', '#ccc')", - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000004', true, 'd@test.com', 'pw', 'Dirk', 'Ott', '#ddd')", - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000005', true, 'e@test.com', 'pw', 'Eva', 'Kern', '#eee')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#aaa')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000002', true, 'b@test.com', 'pw', 'Ben', 'Wolf', '#bbb')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000003', true, 'c@test.com', 'pw', 'Clara', 'Zorn', '#ccc')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000004', true, 'd@test.com', 'pw', 'Dirk', 'Ott', '#ddd')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000005', true, 'e@test.com', 'pw', 'Eva', 'Kern', '#eee')", "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test', 'test.pdf', 'PLACEHOLDER')", "INSERT INTO audit_log (kind, actor_id, document_id, happened_at) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-000000000001', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', now() - interval '5 hours')", "INSERT INTO audit_log (kind, actor_id, document_id, happened_at) VALUES ('TEXT_SAVED', 'aaaaaaaa-aaaa-aaaa-aaaa-000000000002', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', now() - interval '4 hours')", diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java index a3a3b3ff..05febe3c 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryIntegrationTest.java @@ -33,7 +33,7 @@ class AuditLogQueryRepositoryIntegrationTest { @Test @Sql(statements = { - "INSERT INTO users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')", + "INSERT INTO app_users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')", "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')", "INSERT INTO audit_log (kind, actor_id, document_id) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')" }) @@ -45,7 +45,7 @@ class AuditLogQueryRepositoryIntegrationTest { @Test @Sql(statements = { - "INSERT INTO users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')", + "INSERT INTO app_users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')", "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')", "INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"pageNumber\":1}')" }) @@ -63,7 +63,7 @@ class AuditLogQueryRepositoryIntegrationTest { @Test @Sql(statements = { - "INSERT INTO users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')", + "INSERT INTO app_users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')", "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')", "INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"pageNumber\":1}')", "INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('TEXT_SAVED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"blockId\":\"ccc\",\"pageNumber\":1}')", @@ -82,7 +82,7 @@ class AuditLogQueryRepositoryIntegrationTest { @Test @Sql(statements = { - "INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw', 'Anna', 'Meier', '#f00')", + "INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw', 'Anna', 'Meier', '#f00')", "INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')", "INSERT INTO audit_log (kind, actor_id, document_id) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')" }) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java index c29a2a14..1e1cd764 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryRepositoryRolledUpTest.java @@ -51,10 +51,10 @@ class AuditLogQueryRepositoryRolledUpTest { private void insertUserAndDocs() { jdbcTemplate.update( - "INSERT INTO users (id, enabled, email, password) VALUES (?, true, ?, 'pw')", + "INSERT INTO app_users (id, enabled, email, password) VALUES (?, true, ?, 'pw')", USER_ID, "rollup-" + USER_ID + "@test.com"); jdbcTemplate.update( - "INSERT INTO users (id, enabled, email, password) VALUES (?, true, ?, 'pw')", + "INSERT INTO app_users (id, enabled, email, password) VALUES (?, true, ?, 'pw')", OTHER_USER_ID, "rollup-" + OTHER_USER_ID + "@test.com"); jdbcTemplate.update( "INSERT INTO documents (id, title, original_filename, status) VALUES (?, 'Brief A', 'a.pdf', 'PLACEHOLDER')", diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java index e751b85d..820705fd 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java @@ -280,7 +280,7 @@ class MigrationIntegrationTest { void v44_emailNotNullConstraint_rejectsInsertWithNullEmail() { assertThatThrownBy(() -> jdbc.update(""" - INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention) + INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention) VALUES (gen_random_uuid(), NULL, 'hash', true, false, false) """) ).isInstanceOf(DataIntegrityViolationException.class); @@ -290,13 +290,13 @@ class MigrationIntegrationTest { void v44_emailUniqueConstraint_rejectsDuplicateEmail() { String email = "unique-test-" + UUID.randomUUID() + "@example.com"; jdbc.update(""" - INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention) + INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention) VALUES (gen_random_uuid(), ?, 'hash', true, false, false) """, email); assertThatThrownBy(() -> jdbc.update(""" - INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention) + INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention) VALUES (gen_random_uuid(), ?, 'hash', true, false, false) """, email) ).isInstanceOf(DataIntegrityViolationException.class); @@ -446,7 +446,7 @@ class MigrationIntegrationTest { private UUID insertUser(String email) { UUID id = UUID.randomUUID(); jdbc.update(""" - INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention) + INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention) VALUES (?, ?, 'hash', true, false, false) """, id, email); return id; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/RenameUsersToAppUsersMigrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/RenameUsersToAppUsersMigrationTest.java new file mode 100644 index 00000000..fc8d5c6f --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/RenameUsersToAppUsersMigrationTest.java @@ -0,0 +1,73 @@ +package org.raddatz.familienarchiv.repository; + +import org.junit.jupiter.api.Test; +import org.raddatz.familienarchiv.PostgresContainerConfig; +import org.raddatz.familienarchiv.config.FlywayConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies the V60 rename: {@code users → app_users} and the three literal + * {@code user_id} FK columns. Semantic FK columns (e.g. {@code audit_log.actor_id}) + * are deliberately untouched and asserted to remain. See issue #418. + */ +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import({PostgresContainerConfig.class, FlywayConfig.class}) +class RenameUsersToAppUsersMigrationTest { + + @Autowired JdbcTemplate jdbc; + + @Test + void v60_appUsersTableExists_andUsersTableIsGone() { + assertThat(tableExists("app_users")).isTrue(); + assertThat(tableExists("users")).isFalse(); + } + + @Test + void v60_appUsersGroupsJoinTableExists_withRenamedColumn() { + assertThat(tableExists("app_users_groups")).isTrue(); + assertThat(tableExists("users_groups")).isFalse(); + assertThat(columnExists("app_users_groups", "app_user_id")).isTrue(); + assertThat(columnExists("app_users_groups", "user_id")).isFalse(); + } + + @Test + void v60_commentMentionsColumnRenamed() { + assertThat(columnExists("comment_mentions", "app_user_id")).isTrue(); + assertThat(columnExists("comment_mentions", "user_id")).isFalse(); + } + + @Test + void v60_passwordResetTokensColumnRenamed() { + assertThat(columnExists("password_reset_tokens", "app_user_id")).isTrue(); + assertThat(columnExists("password_reset_tokens", "user_id")).isFalse(); + } + + @Test + void v60_auditLogActorIdRemains() { + assertThat(columnExists("audit_log", "actor_id")).isTrue(); + assertThat(columnExists("audit_log", "app_user_id")).isFalse(); + } + + private boolean tableExists(String tableName) { + Integer count = jdbc.queryForObject( + "SELECT COUNT(*) FROM information_schema.tables " + + "WHERE table_schema = 'public' AND table_name = ?", + Integer.class, tableName); + return count != null && count > 0; + } + + private boolean columnExists(String tableName, String columnName) { + Integer count = jdbc.queryForObject( + "SELECT COUNT(*) FROM information_schema.columns " + + "WHERE table_schema = 'public' AND table_name = ? AND column_name = ?", + Integer.class, tableName, columnName); + return count != null && count > 0; + } +}