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 $$;