You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ce2bfde
Browse filesBrowse the repository at this point in the historyBrowse files
fix(hocuspocus): re-check ghost accounts before delete and clear soft-deleted profiles
The ghost-account delete no longer trusts the cached list. Each id is checked again before the delete. A user who signed in, an admin, or an unknown id is refused. The single route answers 409, and the bulk route counts the id as failed. A non-uuid id answers the house 400. A failed impact read or admin read now deletes nothing. Before, a failed impact read could fall through to a hard delete.
Revoking admin access is now one locked call to public.admin_revoke_admin. Two admins who revoke each other at the same moment cannot leave zero admins. The migration pairs with its script, and seed.sql and the webapp types are regenerated. Push the migration to production by hand before this server ships.
A soft delete now clears the public profile. It clears the name, photo, bio and links, sets a deleted_ username, removes the user's avatar objects, and bans the user. Messages keep their author.
Refs #412
Refs #427
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| GET |`/audit/ghost-accounts`| List ghost accounts |
1300
+
| GET |`/audit/ghost-accounts/summary`| Category summary |
1301
+
| GET |`/audit/ghost-accounts/:id/impact`| FK dependency check |
1302
+
| DELETE |`/audit/ghost-accounts/:id`| Smart-delete one |
1303
+
| POST |`/audit/ghost-accounts/bulk-delete`| Bulk delete (max 50) |
1304
+
| POST |`/audit/ghost-accounts/resend-confirmation`| Resend magic link |
1305
+
| POST |`/audit/ghost-accounts/cleanup-anonymous`| Clean stale anon sessions |
1306
+
| Both ghost-account delete routes check each id again before they delete. They refuse a user who signed in after the list loaded, an admin, and an unknown id. `DELETE /audit/ghost-accounts/:id` then answers `409` with `{ error }`. A non-uuid id answers `400``VALIDATION_ERROR`. The bulk route counts a refused id under `failed` and adds a line to `errors`. A failed impact read or admin read also stops the delete. The single route answers `500`, and the bulk route counts the id as failed. A soft delete bans the user and clears the public profile: name, photo, bio and links. It also removes the `user_avatars` objects and sets the username to `deleted_` plus 12 hex characters of the id. |
Copy file name to clipboardExpand all lines: apps/hocuspocus.server/CLAUDE.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -223,6 +223,8 @@ Moved verbatim out of the repo-root [AGENTS.md](../../AGENTS.md). The `### Supab
223
223
### Admin API And Dashboard
224
224
225
225
-**Any admin-only data path goes through an `is_admin()`-gated `SECURITY DEFINER` RPC or the `service_role` hocuspocus REST API — never a direct anon-key table read.** The browser client is RLS-scoped `authenticated`. Several admin tables/columns are revoked from that role (`users.email`, `push_subscriptions`, `email_queue`) or scoped to the caller's own rows. Direct reads therefore return wrong, zeroed, or `42501` results. Aggregations must paginate past the 1000-row cap.
226
+
-**The ghost-account delete trusts no cached list (#412).**`assertDeletableGhost` (`src/api/services/adminGhostAccounts.service.ts`) is the one gate, and the single and bulk paths call it for each id. It re-reads the auth user and refuses a user who is no longer a ghost, an admin, or an unknown id. A hard delete cascades to `admin_users`, so it would bypass the last-admin guard. A failed impact read or admin read deletes nothing. Both paths lowercase the id first, because Prisma compares `ownerId` as text. Revoking admin goes only through `public.admin_revoke_admin`. Never count and delete in two calls, because two admins could then remove each other.
227
+
-**Every user soft delete goes through `softDeleteUsers` (#427).** The `users` read policies are `USING (true)`, so `deleted_at` alone leaves the name, photo, bio and links public. The helper clears them and sets a `deleted_<12 hex>` username. It then removes the `user_avatars` objects and bans the user. A new deletion path must call the helper, never write `deleted_at` alone. Messages keep their author, because the row stays.
0 commit comments