Managing users
Cross-tenant user lifecycle — suspend, force logout, force password reset, soft-delete, impersonate.
/admin/users lists every user on the platform. Click a row to open the user detail page where the lifecycle actions live.
Actions
All mutations are gated by authMiddleware → requirePlatformAdmin → writeLimiter and emit a platform.user.* audit row with the actor's real userId.
| Action | Route | Audit | Notes |
|---|---|---|---|
| Suspend | POST /api/v1/platform/users/{id}/suspend | platform.user.suspended | Sets isActive=false + revokes all sessions. Refuses on the last platform admin. |
| Reactivate | POST /api/v1/platform/users/{id}/reactivate | platform.user.reactivated | Refuses if deletedAt is set. |
| Force logout | POST /api/v1/platform/users/{id}/force-logout | platform.user.force_logout | Pure session revocation, returns { revoked } count. |
| Force password reset | POST /api/v1/platform/users/{id}/force-password-reset | platform.user.force_password_reset | Sends reset email + revokes sessions. |
| Soft-delete | DELETE /api/v1/platform/users/{id} | platform.user.soft_deleted | Refuses self + last-admin. Idempotent. |
| Resend verification | POST /api/v1/platform/users/{id}/resend-verification | platform.user.resend_verification | Wraps the existing verification flow. |
Impersonation
From /admin/users/[id], click Impersonate to start a time-bounded session masquerading as another user. The mutation hits POST /api/v1/platform/impersonate/{userId}; the response sets a signed cookie that flips req.session.userId to the target while preserving req.session.realUserId (your actual id).
[!IMPORTANT] Every audit row emitted while impersonating uses
realUserId— there is no way to "audit-launder" actions through someone else's identity. TherequirePlatformAdminand the suspended-user gates also userealUserIdso an impersonated regular user does not gain admin powers.
End impersonation from the persistent banner at the top of the dashboard. The banner re-renders on every authenticated page; all routes redirect through impersonationHydration which validates the cookie before resolving req.session.
Soft-delete semantics
Soft-deleted users have deletedAt set; their email becomes free for re-registration. They do not appear in GET /api/v1/platform/users unless the caller passes includeDeleted=true. The UI does not surface a reactivate path for them — this prevents accidentally restoring a stale account; operators who need to undo a soft-delete should clear deletedAt directly. Soft-deleted users are excluded from the countActivePlatformAdmins() invariant, so deleting the last admin is correctly refused.