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