B Blengi docs

Platform admin

Users (incl. delete)

/admin/users lists every user account on the platform — super-admins, customers, and members invited into any workspace. The page supports search by name/email, role promotion, BYOK per-user overrides, edit, impersonate, and (since v1.1) delete.

Who can open /admin

Only super-admins. Everyone else gets a plain 404 Not Found, signed in or not, so the area is not advertised to customers (EnsureSuperAdmin). A super-admin who is impersonating a customer still gets through. If the platform owner sees that 404, the browser is signed into another account: a test customer account created a moment ago sees it too. Sign out and sign back in with the super-admin account, or keep test accounts in a private window.

Sign-up, the email-verification link, login and the two-factor step send a user back to the page they opened before signing in. When that page is under /admin and the user is not a super-admin, it is dropped and they land on the dashboard instead (#720, DropsAdminIntendedUrl). Before that, a customer who had opened /admin/users while signed out landed on the 404 right after confirming their email. Super-admins still return to the admin page they asked for, and every other remembered page, such as an invitation link, is honoured as before.

Delete

Super-admin only. Click the Delete button on any user row (the button is hidden on your own row). A confirm dialog spells out the consequence — they lose access to every workspace and the row is soft-deleted for audit.

Safety rails

The controller refuses four cases:

  • Self-delete — you cannot delete the account you are signed into. Would lock you out of admin.
  • Last super-admin — defence-in-depth check; the controller refuses if the target is a super-admin and no other live super-admin exists.
  • Workspace owner — if the user still owns one or more workspaces, the delete returns an error toast naming the count (its text says "Transfer ownership first"). There is no ownership transfer anywhere in the app: neither /admin/workspaces/{id} nor the workspace's own Team page can change the owner. The only way through is to delete those workspaces first (from /admin/workspaces) and retry.
  • Non-super-admin actor — the route is in the platform-admin group; a customer hitting DELETE /admin/users/{id} directly returns 404 to hide the endpoint's existence.

What soft-delete touches

  • users.deleted_at is set to the current timestamp. Subsequent reads via Eloquent's default scope exclude the row.
  • The address is released: the row's email becomes <email>#deleted-<id> (User::releasedEmail(), fired by the model's soft-delete event, so single and bulk delete both do it). The same address can sign up again, or be given to another account, right away. The # after the domain makes the marker an invalid address, so nobody can sign up with the marker itself. Before #719 the row kept the bare address: sign-up answered "The email has already been taken", the UNIQUE index would have refused a second row anyway, and login ignores deleted accounts, so the address was locked for good. The migration 2026_10_06_170000_release_emails_of_deleted_users released the rows deleted before that.
  • All workspace_users pivot rows for the user are deleted so a re-invited account doesn't collide on the unique composite key.
  • Foreign-key references in conversations, leads, audit_logs, and other tables with a user_id column are left intact — soft-delete preserves the row, so those joins keep working.
  • An AuditLog entry with action=platform_user.deleted is written under the acting admin's default workspace, including the deleted email (as it was, without the marker) + role for forensic recovery.

Customer-initiated deletion (different code path)

When a customer deletes their own account from /settings/profile, the row is hard-deleted via forceDelete() — a customer-initiated deletion is treated as a GDPR right-to-erasure request, so the data goes away for real. Only the platform-admin delete is soft.

Restoring a soft-deleted user

There is no admin UI for restore yet. A deleted account carries the marker, so look it up by it and put the address back after restoring:

php artisan tinker --execute '$u = App\Models\User::withTrashed()->where("email", "like", "x@y.com#deleted-%")->first(); $u->restore(); $u->forceFill(["email" => "x@y.com"])->save();'

If someone has signed up with that address since, the last step fails on the UNIQUE index: the address belongs to the new account now.

The user's workspace memberships are NOT restored — they need a fresh invitation. If the original workspace is gone too, default_workspace_id will be stale and the next visit redirects them through the standard onboarding flow.