CREATE TABLE IF NOT EXISTS gateway.departments ( id uuid PRIMARY KEY, tenant_id uuid, code text NOT NULL, name text NOT NULL, active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE NULLS NOT DISTINCT (tenant_id, code) ); CREATE TABLE IF NOT EXISTS gateway.admin_accounts ( id uuid PRIMARY KEY, username varchar(64) NOT NULL, password_hash text NOT NULL, display_name varchar(64) NOT NULL DEFAULT '', role varchar(24) NOT NULL DEFAULT 'operator' CHECK (role IN ('superadmin', 'operator', 'auditor')), active boolean NOT NULL DEFAULT true, failed_logins integer NOT NULL DEFAULT 0 CHECK (failed_logins >= 0), locked_until timestamptz, last_login timestamptz, encrypted_totp_secret bytea, totp_kek_version integer, totp_enabled boolean NOT NULL DEFAULT false, totp_last_step bigint, totp_backup_codes jsonb NOT NULL DEFAULT '[]'::jsonb, totp_confirmed_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE UNIQUE INDEX IF NOT EXISTS admin_accounts_username_lower_idx ON gateway.admin_accounts (lower(username)); CREATE TABLE IF NOT EXISTS gateway.portal_users ( id uuid PRIMARY KEY, tenant_id uuid, account varchar(128) NOT NULL, name varchar(64) NOT NULL DEFAULT '', department_id uuid REFERENCES gateway.departments(id), password_hash text, auth_source varchar(24) NOT NULL DEFAULT 'local' CHECK (auth_source IN ('local', 'feishu', 'oidc', 'saml')), external_subject text, active boolean NOT NULL DEFAULT true, provisioning_status varchar(24) NOT NULL DEFAULT 'active' CHECK (provisioning_status IN ('active', 'pending', 'rejected')), failed_logins integer NOT NULL DEFAULT 0 CHECK (failed_logins >= 0), locked_until timestamptz, last_login timestamptz, encrypted_totp_secret bytea, totp_kek_version integer, totp_enabled boolean NOT NULL DEFAULT false, totp_last_step bigint, totp_backup_codes jsonb NOT NULL DEFAULT '[]'::jsonb, totp_confirmed_at timestamptz, deleted_name_snapshot varchar(128) NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE UNIQUE INDEX IF NOT EXISTS portal_users_account_lower_idx ON gateway.portal_users (lower(account)); CREATE INDEX IF NOT EXISTS portal_users_department_idx ON gateway.portal_users (department_id, active); CREATE UNIQUE INDEX IF NOT EXISTS portal_users_external_subject_idx ON gateway.portal_users (auth_source, external_subject) WHERE external_subject IS NOT NULL; ALTER TABLE gateway.api_keys ADD COLUMN IF NOT EXISTS user_id uuid REFERENCES gateway.portal_users(id), ADD COLUMN IF NOT EXISTS department_id uuid REFERENCES gateway.departments(id); ALTER TABLE gateway.providers ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES gateway.admin_accounts(id); COMMENT ON COLUMN gateway.admin_accounts.password_hash IS 'PBKDF2-SHA256 self-describing hash; compatible with the Python gateway format.'; COMMENT ON TABLE gateway.portal_users IS 'Portal identities. Runtime API keys remain separate credentials.';