From a2e4393d051d3e274acd02d7e435bb1bbdf123d9 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Wed, 24 Jun 2026 15:48:00 +0200 Subject: [PATCH] feat(w5): dual Flyway history (V1-V6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrations in db/migration/auth/ with separate flyway_schema_history_auth table: - V1: users + user_identities (with provider/subject unique constraint) - V2: memberships (polymorphic org_type/org_id, unique per user+org) - V3: invitations (64-char token, status lifecycle) - V4: access_requests (requester → reviewer workflow) - V5: Microsoft tenant_id partial index on user_identities - V6: login_events + refresh_tokens + revinfo actor_user_id column PlateAuthFlywayConfig runs a second Flyway bean against flyway_schema_history_auth, independent of consumer's own flyway_schema_history. Runs at bean init (before JPA). --- .../auth/config/PlateAuthFlywayConfig.java | 28 ++++++++++++ .../auth/V1__create_users_and_identities.sql | 32 ++++++++++++++ .../migration/auth/V2__create_memberships.sql | 20 +++++++++ .../migration/auth/V3__create_invitations.sql | 23 ++++++++++ .../auth/V4__create_access_requests.sql | 19 ++++++++ .../V5__add_microsoft_tenant_id_index.sql | 3 ++ ..._create_login_events_and_revinfo_actor.sql | 43 +++++++++++++++++++ 7 files changed, 168 insertions(+) create mode 100644 plate-auth-starter/src/main/java/de/platesoft/auth/config/PlateAuthFlywayConfig.java create mode 100644 plate-auth-starter/src/main/resources/db/migration/auth/V1__create_users_and_identities.sql create mode 100644 plate-auth-starter/src/main/resources/db/migration/auth/V2__create_memberships.sql create mode 100644 plate-auth-starter/src/main/resources/db/migration/auth/V3__create_invitations.sql create mode 100644 plate-auth-starter/src/main/resources/db/migration/auth/V4__create_access_requests.sql create mode 100644 plate-auth-starter/src/main/resources/db/migration/auth/V5__add_microsoft_tenant_id_index.sql create mode 100644 plate-auth-starter/src/main/resources/db/migration/auth/V6__create_login_events_and_revinfo_actor.sql diff --git a/plate-auth-starter/src/main/java/de/platesoft/auth/config/PlateAuthFlywayConfig.java b/plate-auth-starter/src/main/java/de/platesoft/auth/config/PlateAuthFlywayConfig.java new file mode 100644 index 0000000..f34ac34 --- /dev/null +++ b/plate-auth-starter/src/main/java/de/platesoft/auth/config/PlateAuthFlywayConfig.java @@ -0,0 +1,28 @@ +package de.platesoft.auth.config; + +import org.flywaydb.core.Flyway; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + * Configures a separate Flyway instance for plate-auth migrations. + * Uses its own history table (flyway_schema_history_auth) to avoid + * version collisions with the consumer application's migrations. + */ +@Configuration +@ConditionalOnClass(Flyway.class) +public class PlateAuthFlywayConfig { + + @Bean(initMethod = "migrate") + public Flyway plateAuthFlyway(DataSource dataSource) { + return Flyway.configure() + .dataSource(dataSource) + .locations("classpath:db/migration/auth") + .table("flyway_schema_history_auth") + .baselineOnMigrate(true) + .load(); + } +} diff --git a/plate-auth-starter/src/main/resources/db/migration/auth/V1__create_users_and_identities.sql b/plate-auth-starter/src/main/resources/db/migration/auth/V1__create_users_and_identities.sql new file mode 100644 index 0000000..e23e590 --- /dev/null +++ b/plate-auth-starter/src/main/resources/db/migration/auth/V1__create_users_and_identities.sql @@ -0,0 +1,32 @@ +-- V1: Create users and user_identities tables +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255), + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + role VARCHAR(50) NOT NULL DEFAULT 'ROLE_USER', + active BOOLEAN NOT NULL DEFAULT TRUE, + default_org_id UUID, + last_provider VARCHAR(32), + version BIGINT DEFAULT 0, + last_login TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS user_identities ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider VARCHAR(32) NOT NULL, + subject VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + tenant_id VARCHAR(64), + linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_login_at TIMESTAMPTZ, + version BIGINT DEFAULT 0, + CONSTRAINT uq_user_identities_provider_subject UNIQUE (provider, subject) +); + +CREATE INDEX idx_user_identities_user_id ON user_identities(user_id); +CREATE INDEX idx_users_email ON users(email); diff --git a/plate-auth-starter/src/main/resources/db/migration/auth/V2__create_memberships.sql b/plate-auth-starter/src/main/resources/db/migration/auth/V2__create_memberships.sql new file mode 100644 index 0000000..2046c40 --- /dev/null +++ b/plate-auth-starter/src/main/resources/db/migration/auth/V2__create_memberships.sql @@ -0,0 +1,20 @@ +-- V2: Create memberships table +CREATE TABLE IF NOT EXISTS memberships ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + org_type VARCHAR(16) NOT NULL, + org_id UUID NOT NULL, + role VARCHAR(16) NOT NULL DEFAULT 'MEMBER', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', + granted_by UUID, + grant_reason VARCHAR(64), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + revoked_at TIMESTAMPTZ, + revoked_by UUID, + version BIGINT DEFAULT 0, + CONSTRAINT uq_memberships_user_org UNIQUE (user_id, org_type, org_id) +); + +CREATE INDEX idx_memberships_user_id ON memberships(user_id); +CREATE INDEX idx_memberships_org ON memberships(org_type, org_id); +CREATE INDEX idx_memberships_status ON memberships(status); diff --git a/plate-auth-starter/src/main/resources/db/migration/auth/V3__create_invitations.sql b/plate-auth-starter/src/main/resources/db/migration/auth/V3__create_invitations.sql new file mode 100644 index 0000000..0fbe7b5 --- /dev/null +++ b/plate-auth-starter/src/main/resources/db/migration/auth/V3__create_invitations.sql @@ -0,0 +1,23 @@ +-- V3: Create invitations table +CREATE TABLE IF NOT EXISTS invitations ( + id UUID PRIMARY KEY, + token VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL, + org_type VARCHAR(16) NOT NULL, + org_id UUID NOT NULL, + role VARCHAR(16) NOT NULL DEFAULT 'MEMBER', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING', + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL, + accepted_at TIMESTAMPTZ, + accepted_by UUID, + revoked_at TIMESTAMPTZ, + revoked_by UUID, + note VARCHAR(500), + version BIGINT DEFAULT 0 +); + +CREATE INDEX idx_invitations_email ON invitations(email); +CREATE INDEX idx_invitations_token ON invitations(token); +CREATE INDEX idx_invitations_org ON invitations(org_type, org_id); diff --git a/plate-auth-starter/src/main/resources/db/migration/auth/V4__create_access_requests.sql b/plate-auth-starter/src/main/resources/db/migration/auth/V4__create_access_requests.sql new file mode 100644 index 0000000..33c8519 --- /dev/null +++ b/plate-auth-starter/src/main/resources/db/migration/auth/V4__create_access_requests.sql @@ -0,0 +1,19 @@ +-- V4: Create access_requests table +CREATE TABLE IF NOT EXISTS access_requests ( + id UUID PRIMARY KEY, + requester_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + org_type VARCHAR(16) NOT NULL, + org_id UUID NOT NULL, + requested_role VARCHAR(16) NOT NULL DEFAULT 'VIEWER', + justification VARCHAR(500), + status VARCHAR(16) NOT NULL DEFAULT 'PENDING', + reviewer_id UUID, + decision_reason VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + decided_at TIMESTAMPTZ, + version BIGINT DEFAULT 0 +); + +CREATE INDEX idx_access_requests_requester ON access_requests(requester_id); +CREATE INDEX idx_access_requests_org ON access_requests(org_type, org_id); +CREATE INDEX idx_access_requests_status ON access_requests(status); diff --git a/plate-auth-starter/src/main/resources/db/migration/auth/V5__add_microsoft_tenant_id_index.sql b/plate-auth-starter/src/main/resources/db/migration/auth/V5__add_microsoft_tenant_id_index.sql new file mode 100644 index 0000000..d2bb7c7 --- /dev/null +++ b/plate-auth-starter/src/main/resources/db/migration/auth/V5__add_microsoft_tenant_id_index.sql @@ -0,0 +1,3 @@ +-- V5: Add index on user_identities.tenant_id for Microsoft Entra lookups +CREATE INDEX IF NOT EXISTS idx_user_identities_microsoft_tenant_id + ON user_identities(tenant_id) WHERE tenant_id IS NOT NULL; diff --git a/plate-auth-starter/src/main/resources/db/migration/auth/V6__create_login_events_and_revinfo_actor.sql b/plate-auth-starter/src/main/resources/db/migration/auth/V6__create_login_events_and_revinfo_actor.sql new file mode 100644 index 0000000..b75ab3a --- /dev/null +++ b/plate-auth-starter/src/main/resources/db/migration/auth/V6__create_login_events_and_revinfo_actor.sql @@ -0,0 +1,43 @@ +-- V6: Create login_events table and revinfo actor column +CREATE TABLE IF NOT EXISTS login_events ( + id BIGSERIAL PRIMARY KEY, + user_id UUID, + email VARCHAR(255) NOT NULL, + provider VARCHAR(32) NOT NULL, + outcome VARCHAR(32) NOT NULL, + ip_address VARCHAR(45), + user_agent VARCHAR(512), + correlation_id VARCHAR(64), + occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_login_events_user_id ON login_events(user_id); +CREATE INDEX idx_login_events_email ON login_events(email); +CREATE INDEX idx_login_events_occurred_at ON login_events(occurred_at); + +-- Create refresh_tokens table +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); +CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token); + +-- Envers revinfo actor tracking (add actor_user_id if revinfo table exists) +-- This is safe to run regardless of whether Envers has created its tables yet, +-- because Hibernate Envers will create revinfo on first boot. +-- Consumer's Flyway or plate-auth's FlywayConfig runs before JPA init. +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_tables WHERE tablename = 'revinfo') THEN + IF NOT EXISTS (SELECT FROM information_schema.columns + WHERE table_name = 'revinfo' AND column_name = 'actor_user_id') THEN + ALTER TABLE revinfo ADD COLUMN actor_user_id UUID; + END IF; + END IF; +END $$;