initial, copy postgres stuff for migrations, main.go test

This commit is contained in:
2026-09-04 21:11:41 +02:00
parent 506df87f84
commit 090cc0f00d
14 changed files with 863 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
-- 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;