216 lines
8.2 KiB
PL/PgSQL
216 lines
8.2 KiB
PL/PgSQL
-- 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;
|