103 lines
3.5 KiB
PL/PgSQL
103 lines
3.5 KiB
PL/PgSQL
-- Deletion paths.
|
|
--
|
|
-- Discord's developer policy requires deleting user data on request, and API
|
|
-- registrants are an international user base whose data protection expectations
|
|
-- apply regardless. Build this before you need it: retrofitting a deletion path
|
|
-- across three stores is miserable, and being asked for one you do not have is
|
|
-- worse.
|
|
--
|
|
-- These functions cover Postgres. The caller must ALSO clear Redis - cached
|
|
-- callsign lookups, rate-limit counters keyed by hashed Discord ID, and any
|
|
-- verification challenge in flight. Postgres is not the whole story.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Removes a Discord user and everything referencing them. The ON DELETE CASCADE
|
|
-- chain reaches verifications, subscriptions, imports and qsos, so this is one
|
|
-- statement rather than a list that can fall out of date as tables are added.
|
|
--
|
|
-- Returns the number of QSOs removed, purely so the caller can tell the user
|
|
-- what was deleted.
|
|
CREATE FUNCTION core.forget_user(p_discord_id bigint)
|
|
RETURNS TABLE (deleted_user boolean, deleted_qsos bigint)
|
|
LANGUAGE plpgsql
|
|
-- Pinned: the body qualifies its tables, but now(), count() and friends resolve
|
|
-- through search_path. Matters more if this ever becomes SECURITY DEFINER.
|
|
SET search_path = pg_catalog, core, logbook
|
|
AS $$
|
|
DECLARE
|
|
v_user_id bigint;
|
|
v_qsos bigint := 0;
|
|
BEGIN
|
|
SELECT id INTO v_user_id FROM core.users WHERE discord_id = p_discord_id;
|
|
|
|
IF v_user_id IS NULL THEN
|
|
RETURN QUERY SELECT false, 0::bigint;
|
|
RETURN;
|
|
END IF;
|
|
|
|
SELECT count(*) INTO v_qsos FROM logbook.qsos WHERE user_id = v_user_id;
|
|
|
|
-- Cascades do the rest.
|
|
DELETE FROM core.users WHERE id = v_user_id;
|
|
|
|
RETURN QUERY SELECT true, v_qsos;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION core.forget_user(bigint) IS 'Hard-deletes a Discord user and all dependent rows. Caller must also purge Redis keys for this user.';
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- The API-side equivalent. Keys and usage rows cascade from the owner.
|
|
CREATE FUNCTION api.forget_owner(p_email core.email)
|
|
RETURNS TABLE (deleted_owner boolean, deleted_keys bigint)
|
|
LANGUAGE plpgsql
|
|
SET search_path = pg_catalog, core, api
|
|
AS $$
|
|
DECLARE
|
|
v_owner_id bigint;
|
|
v_keys bigint := 0;
|
|
BEGIN
|
|
SELECT id INTO v_owner_id FROM api.owners WHERE email = p_email;
|
|
|
|
IF v_owner_id IS NULL THEN
|
|
RETURN QUERY SELECT false, 0::bigint;
|
|
RETURN;
|
|
END IF;
|
|
|
|
SELECT count(*) INTO v_keys FROM api.keys WHERE owner_id = v_owner_id;
|
|
|
|
DELETE FROM api.owners WHERE id = v_owner_id;
|
|
|
|
RETURN QUERY SELECT true, v_keys;
|
|
END;
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Retention. Verification records carry a hashed challenge and a timestamp, and
|
|
-- are useful for a short while when investigating a failed verification. They
|
|
-- are not useful a year later. Run this from cron.
|
|
CREATE FUNCTION core.prune_verifications(p_keep_days integer DEFAULT 90)
|
|
RETURNS bigint
|
|
LANGUAGE plpgsql
|
|
SET search_path = pg_catalog, core
|
|
AS $$
|
|
DECLARE
|
|
v_removed bigint;
|
|
BEGIN
|
|
DELETE FROM core.verifications
|
|
WHERE created_at < now() - make_interval(days => p_keep_days)
|
|
AND status <> 'succeeded';
|
|
|
|
GET DIAGNOSTICS v_removed = ROW_COUNT;
|
|
|
|
RETURN v_removed;
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION core.forget_user(bigint) TO hammy_app;
|
|
GRANT EXECUTE ON FUNCTION api.forget_owner(core.email) TO hammy_app;
|
|
GRANT EXECUTE ON FUNCTION core.prune_verifications(integer) TO hammy_app;
|