User Management ~10 min
Overview
Users belong to an organization. All user operations are scoped to the requesting user's organization — unless the caller has the system-admin role (organization = NULL), which bypasses isolation entirely.
Create a user
POST /api/users
Authorization: Bearer <shell-jwt> (requires users:write)
Content-Type: application/json
{
"email": "jane@acme.com",
"username": "jane",
"firstName": "Jane",
"lastName": "Doe",
"status": "ACTIVE"
}Response
{
"data": {
"id": "uuid",
"email": "jane@acme.com",
"username": "jane",
"firstName": "Jane",
"lastName": "Doe",
"status": "ACTIVE",
"organizationId": "org-uuid",
"createdAt": "2026-01-01T00:00:00Z"
}
}Organization auto-assigned
The new user is placed in the requesting user's organization automatically.
List users
GET /api/users?page=1&pageSize=20&status=ACTIVE&search=jane
Authorization: Bearer <shell-jwt> (requires users:list)| Query param | Default | Description |
|---|---|---|
page | 1 | Page number |
pageSize | 20 | Results per page |
status | — | Filter: ACTIVE, INACTIVE, SUSPENDED |
search | — | Search by name, username, or email |
Get a user
GET /api/users/:id
Authorization: Bearer <shell-jwt> (requires users:read)Returns user detail including assigned roles.
Update a user
PUT /api/users/:id
Authorization: Bearer <shell-jwt> (requires users:write)
Content-Type: application/json
{
"firstName": "Jane",
"lastName": "Smith",
"status": "ACTIVE"
}| Field | Type | Description |
|---|---|---|
firstName | string | |
lastName | string | |
status | string | ACTIVE | INACTIVE | SUSPENDED |
Delete a user
DELETE /api/users/:id
Authorization: Bearer <shell-jwt> (requires users:delete)Cannot delete yourself
The API rejects requests where id matches the requesting user.
Role assignment
Assign roles
POST /api/users/:id/roles
Authorization: Bearer <shell-jwt> (requires users:write)
Content-Type: application/json
{ "roleIds": ["role-uuid-1", "role-uuid-2"] }This replaces all current role assignments for the user.
Remove a single role
DELETE /api/users/:id/roles/:roleId
Authorization: Bearer <shell-jwt> (requires users:write)Get user's roles
GET /api/users/:id/roles
Authorization: Bearer <shell-jwt> (requires users:read)Password management
Change password (authenticated user)
POST /api/auth/change-password
Authorization: Bearer <shell-jwt>
Content-Type: application/json
{
"currentPassword": "OldPass123",
"newPassword": "NewPass456!"
}Password rules:
- 8–128 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- Last 10 passwords cannot be reused
Admin password reset flow
Step 1 — Request reset token (sent to user's email):
POST /api/auth/request-password-reset
Content-Type: application/json
{ "email": "jane@acme.com" }Token expires in 1 hour.
Step 2 — Confirm reset with token:
POST /api/auth/confirm-password-reset
Content-Type: application/json
{
"token": "<reset-token>",
"newPassword": "NewSecurePass1"
}Account locking
After 5 consecutive failed logins, the account is locked for 30 minutes.
| Event | Behaviour |
|---|---|
| Failed login | failedLoginCount++ |
| 5th failure | Account locked for 30 min |
| Successful login | failedLoginCount reset to 0 |
| Lock expires | Login allowed again automatically |
User statuses
| Status | Meaning |
|---|---|
ACTIVE | Can log in |
INACTIVE | Soft-disabled, cannot log in |
SUSPENDED | Blocked, typically policy violation |
Organization isolation
All user queries automatically filter by the requesting user's organizationId from the JWT.
Exception: users with the system-admin role (identified by organizationId = NULL in the JWT) can see and manage users across all organizations.
Checking admin status
// UserService — used internally
await userService.isSystemAdmin(userId); // system-admin role + null org
await userService.isAdmin(userId); // admin role (any org)