Add self-service account deletion, DELETE /api/users/me #60
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/account-deletion-endpoint"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements task 122 under ADR-0011 / ADR-0019.
DELETE /api/users/me, re-confirmed by current password (IPasswordHasher.Verify). An account with no local password (a seeded one today, an OIDC-only one from task 121) can never satisfy this — see the remarks onUserEndpoints.DeleteMe. Left open deliberately; task 121 should close it.AccountDeletionRepository.DeleteAsyncruns ADR-0019's refuse/delete-groups/delete-memberships-and-user sequence in one transaction, withFOR UPDATErow locks taken up front to close the write-skew racedocs/data-model.mdnames (two co-Owners of one group deleting concurrently).fk_refresh_tokens_users_user_idcascades). The already-issued access token stays valid for its remaining 15 minutes exactly as ADR-0032 accepts for logout — this endpoint additionally resolvessubto ausersrow and refuses (404) when absent, so a second call with the same stale token can't attempt a no-op deletion.AccountDeletionAtomicityTestsforces the finalDELETE FROM usersto fail via a trigger and asserts every earlier statement in the same call (group deletion, membership deletion) rolls back with it.No schema change and no new ADR — the FK delete behaviours and the locking strategy are already fixed by ADR-0019 and
docs/data-model.md's "Account deletion" section; this is that design, built.Verdict: mergeable
Reviewed against task 122, ADR-0005/0011/0018/0019/0028/0032/0047/0054 and
docs/data-model.md. Full solution build and test suite pass under the pinned SDK (10.0.100), 851/851 tests green.Verified by mutation, not just reading:
FOR UPDATE OF gmfrom the lock query — the co-Owner concurrent-delete test reddens deterministically (20/20 runs), with a genuine assertion failure (groupGone || acceptedOwnerCount >= 1false), not a flaky pass. The lock is real.true— two tests immediately catch the resulting cross-member data loss (AnEditorOfAGroupSharedWithAnotherOwner,ASecondAcceptedOwnerAlreadyPromoted). The "don't touch another member's group" boundary is genuinely covered, not just asserted.Confirmed by reading:
group_memberships.user_idis stillRESTRICTin0001-initial-schema.sql, unweakened; pending-Owner is correctly excluded from "another accepted Owner" in the refusal predicate (DeleteAsync_TheSoleAcceptedOwnerWithOnlyAPendingInvitee_Refuses); the promote-then-demote-then-delete escape route is traced end to end at the HTTP layer; the atomicity test forces the trigger failure after both the group deletion and the membership deletion have run, and asserts full rollback; the "no local password" gap is accurate as described — no OIDC endpoint exists yet in this codebase, and a seeded account can't sign in to reach this route at all, so it's correctly unreachable today.Nothing to act on.