feat(auth): return 410 Gone (ACCOUNT_DELETED) for tombstoned users on the API surface #421
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-server!421
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/MAPPS-348-410-gone-account-deleted"
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?
PMS-591 stamps
users.deleted_atwhen Bunyip fires theaccount_deletedwebhook; the auth extractors already close down access via thedeleted_at IS NULLgate onget_user_by_idandfind_user_placement. What was missing was a distinct signal on the wire: every downstream request came back as a plain 401, indistinguishable from "your session expired, please refresh". The SPA (msp.a8n.systems) took that at face value, sat on its cached bearer, ran the token-refresh loop until it also failed, and in the meantime rendered the page-level "server unreachable" degradation with demo rows instead of telling the user their account was gone.Add a distinct 410 path so the SPA can catch the terminal state directly (MAPPS-348).
AuthService::is_user_tombstoned(sub) -> bool: probesSELECT (deleted_at IS NOT NULL) FROM users WHERE id = $1, unscoped by tenant so the auth middleware can call it before any tenant GUC is set. Returns true only when the row physically exists AND itsdeleted_atis stamped; a truly missing row and an active row both return false, preserving the pre-348 401 for those cases.AuthState(mokosh-types) with adeleted: boolfield,#[serde(default)]so any older consumer that still deserializes the shape stays backward-compatible. A newAuthState::deleted()factory sets it.is_user_tombstoned; on a positive result, upgrade the extension state from the plain default toAuthState::deleted(). Missing / malformed bearer skips the probe entirely.AppError::AccountDeleted: 410 Gone, error codeACCOUNT_DELETED, message"Account has been deleted.". Distinct from the existingGone(String)so the SPA can pattern-match the code, not the message.RequireAuth,TenantScope,RequireRole,RequireModuleEnabled) through a shareduser_or_auth_errorhelper that returnsAccountDeletedwhen the deleted flag is set and falls through to the pre-348Unauthorizedotherwise. Ordering matters: deleted-first ensures the SPA never sees a spurious 401 for a tombstoned bearer.Tests:
src/modules/auth/middleware.rs: pin the three arms ofuser_or_auth_error(deleted -> AccountDeleted, empty -> Unauthorized, authenticated -> user).#[sqlx::test]intests/auth.rs: seed admin, log in,UPDATE users SET deleted_at = NOW(), hit/api/v1/auth/meon the same bearer, assert410 Gonewith body{error:{code:"ACCOUNT_DELETED"}}. Reproduces the exact production shape PMS-591 leaves the row in.SPA-side companion (mokosh-apps:
Signal<TerminalAuthError>catches 410 in the shared fetch layer,AppLayoutrenders a terminal modal, 5s countdown, force-redirect to Bunyip logout) ships in a separate PR againstmokosh-appsmain. Rollout order is server-first: the SPA reads the 410 code, but if it lands on staging before the SPA update, older SPAs still get the same "signed out" behaviour they would have hit via the 401 path (just without the polished modal), so no regression.#MAPPS-348
b21a1a15dc05fdfc7170