Răsfoiți Sursa

feat(api): document user management endpoints and authorization requirements

codeUWE 1 lună în urmă
părinte
comite
0e8022d9c2
4 a modificat fișierele cu 409 adăugiri și 37 ștergeri
  1. 188 3
      Docs/api.md
  2. 11 4
      Docs/auth.md
  3. 127 5
      Docs/frontend-api-usage.md
  4. 83 25
      Docs/frontend-ui.md

+ 188 - 3
Docs/api.md

@@ -110,7 +110,7 @@ Response semantics:
 - **401 Unauthorized**: no valid session
 - **403 Forbidden**: session exists but branch access is not allowed
 
-### 2.3 User management authorization (RHL-041 prerequisite for RHL-012)
+### 2.3 User management authorization (RHL-041 + RHL-012)
 
 User management is a separate capability from branch access.
 
@@ -134,8 +134,8 @@ When an endpoint is guarded by this capability, it must return:
 
 Notes:
 
-- RHL-041 does **not** introduce user management endpoints.
-- RHL-012 will add the actual user management endpoints and must enforce this capability consistently.
+- **RHL-041** introduced the capability separation (branch access vs user management).
+- **RHL-012** implements the actual user management endpoints under `/api/admin/users` and enforces this capability consistently.
 
 ---
 
@@ -200,6 +200,7 @@ The API uses these machine-readable codes (non-exhaustive list):
   - `VALIDATION_INVALID_JSON`
   - `VALIDATION_INVALID_BODY`
   - `VALIDATION_MISSING_FIELD`
+  - `VALIDATION_INVALID_FIELD`
 
 - Validation (password management):
   - `VALIDATION_WEAK_PASSWORD`
@@ -223,6 +224,9 @@ The API uses these machine-readable codes (non-exhaustive list):
   - `VALIDATION_SEARCH_CURSOR`
   - `VALIDATION_SEARCH_MISSING_FILTER`
 
+- User management (RHL-012):
+  - `USER_NOT_FOUND`
+
 - Storage:
   - `FS_NOT_FOUND`
   - `FS_STORAGE_ERROR`
@@ -481,6 +485,184 @@ Filter rule:
 
 ---
 
+### 4.13 `GET /api/admin/users` (RHL-012)
+
+List user accounts (cursor-based pagination).
+
+**Authentication**: required.
+
+**Authorization**: user management capability required (`superadmin` / `dev`).
+
+**Query params (optional)**
+
+- `q` — free-text filter (matches username/email)
+- `role` — one of `branch | admin | superadmin | dev`
+- `branchId` — `NLxx` (filters branch users)
+- `limit` — page size (default `50`, min `1`, max `200`)
+- `cursor` — opaque cursor from the previous response
+
+**Response 200**
+
+```json
+{
+	"items": [
+		{
+			"id": "...",
+			"username": "...",
+			"email": "...",
+			"role": "branch",
+			"branchId": "NL01",
+			"mustChangePassword": true,
+			"createdAt": "2026-02-01T10:00:00.000Z",
+			"updatedAt": "2026-02-02T10:00:00.000Z"
+		}
+	],
+	"nextCursor": "..."
+}
+```
+
+Notes:
+
+- `nextCursor` is `null` when there are no more results.
+- All returned user objects are safe (no password hash/token fields).
+
+---
+
+### 4.14 `POST /api/admin/users` (RHL-012)
+
+Create a new user.
+
+**Authentication**: required.
+
+**Authorization**: user management capability required (`superadmin` / `dev`).
+
+**Request body (JSON)**
+
+```json
+{
+	"username": "branchuser",
+	"email": "nl01@example.com",
+	"role": "branch",
+	"branchId": "NL01",
+	"initialPassword": "StrongPassword123"
+}
+```
+
+Rules:
+
+- `branchId` is required **only** when `role === "branch"`.
+- `initialPassword` is validated using the password policy.
+- The created user is initialized with `mustChangePassword = true`.
+
+**Response 200**
+
+```json
+{ "ok": true, "user": { "id": "...", "username": "...", "email": "..." } }
+```
+
+Duplicate handling (common):
+
+- Username exists:
+
+  ```json
+  {
+  	"error": {
+  		"message": "Username already exists",
+  		"code": "VALIDATION_INVALID_FIELD",
+  		"details": { "field": "username" }
+  	}
+  }
+  ```
+
+- Email exists:
+
+  ```json
+  {
+  	"error": {
+  		"message": "Email already exists",
+  		"code": "VALIDATION_INVALID_FIELD",
+  		"details": { "field": "email" }
+  	}
+  }
+  ```
+
+- Username and email exist:
+
+  ```json
+  {
+  	"error": {
+  		"message": "Username and email already exist",
+  		"code": "VALIDATION_INVALID_FIELD",
+  		"details": { "fields": ["username", "email"] }
+  	}
+  }
+  ```
+
+---
+
+### 4.15 `PATCH /api/admin/users/:userId` (RHL-012)
+
+Update a user.
+
+**Authentication**: required.
+
+**Authorization**: user management capability required (`superadmin` / `dev`).
+
+**Route params**
+
+- `userId` — MongoDB ObjectId
+
+**Request body (JSON)**
+
+Supports patching (any subset):
+
+- `username`
+- `email`
+- `role`
+- `branchId` (nullable)
+- `mustChangePassword` (boolean)
+
+Role/branch consistency rule:
+
+- If the resulting role is `branch`, a valid `branchId` is required.
+- For non-branch roles, `branchId` is cleared.
+
+**Response 200**
+
+```json
+{ "ok": true, "user": { "id": "...", "username": "...", "email": "..." } }
+```
+
+Not found:
+
+- `404 USER_NOT_FOUND`
+
+---
+
+### 4.16 `DELETE /api/admin/users/:userId` (RHL-012)
+
+Delete a user.
+
+**Authentication**: required.
+
+**Authorization**: user management capability required (`superadmin` / `dev`).
+
+**Route params**
+
+- `userId` — MongoDB ObjectId
+
+**Response 200**
+
+```json
+{ "ok": true }
+```
+
+Not found:
+
+- `404 USER_NOT_FOUND`
+
+---
+
 ## 5. API v1 freeze (RHL-008)
 
 The endpoints and response shapes documented here (and in `docs/frontend-api-usage.md`) are considered **API v1** for the first frontend implementation.
