feat(w5): dual Flyway history (V1-V6)
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).
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
+19
@@ -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);
|
||||||
+3
@@ -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;
|
||||||
+43
@@ -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 $$;
|
||||||
Reference in New Issue
Block a user