Files
pad/internal/store
xarmian 366d4fb7e5 fix(store): harden account-deletion FK cascade (TASK-1959) (#821)
* fix(store): harden account-deletion FK cascade (TASK-1959)

DeleteAccountAtomic could 500 with nothing deleted when a user had rows
referencing them via foreign keys with no ON DELETE action — notably
activities.user_id (the audit/history log, including the session_ip_changed
rows the auth middleware writes on the very request that deletes the
account). The delete-account tests only passed by working around this
(pinning RemoteAddr to loopback, scrubbing activities.user_id).

Audit every table with a FK to users(id) and handle each in the delete
transaction:

  - de-identify (UPDATE ... SET NULL) audit/history rows: items
    created/modified, comments, comment_reactions, item_links,
    item_versions, share_link_views
  - delete owned/transient/audit rows: sessions, api_tokens,
    workspace_members, sent invitations, password/email tokens, issued
    grants, created share links, mcp_audit_log, oauth_connections
  - rely on existing ON DELETE CASCADE / SET NULL for item_stars,
    user_report_layouts, {collection,item}_grants.user_id,
    items.assigned_user_id

Migrations 072 (SQLite) / 050 (Postgres) give activities.user_id an
ON DELETE SET NULL FK so the highest-write-frequency audit table can't
block a delete via a row written concurrently during the request. SQLite
rebuilds the table (022_audit_trail pattern); Postgres never had the FK,
so it is added after an orphan scrub so validation passes.

Remove the test work-arounds now that the cascade holds: deleteAccountReq
deletes from a changed IP so the session-IP-change audit row exercises the
fix, and the partial-delete test injects its post-cancel failure via the
sidecar hook instead of the (now-fixed) FK gap. Add a store-level test that
deletes a fully-populated user atomically.

Closes TASK-1959

Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST

* fix(store): harden account-deletion FK cascade (TASK-1959)

DeleteAccountAtomic could 500 with nothing deleted when a user had rows
referencing them via foreign keys with no ON DELETE action — notably
activities.user_id (the audit/history log, including the session_ip_changed
rows the auth middleware writes on the very request that deletes the
account). The delete-account tests only passed by working around this
(pinning RemoteAddr to loopback, scrubbing activities.user_id).

Audit every table with a FK to users(id) and handle each in the delete
transaction:

  - de-identify (UPDATE ... SET NULL) audit/history rows: items
    created/modified, comments, comment_reactions, item_links,
    item_versions, share_link_views
  - delete owned/transient/audit rows: sessions, api_tokens,
    workspace_members, sent invitations, password/email tokens, issued
    grants, created share links, mcp_audit_log, oauth_connections
  - rely on existing ON DELETE CASCADE / SET NULL for item_stars,
    user_report_layouts, {collection,item}_grants.user_id,
    items.assigned_user_id

Migrations 072 (SQLite) / 050 (Postgres) give activities.user_id an
ON DELETE SET NULL FK so the highest-write-frequency audit table can't
block a delete via a row written concurrently during the request. SQLite
rebuilds the table (022_audit_trail pattern); Postgres never had the FK,
so it is added after an orphan scrub so validation passes.

Log the account_deleted audit row with an empty user_id (deleted id kept
in metadata): the user row is already gone by then, and the new
activities.user_id FK would otherwise reject the insert and silently drop
the row. This makes the account_deleted event actually recorded on both
dialects.

Remove the test work-arounds now that the cascade holds: deleteAccountReq
deletes from a changed IP so the session-IP-change audit row exercises the
fix, and the partial-delete test injects its post-cancel failure via the
sidecar hook instead of the (now-fixed) FK gap. Add a store-level test that
deletes a fully-populated user atomically.

Closes TASK-1959

Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
2026-07-05 19:23:02 -04:00
..