@@ -498,7 +680,9 @@ Rules:
 When adding new endpoints:
 
 1. Define URL + method.
+
 2. Implement a `route.js` under `app/api/...`.
+
 3. Enforce auth:
    - `getSession()` for protected endpoints
    - return `401 AUTH_UNAUTHENTICATED` when session is missing
@@ -515,4 +699,5 @@ When adding new endpoints:
    - `lib/api/errors.js` (`withErrorHandling`, `ApiError`, helpers)
 
 7. Add route tests (Vitest).
+
 8. Update this document.

+ 11 - 4
Docs/auth.md

@@ -8,6 +8,7 @@ The system uses:
 - Cookie-based sessions with a signed JWT payload.
 - Role-aware access control (`branch`, `admin`, `superadmin`, `dev`).
 - Branch-level RBAC enforcement for filesystem-related APIs.
+- A separate **user management capability** for admin APIs (RHL-041 + RHL-012).
 
 ---
 
@@ -32,7 +33,7 @@ This document covers:
 Non-goals (for this document):
 
 - Email-based password recovery (documented as a follow-up ticket/phase).
-- User management endpoints (planned as **RHL-012**; this doc only defines the required permission semantics).
+- Full user management API contract details (documented in `Docs/api.md` and `Docs/frontend-api-usage.md`).
 
 ---
 
@@ -103,8 +104,8 @@ This project intentionally separates **two independent capabilities**:
 1. **Branch access** (existing capability)
    - Governs whether a user can browse/search/open PDFs for a given branch.
 
-2. **User management** (new capability, used by RHL-012)
-   - Governs whether a user can create/update/manage user accounts.
+2. **User management** (RHL-012)
+   - Governs whether a user can create/update/delete user accounts.
 
 ### 3.3 Role matrix
 
@@ -194,7 +195,7 @@ Common branch RBAC responses:
   { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } }
   ```
 
-### 4.2 User management capability (RHL-012 prerequisite)
+### 4.2 User management capability (RHL-012)
 
 User management is a separate permission capability from branch access.
 
@@ -216,6 +217,10 @@ For endpoints guarded by this capability, the standardized error is:
   }
   ```
 
+Where it is used:
+
+- User management endpoints under `/api/admin/users` (see `Docs/api.md`).
+
 ### 4.3 Permission helpers
 
 Authorization rules live in `lib/auth/permissions.js`:
