mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-21 23:14:26 +00:00
feat(repository): implement user repository (#10967)
## Summary This PR introduces the relational user repository for backend v3, including schema, repository APIs, reducers/projections, and tests for both human and machine users. ## What changed * Added user-focused relational migrations (users, verifications, metadata, PATs, machine keys, passkeys, identity provider links) and related FK wiring. * Implemented/expanded repository domain contracts and storage implementations for human and machine user flows. * Added support for machine secret handling and aligned access token type modeling. * Extended user domain models (verifications, password/email/phone/TOTP state, passkeys, metadata, invite handling, identity provider links). * Updated projection/reduction coverage and integration tests for key user lifecycle events. * Regenerated and updated mocks/enums plus supporting database change/column utilities. ## Notes for reviewers * This PR is large because it establishes the foundational relational user storage layer and related test scaffolding. Sorry for that. ## Closes Closes #10218 Closes #10224 Closes #10225 Closes #10209 Closes #10802 --------- Co-authored-by: Iraq Jaber <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Marco A. <[email protected]> Co-authored-by: Tim Möhlmann <[email protected]>
This commit is contained in:
co-authored by
Iraq Jaber
Copilot
Marco A.
Tim Möhlmann
parent
40c7b38ca1
commit
5df8a97f9d
@@ -14,7 +14,6 @@ CREATE TABLE zitadel.authorizations
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (instance_id, id),
|
||||
-- FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (instance_id, project_id) REFERENCES zitadel.projects (instance_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (instance_id, grant_id) REFERENCES zitadel.project_grants (instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ CREATE TABLE zitadel.sessions (
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id)
|
||||
-- , FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
, FOREIGN KEY (instance_id, user_agent_id) REFERENCES zitadel.session_user_agents(instance_id, fingerprint_id) ON DELETE SET NULL (user_agent_id)
|
||||
);
|
||||
|
||||
|
||||
-2
@@ -31,8 +31,6 @@ CREATE TABLE zitadel.identity_provider_intents
|
||||
expires_at TIMESTAMPTZ,
|
||||
|
||||
PRIMARY KEY (instance_id, id),
|
||||
-- TODO(@adlerhurst): easiest would be to create the index in the user migrations so that we can merge this one but the problem is that the migration might fail if there are already intents created.
|
||||
-- FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (instance_id, idp_id) REFERENCES zitadel.identity_providers (instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 013_user_table/up.sql
|
||||
up013UserTable string
|
||||
//go:embed 013_user_table/down.sql
|
||||
down013UserTable string
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerSQLMigration(13, up013UserTable, down013UserTable)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
DROP TABLE IF EXISTS zitadel.user_identity_provider_links;
|
||||
DROP TABLE IF EXISTS zitadel.user_passkeys;
|
||||
DROP TYPE IF EXISTS zitadel.passkey_type;
|
||||
DROP TABLE IF EXISTS zitadel.machine_keys;
|
||||
DROP TABLE IF EXISTS zitadel.user_personal_access_tokens;
|
||||
DROP TABLE IF EXISTS zitadel.user_metadata CASCADE;
|
||||
|
||||
ALTER TABLE zitadel.identity_provider_intents DROP CONSTRAINT IF EXISTS fk_idp_intent_user;
|
||||
ALTER TABLE zitadel.sessions DROP CONSTRAINT IF EXISTS fk_session_user;
|
||||
ALTER TABLE zitadel.authorizations DROP CONSTRAINT IF EXISTS fk_authorization_user;
|
||||
|
||||
DROP TABLE IF EXISTS zitadel.users;
|
||||
DROP TYPE IF EXISTS zitadel.user_type;
|
||||
DROP TYPE IF EXISTS zitadel.user_state;
|
||||
@@ -0,0 +1,228 @@
|
||||
CREATE TYPE zitadel.user_state AS ENUM (
|
||||
'initial'
|
||||
, 'active'
|
||||
, 'inactive'
|
||||
, 'locked'
|
||||
, 'suspended'
|
||||
);
|
||||
|
||||
CREATE TYPE zitadel.user_type AS ENUM (
|
||||
'human'
|
||||
, 'machine'
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.users(
|
||||
instance_id TEXT NOT NULL
|
||||
, organization_id TEXT NOT NULL
|
||||
, id TEXT NOT NULL CHECK (id <> '')
|
||||
|
||||
, username TEXT NOT NULL CHECK (username <> '')
|
||||
, username_org_unique BOOLEAN DEFAULT FALSE NOT NULL -- this field MUST be filled if the username must be unique on organization level
|
||||
, state zitadel.user_state NOT NULL DEFAULT 'active'
|
||||
, type zitadel.user_type NOT NULL
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, FOREIGN KEY (instance_id, organization_id) REFERENCES zitadel.organizations(instance_id, id) ON DELETE CASCADE
|
||||
|
||||
-- human
|
||||
|
||||
, first_name TEXT CHECK ((type = 'machine' AND first_name IS NULL) OR (type = 'human' AND first_name <> ''))
|
||||
, last_name TEXT CHECK ((type = 'machine' AND last_name IS NULL) OR (type = 'human' AND last_name <> ''))
|
||||
, nickname TEXT CHECK ((type = 'machine' AND nickname IS NULL) OR (type = 'human'))
|
||||
, display_name TEXT CHECK ((type = 'machine' AND display_name IS NULL) OR (type = 'human'))
|
||||
, preferred_language TEXT CHECK ((type = 'machine' AND preferred_language IS NULL) OR (type = 'human'))
|
||||
, gender SMALLINT CHECK ((type = 'machine' AND gender IS NULL) OR (type = 'human'))
|
||||
, avatar_key TEXT CHECK ((type = 'machine' AND avatar_key IS NULL) OR (type = 'human'))
|
||||
, multifactor_initialization_skipped_at TIMESTAMPTZ CHECK ((type = 'machine' AND multifactor_initialization_skipped_at IS NULL) OR (type = 'human'))
|
||||
|
||||
, password_hash TEXT CHECK ((type = 'machine' AND password_hash IS NULL) OR (type = 'human'))
|
||||
, password_change_required BOOLEAN CHECK ((type = 'machine' AND password_change_required IS NULL) OR (type = 'human'))
|
||||
, password_changed_at TIMESTAMPTZ CHECK ((type = 'machine' AND password_changed_at IS NULL) OR (type = 'human'))
|
||||
, password_verification_id TEXT CHECK ((type = 'machine' AND password_verification_id IS NULL) OR (type = 'human')) -- used for reset password flow
|
||||
, password_last_successful_check TIMESTAMPTZ CHECK ((type = 'machine' AND password_last_successful_check IS NULL) OR (type = 'human'))
|
||||
, password_failed_attempts SMALLINT CHECK ((type = 'machine' AND password_failed_attempts IS NULL) OR (type = 'human') AND (password_failed_attempts >= 0))
|
||||
|
||||
, email TEXT CHECK ((type = 'machine' AND email IS NULL) OR (type = 'human'))
|
||||
, unverified_email TEXT CHECK ((type = 'machine' AND unverified_email IS NULL) OR (type = 'human' AND unverified_email <> '')) -- after successful verification this column is not cleared.
|
||||
, email_verified_at TIMESTAMPTZ CHECK ((type = 'machine' AND email_verified_at IS NULL) OR (type = 'human'))
|
||||
, email_verification_id TEXT CHECK ((type = 'machine' AND email_verification_id IS NULL) OR (type = 'human' AND (email IS DISTINCT FROM unverified_email OR email_verification_id IS NULL)))
|
||||
, email_otp_enabled_at TIMESTAMPTZ CHECK ((type = 'machine' AND email_otp_enabled_at IS NULL) OR (type = 'human'))
|
||||
, email_otp_last_successful_check TIMESTAMPTZ CHECK ((type = 'machine' AND email_otp_last_successful_check IS NULL) OR (type = 'human'))
|
||||
, email_otp_failed_attempts SMALLINT CHECK ((type = 'machine' AND email_otp_failed_attempts IS NULL) OR (type = 'human'))
|
||||
|
||||
, phone TEXT CHECK ((type = 'machine' AND phone IS NULL) OR (type = 'human'))
|
||||
, unverified_phone TEXT CHECK ((type = 'machine' AND unverified_phone IS NULL) OR (type = 'human')) -- after successful verification this column is not cleared.
|
||||
, phone_verified_at TIMESTAMPTZ CHECK ((type = 'machine' AND phone_verified_at IS NULL) OR (type = 'human'))
|
||||
, phone_verification_id TEXT CHECK ((type = 'machine' AND phone_verification_id IS NULL) OR (type = 'human' AND (phone IS DISTINCT FROM unverified_phone OR phone_verification_id IS NULL)))
|
||||
, sms_otp_enabled_at TIMESTAMPTZ CHECK ((type = 'machine' AND sms_otp_enabled_at IS NULL) OR (type = 'human'))
|
||||
, sms_otp_last_successful_check TIMESTAMPTZ CHECK ((type = 'machine' AND sms_otp_last_successful_check IS NULL) OR (type = 'human'))
|
||||
, sms_otp_failed_attempts SMALLINT CHECK ((type = 'machine' AND sms_otp_failed_attempts IS NULL) OR (type = 'human'))
|
||||
|
||||
, totp_secret BYTEA CHECK ((type = 'machine' AND totp_secret IS NULL) OR (type = 'human'))
|
||||
, totp_verified_at TIMESTAMPTZ CHECK ((type = 'machine' AND totp_verified_at IS NULL) OR (type = 'human'))
|
||||
, totp_last_successful_check TIMESTAMPTZ CHECK ((type = 'machine' AND totp_last_successful_check IS NULL) OR (type = 'human'))
|
||||
, totp_failed_attempts SMALLINT CHECK ((type = 'machine' AND totp_failed_attempts IS NULL) OR (type = 'human'))
|
||||
|
||||
, invite_verification_id TEXT CHECK ((type = 'machine' AND invite_verification_id IS NULL) OR (type = 'human'))
|
||||
, invite_accepted_at TIMESTAMPTZ CHECK ((type = 'machine' AND invite_accepted_at IS NULL) OR (type = 'human'))
|
||||
, invite_failed_attempts SMALLINT CHECK ((type = 'machine' AND invite_failed_attempts IS NULL) OR (type = 'human'))
|
||||
|
||||
-- foreign keys for verifications are created in the verification migration
|
||||
|
||||
-- machine
|
||||
|
||||
, name TEXT CHECK ((type = 'human' AND name IS NULL) OR (type = 'machine'))
|
||||
, description TEXT CHECK ((type = 'human' AND description IS NULL) OR (type = 'machine'))
|
||||
, secret TEXT CHECK ((type = 'human' AND secret IS NULL) OR (type = 'machine'))
|
||||
, access_token_type SMALLINT CHECK ((type = 'human' AND access_token_type IS NULL) OR (type = 'machine'))
|
||||
);
|
||||
|
||||
-- previously created tables
|
||||
ALTER TABLE zitadel.identity_provider_intents ADD CONSTRAINT fk_idp_intent_user FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE;
|
||||
ALTER TABLE zitadel.sessions ADD CONSTRAINT fk_session_user FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE;
|
||||
ALTER TABLE zitadel.authorizations ADD CONSTRAINT fk_authorization_user FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE;
|
||||
|
||||
-- user
|
||||
CREATE UNIQUE INDEX ON zitadel.users(instance_id, organization_id, username) WHERE username_org_unique IS TRUE; --TODO(adlerhurst): does that work if a username is already present on a user without org unique?
|
||||
CREATE UNIQUE INDEX ON zitadel.users(instance_id, username) WHERE username_org_unique IS FALSE;
|
||||
CREATE INDEX idx_user_username ON zitadel.users (username);
|
||||
CREATE INDEX idx_user_username_lower ON zitadel.users (lower(username));
|
||||
CREATE INDEX idx_machine_name ON zitadel.users (name);
|
||||
CREATE INDEX idx_human_email ON zitadel.users (email);
|
||||
CREATE INDEX idx_human_email_lower ON zitadel.users (lower(email));
|
||||
CREATE INDEX idx_human_phone ON zitadel.users (phone);
|
||||
CREATE INDEX idx_human_phone_lower ON zitadel.users (lower(phone));
|
||||
|
||||
-- human
|
||||
CREATE UNIQUE INDEX ON zitadel.users(password_verification_id) WHERE password_verification_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX ON zitadel.users(email_verification_id) WHERE email_verification_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX ON zitadel.users(phone_verification_id) WHERE phone_verification_id IS NOT NULL;
|
||||
|
||||
CREATE TRIGGER trigger_set_updated_at
|
||||
BEFORE UPDATE ON zitadel.users
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.updated_at IS NULL)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
|
||||
-- ----------------------------------------------------------------
|
||||
-- user metadata
|
||||
-- ----------------------------------------------------------------
|
||||
|
||||
CREATE TABLE zitadel.user_metadata (
|
||||
instance_id TEXT NOT NULL
|
||||
, user_id TEXT NOT NULL
|
||||
, key TEXT NOT NULL CHECK (key <> '')
|
||||
, value BYTEA NOT NULL
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
|
||||
, PRIMARY KEY (instance_id, user_id, key)
|
||||
, FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_metadata_key ON zitadel.user_metadata (key);
|
||||
CREATE INDEX idx_user_metadata_value ON zitadel.user_metadata (sha256(value));
|
||||
|
||||
CREATE TRIGGER trg_set_updated_at_user_metadata
|
||||
BEFORE INSERT OR UPDATE ON zitadel.user_metadata
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.updated_at IS NULL)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
|
||||
-- ----------------------------------------------------------------
|
||||
-- personal access tokens
|
||||
-- ----------------------------------------------------------------
|
||||
|
||||
CREATE TABLE zitadel.user_personal_access_tokens(
|
||||
instance_id TEXT NOT NULL
|
||||
, user_id TEXT NOT NULL
|
||||
|
||||
, id TEXT NOT NULL
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, expires_at TIMESTAMPTZ
|
||||
, scopes TEXT[]
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- ----------------------------------------------------------------
|
||||
-- machine keys
|
||||
-- ----------------------------------------------------------------
|
||||
|
||||
CREATE TABLE zitadel.machine_keys(
|
||||
instance_id TEXT NOT NULL
|
||||
, user_id TEXT NOT NULL
|
||||
|
||||
, id TEXT NOT NULL
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, expires_at TIMESTAMPTZ
|
||||
|
||||
, type SMALLINT NOT NULL CHECK (type >= 0)
|
||||
, public_key BYTEA NOT NULL
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- ----------------------------------------------------------------
|
||||
-- passkeys
|
||||
-- ----------------------------------------------------------------
|
||||
|
||||
CREATE TYPE zitadel.passkey_type AS ENUM (
|
||||
'passwordless'
|
||||
, 'u2f'
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.user_passkeys(
|
||||
instance_id TEXT NOT NULL
|
||||
, token_id TEXT NOT NULL
|
||||
, key_id BYTEA NOT NULL
|
||||
|
||||
, user_id TEXT NOT NULL
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, verified_at TIMESTAMPTZ
|
||||
|
||||
, type zitadel.passkey_type NOT NULL
|
||||
, name TEXT NOT NULL CHECK (name <> '')
|
||||
, sign_count INT NOT NULL DEFAULT 0 CHECK (sign_count >= 0)
|
||||
, challenge BYTEA NOT NULL
|
||||
, public_key BYTEA NOT NULL
|
||||
, attestation_type TEXT NOT NULL CHECK (attestation_type <> '')
|
||||
, authenticator_attestation_guid BYTEA NOT NULL
|
||||
, relying_party_id TEXT NOT NULL CHECK (relying_party_id <> '')
|
||||
|
||||
, PRIMARY KEY (instance_id, token_id)
|
||||
, FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_passkeys_challenge ON zitadel.user_passkeys (sha256(challenge));
|
||||
CREATE INDEX idx_user_passkeys_key_id ON zitadel.user_passkeys (key_id);
|
||||
CREATE INDEX idx_user_passkeys_type ON zitadel.user_passkeys (type);
|
||||
|
||||
-- ----------------------------------------------------------------
|
||||
-- identity provider links
|
||||
-- ----------------------------------------------------------------
|
||||
|
||||
CREATE TABLE zitadel.user_identity_provider_links(
|
||||
instance_id TEXT NOT NULL
|
||||
, identity_provider_id TEXT NOT NULL
|
||||
, user_id TEXT NOT NULL
|
||||
|
||||
, provided_user_id TEXT NOT NULL CHECK(provided_user_id <> '')
|
||||
, provided_username TEXT NOT NULL
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
|
||||
, PRIMARY KEY (instance_id, identity_provider_id, provided_user_id)
|
||||
|
||||
, FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
, FOREIGN KEY (instance_id, identity_provider_id) REFERENCES zitadel.identity_providers(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 014_verification_table/up.sql
|
||||
up014VerificationTable string
|
||||
//go:embed 014_verification_table/down.sql
|
||||
down014VerificationTable string
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerSQLMigration(14, up014VerificationTable, down014VerificationTable)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE zitadel.users DROP CONSTRAINT IF EXISTS fk_unverified_password;
|
||||
ALTER TABLE zitadel.users DROP CONSTRAINT IF EXISTS fk_unverified_email;
|
||||
ALTER TABLE zitadel.users DROP CONSTRAINT IF EXISTS fk_unverified_phone;
|
||||
|
||||
DROP TRIGGER IF EXISTS user_verification_integrity_trigger ON zitadel.users;
|
||||
DROP FUNCTION IF EXISTS zitadel.ensure_user_verification_integrity() CASCADE;
|
||||
DROP TABLE IF EXISTS zitadel.verifications CASCADE;
|
||||
@@ -0,0 +1,61 @@
|
||||
CREATE TABLE zitadel.verifications(
|
||||
instance_id TEXT NOT NULL
|
||||
, user_id TEXT
|
||||
, id TEXT NOT NULL DEFAULT gen_random_uuid()::TEXT
|
||||
|
||||
, code BYTEA
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
, expiry INTERVAL
|
||||
|
||||
, failed_attempts SMALLINT NOT NULL DEFAULT 0 CHECK (failed_attempts >= 0)
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
ALTER TABLE zitadel.users ADD CONSTRAINT fk_unverified_password FOREIGN KEY (instance_id, password_verification_id) REFERENCES zitadel.verifications(instance_id, id) ON DELETE SET NULL (password_verification_id);
|
||||
ALTER TABLE zitadel.users ADD CONSTRAINT fk_unverified_email FOREIGN KEY (instance_id, email_verification_id) REFERENCES zitadel.verifications(instance_id, id) ON DELETE SET NULL (email_verification_id);
|
||||
ALTER TABLE zitadel.users ADD CONSTRAINT fk_unverified_phone FOREIGN KEY (instance_id, phone_verification_id) REFERENCES zitadel.verifications(instance_id, id) ON DELETE SET NULL (phone_verification_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION zitadel.ensure_user_verification_integrity() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (NEW.password_verification_id IS DISTINCT FROM OLD.password_verification_id) THEN
|
||||
DELETE FROM zitadel.verifications
|
||||
WHERE instance_id = NEW.instance_id
|
||||
AND id = OLD.password_verification_id;
|
||||
END IF;
|
||||
|
||||
IF (NEW.email_verification_id IS DISTINCT FROM OLD.email_verification_id) THEN
|
||||
DELETE FROM zitadel.verifications
|
||||
WHERE instance_id = NEW.instance_id
|
||||
AND id = OLD.email_verification_id;
|
||||
END IF;
|
||||
|
||||
IF (NEW.phone_verification_id IS DISTINCT FROM OLD.phone_verification_id) THEN
|
||||
DELETE FROM zitadel.verifications
|
||||
WHERE instance_id = NEW.instance_id
|
||||
AND id = OLD.phone_verification_id;
|
||||
END IF;
|
||||
|
||||
IF (NEW.invite_verification_id IS DISTINCT FROM OLD.invite_verification_id) THEN
|
||||
DELETE FROM zitadel.verifications
|
||||
WHERE instance_id = NEW.instance_id
|
||||
AND id = OLD.invite_verification_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER user_verification_integrity_trigger
|
||||
AFTER UPDATE ON zitadel.users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION zitadel.ensure_user_verification_integrity();
|
||||
|
||||
CREATE TRIGGER trigger_set_updated_at
|
||||
BEFORE UPDATE ON zitadel.verifications
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.updated_at IS NULL)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
Reference in New Issue
Block a user