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>
This commit is contained in:
89
supabase/migrations/019_user_deletion_fks.sql
Normal file
89
supabase/migrations/019_user_deletion_fks.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user