@@ -414,6 +419,7 @@ Response:
 Behavior:
 
 1. Validate JSON and required fields.
+
 2. Load the current user by `session.userId`.
    - If the user cannot be found: treat as invalid session and return `401 AUTH_UNAUTHENTICATED`.
 
@@ -424,6 +430,7 @@ Behavior:
    - If weak: return `400 VALIDATION_WEAK_PASSWORD` with structured `details`.
 
 5. Hash and persist the new password.
+
 6. Clear flags / sensitive reset state:
    - `mustChangePassword = false`
    - `passwordResetToken = null`

+ 127 - 5
Docs/frontend-api-usage.md

@@ -10,6 +10,7 @@ Scope:
 - PDF streaming/opening behavior in the Explorer and in Search.
 - Search **date filters** (`from` / `to`) and **shareable URL sync** (RHL-025).
 - Password change for authenticated users (RHL-009).
+- Admin user management API usage (RHL-012).
 
 > UI developers: For the app shell layout and frontend route scaffold (public vs protected routes, placeholder pages), see **`docs/frontend-ui.md`**.
 >
@@ -26,6 +27,7 @@ Notes:
 - The backend provides a binary PDF stream/download endpoint (RHL-015). The Explorer integrates it for “Open PDF” (RHL-023).
 - The Search UI integrates the same “Open PDF” pattern (RHL-024).
 - The Search UI supports **optional date filters** (`from` / `to`) with a date range picker and presets (RHL-025).
+- User management endpoints are implemented under `/api/admin/users` and are guarded by the user-management capability (RHL-041 + RHL-012).
 
 ---
 
@@ -55,11 +57,15 @@ The core Explorer UI flow is a simple drill-down:
 7. `getFiles(branch, year, month, day)`
 8. Open a PDF via the binary endpoint (see section 4.3)
 
-Optional (admin/dev):
+Optional (admin/superadmin/dev):
 
 - use the search endpoint (`GET /api/search`) to implement cross-branch search UI (see section 4.4)
 - apply date filters via `from` / `to` in `YYYY-MM-DD` format (RHL-025)
 
+Optional (superadmin/dev):
+
+- manage user accounts via `/api/admin/users` (see section 4.5)
+
 Optional (authenticated users):
 
 - change password via `changePassword({ currentPassword, newPassword })` (RHL-009)
@@ -191,6 +197,13 @@ Search:
 
 - `search({ q, branch, scope, branches, from, to, limit, cursor })`
 
+Admin user management (RHL-012):
+
+- `adminListUsers({ q, role, branchId, limit, cursor })`
+- `adminCreateUser({ username, email, role, branchId, initialPassword })`
+- `adminUpdateUser(userId, patch)`
+- `adminDeleteUser(userId)`
+
 Low-level:
 
 - `apiFetch(path, options)`
