BEGIN; CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Drop all managed tables to ensure clean rebuild DROP TABLE IF EXISTS auth_sync_state CASCADE; DROP TABLE IF EXISTS user_scope_permissions CASCADE; DROP TABLE IF EXISTS permission_group_permissions CASCADE; DROP TABLE IF EXISTS permission_group_members CASCADE; DROP TABLE IF EXISTS permission_groups CASCADE; DROP TABLE IF EXISTS modules CASCADE; DROP TABLE IF EXISTS systems CASCADE; DROP TABLE IF EXISTS sites CASCADE; DROP TABLE IF EXISTS companies CASCADE; DROP TABLE IF EXISTS users CASCADE; DROP TABLE IF EXISTS api_clients CASCADE; -- remove legacy table if present DROP TABLE IF EXISTS permissions CASCADE; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), authentik_sub TEXT NOT NULL UNIQUE, authentik_user_id INTEGER, email TEXT UNIQUE, display_name TEXT, status VARCHAR(16) NOT NULL DEFAULT 'active', is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE auth_sync_state ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, last_synced_at TIMESTAMPTZ, source_version TEXT, last_error TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE sites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_key TEXT NOT NULL UNIQUE, company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE systems ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), system_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE modules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), system_key TEXT NOT NULL REFERENCES systems(system_key) ON DELETE CASCADE, module_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- direct permission table retained only for compatibility CREATE TABLE user_scope_permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE, action VARCHAR(32) NOT NULL, scope_type VARCHAR(16) NOT NULL, company_id UUID REFERENCES companies(id) ON DELETE CASCADE, site_id UUID REFERENCES sites(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT user_scope_permissions_scope_check CHECK (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL), CONSTRAINT user_scope_permissions_action_check CHECK (action IN ('view', 'edit')) ); CREATE TABLE permission_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE permission_group_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE, authentik_sub TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_permission_group_members_group_sub UNIQUE (group_id, authentik_sub) ); CREATE TABLE permission_group_permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE, system TEXT NOT NULL, module TEXT NOT NULL, action TEXT NOT NULL, scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT permission_group_permissions_scope_check CHECK (scope_type = 'site'), CONSTRAINT permission_group_permissions_action_check CHECK (action IN ('view', 'edit')), CONSTRAINT uq_pgp_group_rule UNIQUE (group_id, system, module, action, scope_type, scope_id) ); CREATE TABLE api_clients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), client_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', api_key_hash TEXT NOT NULL, allowed_origins JSONB NOT NULL DEFAULT '[]'::jsonb, allowed_ips JSONB NOT NULL DEFAULT '[]'::jsonb, allowed_paths JSONB NOT NULL DEFAULT '[]'::jsonb, rate_limit_per_min INTEGER, expires_at TIMESTAMPTZ, last_used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); INSERT INTO systems (system_key, name, status) VALUES ('member', 'Member Center', 'active') ON CONFLICT (system_key) DO NOTHING; CREATE INDEX idx_users_authentik_sub ON users(authentik_sub); CREATE INDEX idx_sites_company_id ON sites(company_id); CREATE INDEX idx_usp_user_id ON user_scope_permissions(user_id); CREATE INDEX idx_usp_module_id ON user_scope_permissions(module_id); CREATE INDEX idx_usp_site_id ON user_scope_permissions(site_id); CREATE UNIQUE INDEX uq_usp_site ON user_scope_permissions(user_id, module_id, action, scope_type, site_id); CREATE INDEX idx_pgm_group_id ON permission_group_members(group_id); CREATE INDEX idx_pgm_authentik_sub ON permission_group_members(authentik_sub); CREATE INDEX idx_pgp_group_id ON permission_group_permissions(group_id); CREATE INDEX idx_pgp_scope_site ON permission_group_permissions(scope_id); CREATE INDEX idx_api_clients_status ON api_clients(status); CREATE INDEX idx_api_clients_expires_at ON api_clients(expires_at); CREATE INDEX idx_systems_system_key ON systems(system_key); CREATE INDEX idx_modules_system_key ON modules(system_key); CREATE INDEX idx_modules_module_key ON modules(module_key); COMMIT;