feat: sync account deletion lifecycle
Sync telesrv 73f5c91 (feat(account): implement unified account deletion lifecycle). Skipped telesrv docs changes per public sync rules.
This commit is contained in:
parent
96a419b565
commit
edb7057757
32 changed files with 3236 additions and 88 deletions
25
deploy/migrations/0107_account_lifecycle.down.sql
Normal file
25
deploy/migrations/0107_account_lifecycle.down.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
DROP TRIGGER IF EXISTS account_settings_ttl_trigger ON public.account_settings;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_account_settings_ttl_trigger();
|
||||
DROP TRIGGER IF EXISTS users_account_delete_at_trigger ON public.users;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_users_account_delete_at_trigger();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_account_delete_at(timestamp with time zone, bigint, integer);
|
||||
|
||||
DROP TRIGGER IF EXISTS account_passwords_changed_at_trigger ON public.account_passwords;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_password_changed_at_trigger();
|
||||
ALTER TABLE public.account_passwords DROP COLUMN IF EXISTS password_changed_at;
|
||||
|
||||
ALTER TABLE public.account_settings
|
||||
DROP CONSTRAINT account_settings_account_ttl_days_check,
|
||||
ADD CONSTRAINT account_settings_account_ttl_days_check CHECK (account_ttl_days > 0);
|
||||
|
||||
DROP TABLE IF EXISTS public.account_deletion_notifications;
|
||||
DROP TABLE IF EXISTS public.account_deletion_requests;
|
||||
DROP INDEX IF EXISTS public.dialogs_user_peer_reverse_idx;
|
||||
|
||||
DROP INDEX IF EXISTS public.users_account_delete_due_idx;
|
||||
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS users_deletion_state_check;
|
||||
ALTER TABLE public.users
|
||||
DROP COLUMN IF EXISTS account_delete_at,
|
||||
DROP COLUMN IF EXISTS deletion_reason,
|
||||
DROP COLUMN IF EXISTS deletion_source,
|
||||
DROP COLUMN IF EXISTS deleted_at;
|
||||
216
deploy/migrations/0107_account_lifecycle.up.sql
Normal file
216
deploy/migrations/0107_account_lifecycle.up.sql
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
-- Unified account deletion lifecycle. A deleted account remains as a minimal
|
||||
-- user tombstone so historical messages keep a stable sender id, while all
|
||||
-- reusable identity and profile fields are released atomically.
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN deleted_at timestamp with time zone,
|
||||
ADD COLUMN deletion_source text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN deletion_reason text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN account_delete_at timestamp with time zone;
|
||||
|
||||
ALTER TABLE public.account_passwords
|
||||
ADD COLUMN password_changed_at timestamp with time zone;
|
||||
|
||||
ALTER TABLE public.account_settings
|
||||
DROP CONSTRAINT account_settings_account_ttl_days_check,
|
||||
ADD CONSTRAINT account_settings_account_ttl_days_check
|
||||
CHECK (account_ttl_days BETWEEN 1 AND 3650);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_password_changed_at_trigger()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
NEW.password_changed_at := CASE WHEN NEW.has_password THEN now() ELSE NULL END;
|
||||
ELSIF NEW.has_password IS DISTINCT FROM OLD.has_password
|
||||
OR NEW.srp_verifier IS DISTINCT FROM OLD.srp_verifier
|
||||
OR NEW.current_algo_salt1 IS DISTINCT FROM OLD.current_algo_salt1
|
||||
OR NEW.current_algo_salt2 IS DISTINCT FROM OLD.current_algo_salt2
|
||||
OR NEW.current_algo_g IS DISTINCT FROM OLD.current_algo_g
|
||||
OR NEW.current_algo_p IS DISTINCT FROM OLD.current_algo_p THEN
|
||||
NEW.password_changed_at := CASE WHEN NEW.has_password THEN now() ELSE NULL END;
|
||||
ELSE
|
||||
NEW.password_changed_at := OLD.password_changed_at;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
UPDATE public.account_passwords
|
||||
SET password_changed_at = updated_at
|
||||
WHERE has_password = true AND password_changed_at IS NULL;
|
||||
|
||||
CREATE TRIGGER account_passwords_changed_at_trigger
|
||||
BEFORE INSERT OR UPDATE ON public.account_passwords
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_password_changed_at_trigger();
|
||||
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_deletion_state_check CHECK (
|
||||
(deleted_at IS NULL AND deletion_source = '' AND deletion_reason = '')
|
||||
OR
|
||||
(deleted_at IS NOT NULL
|
||||
AND deletion_source IN (
|
||||
'manual',
|
||||
'forgot_password',
|
||||
'tos_decline',
|
||||
'password_reset_expiry',
|
||||
'account_ttl',
|
||||
'freeze_expiry'
|
||||
)
|
||||
AND account_delete_at IS NULL
|
||||
AND phone = ''
|
||||
AND first_name = ''
|
||||
AND last_name = ''
|
||||
AND username = ''
|
||||
AND country_code = ''
|
||||
AND about = ''
|
||||
AND verified = false
|
||||
AND support = false
|
||||
AND premium_expires_at IS NULL
|
||||
AND emoji_status_document_id = 0
|
||||
AND emoji_status_until = 0
|
||||
AND color_set = false
|
||||
AND color = 0
|
||||
AND color_background_emoji_id = 0
|
||||
AND profile_color_set = false
|
||||
AND profile_color = 0
|
||||
AND profile_color_background_emoji_id = 0
|
||||
AND birthday_day = 0
|
||||
AND birthday_month = 0
|
||||
AND birthday_year = 0
|
||||
AND personal_channel_id = 0
|
||||
AND last_seen_at = 0
|
||||
AND octet_length(deletion_reason) <= 1024)
|
||||
);
|
||||
|
||||
CREATE INDEX users_account_delete_due_idx
|
||||
ON public.users (account_delete_at, id)
|
||||
WHERE deleted_at IS NULL AND is_bot = false AND account_delete_at IS NOT NULL;
|
||||
|
||||
CREATE TABLE public.account_deletion_requests (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id bigint NOT NULL REFERENCES public.users(id),
|
||||
requester_auth_key_id bigint NOT NULL,
|
||||
state text DEFAULT 'pending' NOT NULL,
|
||||
reason text DEFAULT '' NOT NULL,
|
||||
confirm_hash_digest bytea NOT NULL,
|
||||
requested_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
execute_at timestamp with time zone NOT NULL,
|
||||
completed_at timestamp with time zone,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT account_deletion_requests_state_check CHECK (
|
||||
(state = 'pending' AND completed_at IS NULL)
|
||||
OR (state IN ('cancelled', 'executed') AND completed_at IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT account_deletion_requests_reason_check CHECK (octet_length(reason) <= 1024),
|
||||
CONSTRAINT account_deletion_requests_hash_check CHECK (octet_length(confirm_hash_digest) = 32)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX account_deletion_requests_one_pending_user_idx
|
||||
ON public.account_deletion_requests(user_id) WHERE state = 'pending';
|
||||
CREATE UNIQUE INDEX account_deletion_requests_pending_hash_idx
|
||||
ON public.account_deletion_requests(confirm_hash_digest) WHERE state = 'pending';
|
||||
CREATE INDEX account_deletion_requests_due_idx
|
||||
ON public.account_deletion_requests(execute_at, id) WHERE state = 'pending';
|
||||
|
||||
-- updateUser is not a pts-bearing update. Keep a dedicated durable online-nudge
|
||||
-- queue so a crash between tombstone commit and best-effort fan-out is recovered.
|
||||
-- Offline clients converge from authoritative dialog/history hydration instead
|
||||
-- of an immortal retry queue or invented user pts events.
|
||||
CREATE TABLE public.account_deletion_notifications (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
target_user_id bigint NOT NULL REFERENCES public.users(id),
|
||||
deleted_user_id bigint NOT NULL REFERENCES public.users(id),
|
||||
status text DEFAULT 'pending' NOT NULL,
|
||||
attempts integer DEFAULT 0 NOT NULL,
|
||||
next_attempt_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
lease_until timestamp with time zone,
|
||||
last_error text DEFAULT '' NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT account_deletion_notifications_status_check
|
||||
CHECK (status IN ('pending', 'dispatching', 'delivered')),
|
||||
CONSTRAINT account_deletion_notifications_attempts_check CHECK (attempts >= 0),
|
||||
CONSTRAINT account_deletion_notifications_not_self_check CHECK (target_user_id <> deleted_user_id),
|
||||
UNIQUE (target_user_id, deleted_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX account_deletion_notifications_ready_idx
|
||||
ON public.account_deletion_notifications(next_attempt_at, id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- Account deletion discovers peers that have an inbound dialog row pointing at
|
||||
-- the deleted user. The ordinary dialog PK only covers the owner direction;
|
||||
-- keep the reverse lookup indexed so tombstoning one account never scans every
|
||||
-- user's dialog table.
|
||||
CREATE INDEX dialogs_user_peer_reverse_idx
|
||||
ON public.dialogs(peer_id, user_id)
|
||||
WHERE peer_type = 'user';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_account_delete_at(
|
||||
p_created_at timestamp with time zone,
|
||||
p_last_seen_at bigint,
|
||||
p_ttl_days integer
|
||||
) RETURNS timestamp with time zone
|
||||
LANGUAGE sql IMMUTABLE AS $$
|
||||
SELECT GREATEST(
|
||||
p_created_at,
|
||||
CASE WHEN p_last_seen_at > 0 THEN to_timestamp(p_last_seen_at) ELSE p_created_at END
|
||||
) + make_interval(days => p_ttl_days)
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_users_account_delete_at_trigger()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_ttl_days integer;
|
||||
BEGIN
|
||||
IF NEW.deleted_at IS NOT NULL OR NEW.is_bot THEN
|
||||
NEW.account_delete_at := NULL;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
SELECT account_ttl_days INTO v_ttl_days
|
||||
FROM public.account_settings WHERE user_id = NEW.id;
|
||||
NEW.account_delete_at := public.telesrv_account_delete_at(
|
||||
NEW.created_at,
|
||||
NEW.last_seen_at,
|
||||
COALESCE(v_ttl_days, 365)
|
||||
);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER users_account_delete_at_trigger
|
||||
BEFORE INSERT OR UPDATE OF last_seen_at, deleted_at, is_bot
|
||||
ON public.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_users_account_delete_at_trigger();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_account_settings_ttl_trigger()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_user_id bigint;
|
||||
v_ttl_days integer;
|
||||
BEGIN
|
||||
v_user_id := COALESCE(NEW.user_id, OLD.user_id);
|
||||
v_ttl_days := CASE WHEN TG_OP = 'DELETE' THEN 365 ELSE NEW.account_ttl_days END;
|
||||
UPDATE public.users
|
||||
SET account_delete_at = public.telesrv_account_delete_at(created_at, last_seen_at, v_ttl_days),
|
||||
updated_at = now()
|
||||
WHERE id = v_user_id AND deleted_at IS NULL AND is_bot = false;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER account_settings_ttl_trigger
|
||||
AFTER INSERT OR UPDATE OF account_ttl_days OR DELETE
|
||||
ON public.account_settings
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_account_settings_ttl_trigger();
|
||||
|
||||
UPDATE public.users u
|
||||
SET account_delete_at = public.telesrv_account_delete_at(
|
||||
u.created_at,
|
||||
u.last_seen_at,
|
||||
COALESCE((
|
||||
SELECT s.account_ttl_days
|
||||
FROM public.account_settings s
|
||||
WHERE s.user_id = u.id
|
||||
), 365)
|
||||
)
|
||||
WHERE u.deleted_at IS NULL AND u.is_bot = false;
|
||||
Loading…
Add table
Add a link
Reference in a new issue