@@ -293,7 +306,7 @@ Success (authenticated):
 {
 	"user": {
 		"userId": "...",
-		"role": "branch|admin|dev",
+		"role": "branch|admin|superadmin|dev",
 		"branchId": "NL01",
 		"email": "nl01@example.com"
 	}
@@ -391,7 +404,7 @@ Success:
 RBAC:
 
 - `branch` role: returns only its own branch
-- `admin`/`dev`: returns all branches
+- `admin`/`superadmin`/`dev`: returns all branches
 
 #### `GET /api/branches/:branch/years`
 
@@ -652,7 +665,110 @@ Pagination:
 RBAC note:
 
 - Branch users will only get results for their own branch.
-- Admin/dev users can use `scope=all` or `scope=multi` for cross-branch search.
+- Admin/superadmin/dev users can use `scope=all` or `scope=multi` for cross-branch search.
+
+### 4.5 Admin: User management (RHL-012)
+
+User management endpoints are served under:
+
+- `/api/admin/users`
+
+Authorization:
+
+- Allowed roles: `superadmin`, `dev`
+- Forbidden roles: `admin`, `branch`
+
+A forbidden call returns:
+
+- `403 AUTH_FORBIDDEN_USER_MANAGEMENT`
+
+#### 4.5.1 List users
+
+Use:
+
+- `adminListUsers({ q, role, branchId, limit, cursor })`
+
+Example:
+
+```js
+import { adminListUsers } from "@/lib/frontend/apiClient";
+
+export async function loadFirstPage() {
+	return adminListUsers({
+		q: "max",
+		role: "branch",
+		branchId: "NL01",
+		limit: 50,
+	});
+}
+```
+
+Response shape:
+
+```json
+{ "items": [{ "id": "...", "username": "..." }], "nextCursor": "..." }
+```
+
+Notes:
+
+- Pagination is cursor-based. Keep `nextCursor` in client state.
+
+#### 4.5.2 Create user
+
+Use:
+
+- `adminCreateUser({ username, email, role, branchId, initialPassword })`
+
+Example:
+
+```js
+import { adminCreateUser } from "@/lib/frontend/apiClient";
+
+export async function createBranchUser() {
+	return adminCreateUser({
+		username: "branchuser",
+		email: "nl01@example.com",
+		role: "branch",
+		branchId: "NL01",
+		initialPassword: "StrongPassword123",
+	});
+}
+```
+
+Duplicate handling:
+
+- The API returns `400 VALIDATION_INVALID_FIELD` and provides a structured `details` payload:
+  - `{ field: "username" }`
+  - `{ field: "email" }`
+  - `{ fields: ["username","email"] }`
+
+The UI should map those details to actionable messages (e.g. “Username exists already”).
+
+#### 4.5.3 Update user
+
+Use:
+
+- `adminUpdateUser(userId, patch)`
+
+Patchable fields (any subset):
+
+- `username`, `email`, `role`, `branchId`, `mustChangePassword`
+
+Notes:
+
+- Role/branch consistency is enforced by the backend:
+  - role `branch` requires a valid `branchId`
+  - non-branch roles clear `branchId`
+
+#### 4.5.4 Delete user
+
+Use:
+
+- `adminDeleteUser(userId)`
+
+Notes:
+
+- Not found returns `404 USER_NOT_FOUND`.
 
 ---
 
@@ -679,6 +795,7 @@ Auth:
 - `AUTH_UNAUTHENTICATED`
 - `AUTH_INVALID_CREDENTIALS`
 - `AUTH_FORBIDDEN_BRANCH`
+- `AUTH_FORBIDDEN_USER_MANAGEMENT`
 
 Password management:
 
@@ -691,6 +808,7 @@ Validation:
 - `VALIDATION_INVALID_JSON`
 - `VALIDATION_INVALID_BODY`
 - `VALIDATION_MISSING_FIELD`
+- `VALIDATION_INVALID_FIELD`
 
 Search validation:
 
@@ -703,6 +821,10 @@ Search validation:
 - `VALIDATION_SEARCH_CURSOR`
 - `VALIDATION_SEARCH_MISSING_FILTER`
 
+User management:
+
+- `USER_NOT_FOUND`
+
 Storage:
 
 - `FS_NOT_FOUND`
@@ -785,7 +907,7 @@ Rules:
 
 ## 9. Out of scope / planned additions
 
-- **Date-only Search UI mode** (admin/dev):
+- **Date-only Search UI mode** (admin/superadmin/dev):
   - The API supports searching by date range without `q`.
   - The current v1 UI intentionally requires `q` to trigger a search.
   - A dedicated UI toggle (“Search by date only”) could be added later to enable this safely.

+ 83 - 25
Docs/frontend-ui.md

@@ -1,4 +1,4 @@
-# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, Navigation Polish, Profile Password Change, and Role Refinement (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032 / RHL-009 / RHL-041)
+# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, Navigation Polish, Profile Password Change, User Management, and Role Refinement (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032 / RHL-009 / RHL-012 / RHL-041)
 
 This document describes the **frontend routing scaffold**, the **application shell layout**, and the core UI modules for the RHL Lieferscheine app.
 
@@ -14,7 +14,8 @@ Timeline:
 - **RHL-037**: Search scope UX improvements (TopNav deep-link branch switching + Single combobox + Multi selection UX + deterministic URL state).
 - **RHL-032**: Navigation UX polish (TopNav branding, theme toggle, user menu, tooltips, session indicator without content flicker, favicon, active states, and debounced loading UI).
 - **RHL-009**: Profile password change UI (Change Password form + toasts).
-- **RHL-041**: Role refinement (`superadmin`) + capability separation for future user management (RHL-012).
+- **RHL-041**: Role refinement (`superadmin`) + capability separation (branch access vs user management).
+- **RHL-012**: User management UI (list/create/update/delete users) + guarded admin endpoints.
 
 > **Language policy**
 >
@@ -26,7 +27,7 @@ Timeline:
 
 ## 1. Scope
 
-### 1.1 Implemented (as of RHL-041 + RHL-037 + RHL-025 + RHL-032 + RHL-009)
+### 1.1 Implemented (as of RHL-012 + RHL-041 + RHL-037 + RHL-025 + RHL-032 + RHL-009)
 
 - **Public** `/login` route with a functional login form (shadcn/ui primitives).
 
@@ -47,7 +48,7 @@ Timeline:
   - Logout action calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
 
 - **User dropdown menu (RHL-032)**:
-  - Profile / Support / Logout
+  - Profil / (optional) Benutzerverwaltung / Support / Abmelden
 
 - **Profile password change (RHL-009)**:
   - Route: `/profile` (protected)
@@ -66,7 +67,20 @@ Timeline:
   - UI introduces a pure role helper:
     - `lib/frontend/auth/roles.js`
       - `isAdminLike(role)` → `admin | superadmin | dev`
-      - `canManageUsers(role)` → `superadmin | dev` (RHL-012 prerequisite, no UI screens yet)
+      - `canManageUsers(role)` → `superadmin | dev`
+
+- **User management UI (RHL-012)**:
+  - Route: `/admin/users` (protected)
+  - Permission gating:
+    - allowed: `superadmin`, `dev`
+    - forbidden: `admin`, `branch`
+
+  - Features (v1):
+    - list users with filters (username/email search, role, branchId)
+    - create user dialog (initial password + must-change-password flag on create)
+    - update user (role/branchId consistency enforced)
+    - delete user
+    - cursor-based pagination (“Mehr laden”)
 
 - **UI RBAC (branch-level)**:
   - `BranchGuard` prevents branch users from accessing other branches’ URLs.
@@ -117,7 +131,7 @@ Timeline:
   - Solid header (no blur/transparency) for crisp borders.
   - Branding logo (light + dark assets).
   - Icon-only theme toggle.
-  - User dropdown menu (Profile / Support / Logout).
+  - User dropdown menu (Profil / Benutzerverwaltung\* / Support / Abmelden).
   - Consistent tooltips across navigation.
   - Session check indicator in TopNav (debounced) to avoid content flicker.
   - Clear active states for Explorer and Search.
@@ -126,14 +140,12 @@ Timeline:
   Implementation note:
   - For small static brand assets (logos), Next Image optimization is disabled (`unoptimized`) to avoid browser-specific “pending” indicators caused by aborted `/_next/image` optimization requests.
 
+  - The “Benutzerverwaltung” entry is only shown when the user has the user-management capability.
+
 ---
 
 ### 1.2 Still out of scope / planned
 
-- User management UI + APIs (RHL-012):
-  - The UI role helpers already define `canManageUsers(role)`.
-  - RHL-012 will add screens and API endpoints and must enforce `superadmin/dev`.
-
 - Email-based password reset/recovery:
   - Planned as a separate follow-up ticket/phase.
   - Rationale: higher security surface (token handling and non-leakage), external SMTP/IT dependencies, and separate rate limiting ticket (RHL-031) to avoid scope creep.
@@ -147,6 +159,14 @@ Timeline:
   - add a dedicated “Herunterladen” UI action (download variant)
   - optional in-app PDF viewer experience (instead of a new tab)
 
+- User management UX hardening (separate follow-up ticket):
+  - delete with explicit username confirmation
+  - branch user creation UX (fixed `NL` prefix + numeric-only input)
+  - optional branch existence check / warning when creating branch users
+
+- `mustChangePassword` enforcement (separate follow-up ticket):
+  - gate/redirect until password change is completed
+
 ---
 
 ## 2. Route Groups & URL Structure
@@ -172,6 +192,7 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
 | `/login`                     | Login page                  | Supports `reason` and `next` query params                  |
 | `/`                          | Protected entry placeholder | Rendered only when authenticated                           |
 | `/profile`                   | Profile                     | Password change is implemented here (RHL-009)              |
+| `/admin/users`               | Benutzerverwaltung          | Only `superadmin`/`dev` (RHL-012)                          |
 | `/:branch`                   | Explorer: years             | Example: `/NL01`                                           |
 | `/:branch/:year`             | Explorer: months            | Example: `/NL01/2025`                                      |
 | `/:branch/:year/:month`      | Explorer: days              | Example: `/NL01/2025/12`                                   |
@@ -265,7 +286,7 @@ Layout groups (left → right):
 - **Brand**: logo link to `/` (light + dark variants).
 - **Primary navigation**: QuickNav (branch selector + Explorer/Search buttons).
 - **Actions**: theme toggle + session indicator.
-- **User menu**: dropdown trigger (Profile / Support / Logout).
+- **User menu**: dropdown trigger (Profil / Benutzerverwaltung / Support / Abmelden).
 
 Design note:
 
@@ -316,6 +337,7 @@ File: `components/app-shell/UserStatus.jsx`
 Menu items (German):
 
 - **Profil** → `/profile` (account info + password change)
+- **Benutzerverwaltung** → `/admin/users` (only `superadmin` / `dev`)
 - **Support** → opens a `mailto:` link to support.
 - **Abmelden** → calls logout and redirects to login.
 
@@ -627,13 +649,43 @@ The project uses Sonner (shadcn/ui integration) for toast notifications.
 
 ---
 
-## 11. Profile: Password change (RHL-009)
+## 11. User management UI (RHL-012)
 
 ### 11.1 Route
 
+- `/admin/users` (protected)
+
+### 11.2 Authorization
+
+- UI gating uses `canManageUsers(role)` from `lib/frontend/auth/roles.js`.
+- Backend is authoritative:
+  - all user management APIs require `requireUserManagement(session)`.
+
+### 11.3 UI components
+
+Files:
+
+- `components/admin/users/AdminUsersPage.jsx` (gating)
+- `components/admin/users/AdminUsersClient.jsx` (list + filters + actions)
+- `components/admin/users/UsersTable.jsx` (table)
+- `components/admin/users/CreateUserDialog.jsx` + `components/admin/users/create-user/*` (create flow)
+
+The UI uses the API client wrappers from `lib/frontend/apiClient.js`:
+
+- `adminListUsers`
+- `adminCreateUser`
+- `adminUpdateUser`
+- `adminDeleteUser`
+
+---
+
+## 12. Profile: Password change (RHL-009)
+
+### 12.1 Route
+
 - `/profile` (protected)
 
-### 11.2 Components
+### 12.2 Components
 
 - `components/profile/ProfilePage.jsx`
   - Shows read-only account/session info (role, branch, email).
@@ -644,13 +696,13 @@ The project uses Sonner (shadcn/ui integration) for toast notifications.
 
 ---
 
-## 12. UI primitives (shadcn/ui)
+## 13. UI primitives (shadcn/ui)
 
 The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`.
 
 ---
 
-## 13. File Naming Convention (.js vs .jsx)
+## 14. File Naming Convention (.js vs .jsx)
 
 To keep the project consistent:
 
@@ -659,9 +711,9 @@ To keep the project consistent:
 
 ---
 
-## 14. Tests
+## 15. Tests
 
-### 14.1 Unit tests
+### 15.1 Unit tests
 
 Role helper tests (RHL-041):
 
@@ -669,7 +721,7 @@ Role helper tests (RHL-041):
 
 Existing tests remain as documented in the project.
 
-### 14.2 Running tests
+### 15.2 Running tests
 
 From the repo root:
 
@@ -685,27 +737,33 @@ npm run build
 
 ---
 
-## 15. Manual verification checklist
+## 16. Manual verification checklist
 
-### 15.1 Local (Docker)
+### 16.1 Local (Docker)
 
 - Login and verify TopNav role label per account.
 - Branch switching for admin-like users.
 - Explorer drill-down.
 - Search scopes (admin-like) + Search restrictions (branch).
 - Open PDFs.
+- User management UI (superadmin/dev): list/create/update/delete.
 
-### 15.2 Server
+### 16.2 Server
 
 - Repeat the local checks on the real server.
 - Validate RBAC behavior:
   - branch user → forbidden on foreign branches
   - admin-like → notfound on non-existent branches
+  - admin/branch → forbidden on `/admin/users`
 
 ---
 
-## 16. Planned follow-ups
+## 17. Planned follow-ups
+
+- User management UX hardening (separate ticket):
+  - delete with explicit username confirmation
+  - branch user creation UX (fixed `NL` prefix + numeric-only input)
+  - optional branch existence check / warning when creating branch users
 
-- User management UI + APIs (RHL-012):
-  - Guard endpoints using `requireUserManagement(session)` (backend).
-  - Gate UI screens using `canManageUsers(role)` (frontend).
+- `mustChangePassword` enforcement (separate ticket):
+  - redirect/gate until password is changed