Files
collective-lists/supabase/migrations/019_user_deletion_fks.sql
Oier Bravo Urtasun 92ad29696d feat(fase-10): account hard-delete — FK relax + RPC (10.5)
Migration 019 normalizes every \`created_by\` / \`updated_by\` FK from
\`NOT NULL REFERENCES users(id) ON DELETE RESTRICT\` to
\`NULL REFERENCES users(id) ON DELETE SET NULL\`. Affected tables:
collectives, collective_invitations, shopping_lists, shopping_items,
task_lists, tasks, notes (created_by + updated_by). Without this,
delete_account() would be blocked the first time the user creates any
content. Spec §6.3: content survives the user; the row is orphaned with
the FK set to NULL (UI shows "Deleted user" — to be polished later).

Migration 021 adds delete_account():
- two-step delete: public.users (fires the existing promote-oldest-
  admin trigger from migration 002, then CASCADEs collective_members
  and SET-NULLs content FKs), then auth.users (CASCADEs sync_conflicts
  + drops the GoTrue identity row + token rows)
- guard: errcode P0003 when the caller is the sole admin of a
  collective where every other member is a guest (nobody promotable);
  the user must promote someone manually first
- Keycloak is NOT touched (documented limitation in the UI body); the
  user can re-register with the same email and will receive a fresh
  UUID-distinct profile

Settings UI (already shipped with 10.1) wires the modal: explicit body
listing what is and isn't deleted; confirm button only enables when the
user types DELETE exactly. Post-success calls logout() so the Keycloak
SSO cookie is ended too.

pgTAP 013 (8 assertions): RPC exists, regular user delete + cascade +
content-orphan, sole-admin-with-promotable promotes, sole-admin-with-
only-guest blocked with P0003. Playwright DEL-01 covers the
type-the-word UI guard; full "delete + re-login fails" is left to
pgTAP because seeding ephemeral Keycloak accounts in E2E would
contaminate every downstream suite.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 02:21:25 +02:00

90 lines
4.6 KiB
SQL

-- Migration 019: relax user FKs so account deletion (Fase 10.5) works.
--
-- Spec (§6.3): deleting a user account must NOT delete the content they
-- created in collectives. Several `created_by`/`updated_by` FKs were
-- declared `NOT NULL REFERENCES public.users(id) ON DELETE RESTRICT`,
-- which would block delete_account() whenever the user has ever created
-- a list, task, note, etc.
--
-- This migration:
-- * drops the offending RESTRICT FKs, drops the NOT NULL, re-adds them
-- as `ON DELETE SET NULL`
-- * relaxes `collectives.created_by` likewise (the orphaned collective
-- becomes "created by deleted user")
-- * relaxes `collective_invitations.created_by` from CASCADE → SET NULL
-- so existing invitations survive the creator's account deletion
--
-- Reads of these columns in the UI should defensively show
-- "Deleted user" when null.
-- ── public.collectives ───────────────────────────────────────────────────────
ALTER TABLE public.collectives
DROP CONSTRAINT collectives_created_by_fkey;
ALTER TABLE public.collectives
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.collectives
ADD CONSTRAINT collectives_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
-- ── public.collective_invitations ────────────────────────────────────────────
ALTER TABLE public.collective_invitations
DROP CONSTRAINT collective_invitations_created_by_fkey;
ALTER TABLE public.collective_invitations
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.collective_invitations
ADD CONSTRAINT collective_invitations_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
-- ── public.shopping_lists ────────────────────────────────────────────────────
ALTER TABLE public.shopping_lists
DROP CONSTRAINT shopping_lists_created_by_fkey;
ALTER TABLE public.shopping_lists
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.shopping_lists
ADD CONSTRAINT shopping_lists_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
-- ── public.shopping_items ────────────────────────────────────────────────────
ALTER TABLE public.shopping_items
DROP CONSTRAINT shopping_items_created_by_fkey;
ALTER TABLE public.shopping_items
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.shopping_items
ADD CONSTRAINT shopping_items_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
-- ── public.task_lists ────────────────────────────────────────────────────────
ALTER TABLE public.task_lists
DROP CONSTRAINT task_lists_created_by_fkey;
ALTER TABLE public.task_lists
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.task_lists
ADD CONSTRAINT task_lists_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
-- ── public.tasks ─────────────────────────────────────────────────────────────
ALTER TABLE public.tasks
DROP CONSTRAINT tasks_created_by_fkey;
ALTER TABLE public.tasks
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.tasks
ADD CONSTRAINT tasks_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
-- ── public.notes ─────────────────────────────────────────────────────────────
ALTER TABLE public.notes
DROP CONSTRAINT notes_created_by_fkey;
ALTER TABLE public.notes
ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE public.notes
ADD CONSTRAINT notes_created_by_fkey
FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL;
ALTER TABLE public.notes
DROP CONSTRAINT notes_updated_by_fkey;
ALTER TABLE public.notes
ALTER COLUMN updated_by DROP NOT NULL;
ALTER TABLE public.notes
ADD CONSTRAINT notes_updated_by_fkey
FOREIGN KEY (updated_by) REFERENCES public.users(id) ON DELETE SET NULL;