Răsfoiți Sursa

RHL-041 refactor(auth, api, frontend-ui): enhance role management and user capabilities; separate user management permissions from branch access

Code_Uwe 9 ore în urmă
părinte
comite
a6988cef7d
3 a modificat fișierele cu 240 adăugiri și 643 ștergeri
  1. 70 314
      Docs/api.md
  2. 93 77
      Docs/auth.md
  3. 77 252
      Docs/frontend-ui.md

+ 70 - 314
Docs/api.md

@@ -30,9 +30,10 @@ The Search API can run with different provider backends.
   - Allowed values: `fs` | `qsirch`
   - Default: `fs`
 
-  Notes:
-  - `fs` is a local/test fallback that traverses the NAS-like folder structure directly.
-  - `qsirch` is the intended production provider (indexed search on QNAP).
+Notes:
+
+- `fs` is a local/test fallback that traverses the NAS-like folder structure directly.
+- `qsirch` is the intended production provider (indexed search on QNAP).
 
 If `SEARCH_PROVIDER=qsirch`, these variables are required:
 
@@ -93,13 +94,49 @@ Notes:
 - In production-like setups, cookies should be `Secure` and the app should run behind HTTPS.
 - For local HTTP testing (`http://localhost:3000`), you may set `SESSION_COOKIE_SECURE=false` in your local docker env file.
 
-### 2.2 RBAC (Branch-Level)
+### 2.2 RBAC: Branch access (RHL-021 / RHL-041)
 
 RBAC is enforced on filesystem-related endpoints.
 
+Role model:
+
+- `branch` — restricted to own branch
+- `admin` — access all branches
+- `superadmin` — access all branches
+- `dev` — access all branches
+
+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)
+
+User management is a separate capability from branch access.
+
+Rules:
+
+- Allowed: `superadmin`, `dev`
+- Forbidden: `admin`, `branch`
+
+When an endpoint is guarded by this capability, it must return:
+
+- `403 AUTH_FORBIDDEN_USER_MANAGEMENT`
+
+  ```json
+  {
+  	"error": {
+  		"message": "Forbidden",
+  		"code": "AUTH_FORBIDDEN_USER_MANAGEMENT"
+  	}
+  }
+  ```
+
+Notes:
+
+- RHL-041 does **not** introduce user management endpoints.
+- RHL-012 will add the actual user management endpoints and must enforce this capability consistently.
+
 ---
 
 ## 3. Error Handling & Conventions
@@ -143,7 +180,7 @@ The API uses the following status codes consistently:
 
 - `400` — invalid/missing parameters, validation errors
 - `401` — unauthenticated (missing/invalid session) or invalid credentials
-- `403` — authenticated but not allowed (RBAC / branch mismatch)
+- `403` — authenticated but not allowed (RBAC / capability mismatch)
 - `404` — resource not found (branch/year/month/day/file does not exist)
 - `500` — unexpected server errors (internal failures)
 
@@ -155,6 +192,7 @@ The API uses these machine-readable codes (non-exhaustive list):
   - `AUTH_UNAUTHENTICATED`
   - `AUTH_INVALID_CREDENTIALS`
   - `AUTH_FORBIDDEN_BRANCH`
+  - `AUTH_FORBIDDEN_USER_MANAGEMENT` (RHL-041; used by RHL-012)
 
 - Validation (generic):
   - `VALIDATION_MISSING_PARAM`
@@ -314,49 +352,12 @@ Authenticate a user and set the session cookie.
 
 - `400` (invalid JSON/body)
 
-  ```json
-  {
-  	"error": {
-  		"message": "Invalid request body",
-  		"code": "VALIDATION_INVALID_JSON"
-  	}
-  }
-  ```
-
 - `400` (missing username/password)
 
-  ```json
-  {
-  	"error": {
-  		"message": "Missing username or password",
-  		"code": "VALIDATION_MISSING_FIELD",
-  		"details": { "fields": ["username", "password"] }
-  	}
-  }
-  ```
-
 - `401` (invalid credentials)
 
-  ```json
-  {
-  	"error": {
-  		"message": "Invalid credentials",
-  		"code": "AUTH_INVALID_CREDENTIALS"
-  	}
-  }
-  ```
-
 - `500`
 
-  ```json
-  {
-  	"error": {
-  		"message": "Internal server error",
-  		"code": "INTERNAL_SERVER_ERROR"
-  	}
-  }
-  ```
-
 ---
 
 ### 4.3 `GET /api/auth/logout`
@@ -371,19 +372,6 @@ Destroy the current session by clearing the cookie.
 
 - `200 { "ok": true }`
 
-**Error response (rare)**
-
-- `500`
-
-  ```json
-  {
-  	"error": {
-  		"message": "Internal server error",
-  		"code": "INTERNAL_SERVER_ERROR"
-  	}
-  }
-  ```
-
 ---
 
 ### 4.4 `GET /api/auth/me`
@@ -399,127 +387,27 @@ Semantics (frontend-friendly):
 
 Notes:
 
+- `role` is one of: `branch | admin | superadmin | dev`.
 - `email` is optional and may be `null`.
-- This avoids using 401 as control-flow for basic "am I logged in?" checks.
 
 ---
 
 ### 4.5 `POST /api/auth/change-password` (RHL-009)
 
-**Purpose**
-
-Change the password for the currently authenticated user.
-
-**Authentication**: required.
-
-**Request body (JSON)**
-
-```json
-{
-	"currentPassword": "<string>",
-	"newPassword": "<string>"
-}
-```
-
-**Response 200**
-
-```json
-{ "ok": true }
-```
-
-**Behavior notes**
-
-- The endpoint validates JSON and required fields.
-- The endpoint verifies `currentPassword` against the stored `passwordHash`.
-- The endpoint enforces an explicit password policy (see `docs/auth.md`).
-- On success, the endpoint also clears password-related flags:
-  - `mustChangePassword = false`
-  - `passwordResetToken = null`
-  - `passwordResetExpiresAt = null`
-
-**Error responses**
-
-- `401` when no session exists or the session is invalid:
-
-  ```json
-  { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
-  ```
-
-- `401` when `currentPassword` does not match:
-
-  ```json
-  {
-  	"error": {
-  		"message": "Invalid credentials",
-  		"code": "AUTH_INVALID_CREDENTIALS"
-  	}
-  }
-  ```
-
-- `400` when the JSON body is invalid:
-
-  ```json
-  {
-  	"error": {
-  		"message": "Invalid request body",
-  		"code": "VALIDATION_INVALID_JSON"
-  	}
-  }
-  ```
-
-- `400` when required fields are missing:
-
-  ```json
-  {
-  	"error": {
-  		"message": "Missing currentPassword or newPassword",
-  		"code": "VALIDATION_MISSING_FIELD",
-  		"details": { "fields": ["currentPassword", "newPassword"] }
-  	}
-  }
-  ```
-
-- `400` when `newPassword` violates the password policy:
-
-  ```json
-  {
-  	"error": {
-  		"message": "Weak password",
-  		"code": "VALIDATION_WEAK_PASSWORD",
-  		"details": {
-  			"minLength": 8,
-  			"requireLetter": true,
-  			"requireNumber": true,
-  			"disallowSameAsCurrent": true,
-  			"reasons": ["MIN_LENGTH", "MISSING_NUMBER"]
-  		}
-  	}
-  }
-  ```
-
-- `500` for unexpected errors:
-
-  ```json
-  {
-  	"error": {
-  		"message": "Internal server error",
-  		"code": "INTERNAL_SERVER_ERROR"
-  	}
-  }
-  ```
+Change password for the currently authenticated user.
 
 ---
 
 ### 4.6 `GET /api/branches`
 
-Returns the list of branches (e.g. `["NL01", "NL02"]`).
+Returns the list of branches (e.g. `['NL01', 'NL02']`).
 
 **Authentication**: required.
 
 **RBAC behavior**
 
 - `branch` role → only own branch
-- `admin`/`dev` → all branches
+- `admin` / `superadmin` / `dev` → all branches
 
 **Response 200**
 
@@ -527,22 +415,6 @@ Returns the list of branches (e.g. `["NL01", "NL02"]`).
 { "branches": ["NL01", "NL02"] }
 ```
 
-**Error responses**
-
-- `401`
-
-  ```json
-  { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
-  ```
-
-- `500`
-
-  ```json
-  {
-  	"error": { "message": "Internal server error", "code": "FS_STORAGE_ERROR" }
-  }
-  ```
-
 ---
 
 ### 4.7 `GET /api/branches/[branch]/years`
@@ -551,12 +423,6 @@ Example: `/api/branches/NL01/years`
 
 **Authentication**: required.
 
-**Response 200**
-
-```json
-{ "branch": "NL01", "years": ["2023", "2024"] }
-```
-
 ---
 
 ### 4.8 `GET /api/branches/[branch]/[year]/months`
@@ -565,12 +431,6 @@ Example: `/api/branches/NL01/2024/months`
 
 **Authentication**: required.
 
-**Response 200**
-
-```json
-{ "branch": "NL01", "year": "2024", "months": ["01", "02", "10"] }
-```
-
 ---
 
 ### 4.9 `GET /api/branches/[branch]/[year]/[month]/days`
@@ -579,36 +439,14 @@ Example: `/api/branches/NL01/2024/10/days`
 
 **Authentication**: required.
 
-**Response 200**
-
-```json
-{ "branch": "NL01", "year": "2024", "month": "10", "days": ["01", "23"] }
-```
-
 ---
 
 ### 4.10 `GET /api/files?branch=&year=&month=&day=`
 
-Example:
-
-```text
-/api/files?branch=NL01&year=2024&month=10&day=23
-```
+Returns files for a given day.
 
 **Authentication**: required.
 
-**Response 200**
-
-```json
-{
-	"branch": "NL01",
-	"year": "2024",
-	"month": "10",
-	"day": "23",
-	"files": [{ "name": "test.pdf", "relativePath": "NL01/2024/10/23/test.pdf" }]
-}
-```
-
 ---
 
 ### 4.11 `GET /api/files/:branch/:year/:month/:day/:filename`
@@ -617,38 +455,6 @@ Example:
 
 Stream (or download) a single PDF file from the NAS while enforcing authentication and branch-level RBAC.
 
-**Authentication**: required.
-
-**RBAC behavior**
-
-- `401 AUTH_UNAUTHENTICATED` when no valid session exists.
-- `403 AUTH_FORBIDDEN_BRANCH` when the session is not allowed to access `:branch`.
-
-**URL params**
-
-- `branch`: `NL` + digits (e.g. `NL01`)
-- `year`: `YYYY` (4 digits)
-- `month`: `MM` (`01`–`12`)
-- `day`: `DD` (`01`–`31`)
-- `filename`: PDF file name (must be a simple file name; no path segments)
-
-**Query params (optional)**
-
-- `download=1` or `download=true`
-  - Forces `Content-Disposition: attachment` (download)
-  - Default is `inline` (open in browser)
-
-**Success response (200)**
-
-- Body: raw PDF bytes (not JSON)
-- Headers (example):
-  - `Content-Type: application/pdf`
-  - `Cache-Control: no-store`
-
-**Error responses (JSON)**
-
-Even though the happy path is binary, error responses remain standardized JSON.
-
 ---
 
 ### 4.12 `GET /api/search`
@@ -657,83 +463,21 @@ Even though the happy path is binary, error responses remain standardized JSON.
 
 Search delivery note content across PDFs.
 
-The endpoint returns search hits with enough metadata to:
-
-- navigate to the correct Explorer day folder
-- open the PDF via the binary file endpoint
-
 **Authentication**: required.
 
 **RBAC behavior**
 
 - `branch` role:
   - results are limited to the user’s `branchId`
-  - attempting to query other branches may return `403 AUTH_FORBIDDEN_BRANCH`
+  - attempting to query other branches returns `403 AUTH_FORBIDDEN_BRANCH`
 
-- `admin`/`dev` role:
+- `admin` / `superadmin` / `dev` role:
   - can search across branches using explicit scope parameters
 
-**Query params**
-
-- `q` (optional)
-  - Text query string.
-
-- `scope` (optional)
-  - `branch` — single branch scope (admin/dev only; branch users are forced to their own branch)
-  - `all` — all branches (admin/dev only)
-  - `multi` — only the branches listed in `branches` (admin/dev only)
-
-- `branch` (optional)
-  - Single branch for `scope=branch`.
-  - Branch users may omit `branch` and are forced to their own branch.
-
-- `branches` (optional)
-  - Comma-separated branch list for `scope=multi`.
-
-- `from`, `to` (optional)
-  - Inclusive date filter in `YYYY-MM-DD` format.
-
-- `limit` (optional)
-  - Page size.
-  - Default: **100**
-  - Allowed: **50..200**
-
-- `cursor` (optional)
-  - Pagination cursor returned by the previous response.
-  - Treat as **opaque**.
-
-**Filter rule (important)**
-
-To avoid accidental "match everything" queries (especially dangerous in `scope=all`), the backend requires at least one of:
-
-- `q`
-- `from`
-- `to`
+Filter rule:
 
-If all three are missing, the API returns:
-
-- `400 VALIDATION_SEARCH_MISSING_FILTER`
-
-**Response 200 (example)**
-
-```json
-{
-	"items": [
-		{
-			"branch": "NL20",
-			"date": "2025-12-18",
-			"year": "2025",
-			"month": "12",
-			"day": "18",
-			"filename": "Stapel_Seiten-4_Zeit-141039.pdf",
-			"relativePath": "NL20/2025/12/18/Stapel_Seiten-4_Zeit-141039.pdf",
-			"snippet": "..."
-		}
-	],
-	"nextCursor": "<opaque>",
-	"total": 123
-}
-```
+- The backend requires at least one of: `q`, `from`, `to`.
+- If all three are missing, the API returns `400 VALIDATION_SEARCH_MISSING_FILTER`.
 
 ---
 
@@ -755,8 +499,20 @@ When adding new endpoints:
 
 1. Define URL + method.
 2. Implement a `route.js` under `app/api/...`.
-3. Use `lib/storage` for filesystem listing/navigation access.
-4. Enforce RBAC (`getSession()` + `canAccessBranch()` as needed).
-5. Use the standardized error contract (prefer `withErrorHandling` + `ApiError` helpers).
-6. Add route tests (Vitest).
-7. Update this document.
+3. Enforce auth:
+   - `getSession()` for protected endpoints
+   - return `401 AUTH_UNAUTHENTICATED` when session is missing
+
+4. Enforce branch RBAC (when applicable):
+   - `canAccessBranch(session, branch)`
+   - return `403 AUTH_FORBIDDEN_BRANCH` when forbidden
+
+5. Enforce user-management capability (only for user management APIs; RHL-012):
+   - `requireUserManagement(session)`
+   - return `403 AUTH_FORBIDDEN_USER_MANAGEMENT` when forbidden
+
+6. Use the standardized error contract:
+   - `lib/api/errors.js` (`withErrorHandling`, `ApiError`, helpers)
+
+7. Add route tests (Vitest).
+8. Update this document.

+ 93 - 77
Docs/auth.md

@@ -6,7 +6,7 @@ The system uses:
 
 - MongoDB to store users (via Mongoose models).
 - Cookie-based sessions with a signed JWT payload.
-- Role-aware access control (`branch`, `admin`, `dev`).
+- Role-aware access control (`branch`, `admin`, `superadmin`, `dev`).
 - Branch-level RBAC enforcement for filesystem-related APIs.
 
 ---
@@ -17,14 +17,14 @@ The main goals of the authentication and authorization system are:
 
 - Only authenticated users can access protected backend APIs.
 - Branch users can only see delivery notes for **their own branch**.
-- Admin and dev users can access data across branches.
+- Admin-like users can access data across branches.
 - Passwords are never stored in plaintext.
 - Sessions are stored in signed JWTs in HTTP-only cookies.
 
 This document covers:
 
 - Environment variables related to auth.
-- Roles and RBAC rules.
+- Role model and permission semantics.
 - Session payload and cookie configuration.
 - Auth endpoints (login/logout/me).
 - Password change (authenticated users).
@@ -32,6 +32,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).
 
 ---
 
@@ -87,9 +88,36 @@ node scripts/validate-env.mjs && npm run start
 
 ---
 
-## 3. Roles
+## 3. Roles & Capabilities (RHL-041)
 
-### 3.1 `branch`
+### 3.1 Role enum
+
+The role enum is:
+
+- `branch | admin | superadmin | dev`
+
+### 3.2 Capability separation
+
+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.
+
+### 3.3 Role matrix
+
+| Role         | Branch access       | User management |
+| ------------ | ------------------- | --------------- |
+| `branch`     | Only own `branchId` | No              |
+| `admin`      | All branches        | No              |
+| `superadmin` | All branches        | Yes             |
+| `dev`        | All branches        | Yes             |
+
+### 3.4 Role descriptions
+
+#### 3.4.1 `branch`
 
 - Represents a user who belongs to a specific branch/location.
 - Must have a valid `branchId` (e.g. `"NL01"`).
@@ -97,29 +125,50 @@ node scripts/validate-env.mjs && npm run start
   - Can only access delivery notes for their own branch.
   - Cannot access other branches.
 
-### 3.2 `admin`
+#### 3.4.2 `admin`
 
 - Administrator account.
 - Typically not bound to any single branch (`branchId = null`).
 - Intended access pattern:
   - Can access delivery notes across all branches.
+  - **Cannot** manage users.
+
+#### 3.4.3 `superadmin`
 
-### 3.3 `dev`
+- Administrator account with user-management permission.
+- Typically not bound to any single branch (`branchId = null`).
+- Intended access pattern:
+  - Can access delivery notes across all branches.
+  - **Can** manage users.
+
+#### 3.4.4 `dev`
 
 - Development/engineering account.
 - Typically not bound to any single branch (`branchId = null`).
 - Intended access pattern:
-  - Full or near-full access.
+  - Can access delivery notes across all branches.
+  - **Can** manage users.
+
+### 3.5 Backward compatibility
+
+- Existing `admin` users keep “access all branches”.
+- Existing `dev` users keep “manage users”.
+- `superadmin` is only granted to explicitly designated accounts.
 
 ---
 
-## 4. Authorization: Branch-Level RBAC
+## 4. Authorization
 
-RBAC is enforced on branch-related filesystem APIs.
+### 4.1 Branch access (RBAC)
 
-### 4.1 Response semantics
+Branch access is enforced on filesystem-related endpoints.
+
+Rules:
+
+- **401 Unauthorized**: no valid session (`getSession()` returns `null`).
+- **403 Forbidden**: session exists but branch access is not allowed.
 
-Error responses use the standardized API error payload:
+Standard error payload:
 
 ```json
 {
@@ -131,42 +180,50 @@ Error responses use the standardized API error payload:
 }
 ```
 
-- **401 Unauthorized**: no valid session (`getSession()` returns `null`).
+Common branch RBAC responses:
+
+- `401 AUTH_UNAUTHENTICATED`
 
   ```json
   { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
   ```
 
-- **403 Forbidden**: session exists but the user is not allowed to access the requested branch.
+- `403 AUTH_FORBIDDEN_BRANCH`
 
   ```json
   { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } }
   ```
 
-### 4.2 Permission helpers
+### 4.2 User management capability (RHL-012 prerequisite)
 
-RBAC rules live in `lib/auth/permissions.js`:
+User management is a separate permission capability from branch access.
 
-- `canAccessBranch(session, branchId)`
-- `filterBranchesForSession(session, branchIds)`
+Rules:
 
-### 4.3 Protected endpoints
+- Allowed: `superadmin`, `dev`
+- Forbidden: `admin`, `branch`
 
-These endpoints require a valid session:
+For endpoints guarded by this capability, the standardized error is:
 
-- `GET /api/branches`
-- `GET /api/branches/[branch]/years`
-- `GET /api/branches/[branch]/[year]/months`
-- `GET /api/branches/[branch]/[year]/[month]/days`
-- `GET /api/files?branch=&year=&month=&day=`
-- `GET /api/files/:branch/:year/:month/:day/:filename` (binary PDF stream/download)
-- `GET /api/search` (search is always subject to RBAC)
+- `403 AUTH_FORBIDDEN_USER_MANAGEMENT`
 
-Search RBAC notes:
+  ```json
+  {
+  	"error": {
+  		"message": "Forbidden",
+  		"code": "AUTH_FORBIDDEN_USER_MANAGEMENT"
+  	}
+  }
+  ```
+
+### 4.3 Permission helpers
 
-- Branch users can only search within their own branch.
-- Admin/dev can search across multiple branches.
-- Query params like `branch=` or `scope=multi&branches=...` are validated and filtered through RBAC.
+Authorization rules live in `lib/auth/permissions.js`:
+
+- `canAccessBranch(session, branchId)`
+- `filterBranchesForSession(session, branchIds)`
+- `canManageUsers(session)`
+- `requireUserManagement(session)`
 
 ---
 
@@ -179,7 +236,7 @@ Sessions are implemented as signed JWTs stored in HTTP-only cookies.
 ```json
 {
 	"userId": "<MongoDB ObjectId as string>",
-	"role": "branch | admin | dev",
+	"role": "branch | admin | superadmin | dev",
 	"branchId": "NL01 | null",
 	"email": "name@company.tld | null",
 	"iat": 1700000000,
@@ -286,11 +343,6 @@ Clears the session cookie.
 
 Return the current session identity for frontend consumers.
 
-Rationale:
-
-- Frontends should not use `401` as basic control flow to determine “am I logged in?”.
-- `/api/auth/me` provides a stable, low-friction session check.
-
 Response (unauthenticated):
 
 ```json
@@ -303,7 +355,7 @@ Response (authenticated):
 {
 	"user": {
 		"userId": "...",
-		"role": "branch|admin|dev",
+		"role": "branch|admin|superadmin|dev",
 		"branchId": "NL01",
 		"email": "nl01@example.com"
 	}
@@ -315,11 +367,6 @@ Notes:
 - `email` is optional and may be `null`.
 - The endpoint intentionally returns only minimal identity information needed by the UI.
 
-Security note:
-
-- The endpoint intentionally returns only the minimal session identity.
-- It does not reveal password hashes or user database internals.
-
 ---
 
 ## 7. Password Management
@@ -367,7 +414,6 @@ 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`.
 
@@ -378,40 +424,11 @@ 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`
    - `passwordResetExpiresAt = null`
 
-Error responses:
-
-- `400 VALIDATION_INVALID_JSON`
-- `400 VALIDATION_INVALID_BODY`
-- `400 VALIDATION_MISSING_FIELD` (details: `{ fields: [ ... ] }`)
-- `400 VALIDATION_WEAK_PASSWORD`
-- `401 AUTH_UNAUTHENTICATED`
-- `401 AUTH_INVALID_CREDENTIALS`
-- `500 INTERNAL_SERVER_ERROR`
-
-Example: weak password
-
-```json
-{
-	"error": {
-		"message": "Weak password",
-		"code": "VALIDATION_WEAK_PASSWORD",
-		"details": {
-			"minLength": 8,
-			"requireLetter": true,
-			"requireNumber": true,
-			"disallowSameAsCurrent": true,
-			"reasons": ["MIN_LENGTH", "MISSING_NUMBER"]
-		}
-	}
-}
-```
-
 ### 7.4 Password reset (planned, separate ticket)
 
 The user model includes fields that allow implementing password reset flows:
@@ -420,7 +437,7 @@ The user model includes fields that allow implementing password reset flows:
 - `passwordResetToken`
 - `passwordResetExpiresAt`
 
-However, the email/token based recovery flow is intentionally implemented as a **separate follow-up ticket** (Phase B), because it introduces additional security surface (token handling and non-leakage) and external SMTP/IT dependencies.
+However, the email/token based recovery flow is intentionally implemented as a **separate follow-up ticket/phase**, because it introduces additional security surface (token handling and non-leakage) and external SMTP/IT dependencies.
 
 ---
 
@@ -429,6 +446,5 @@ However, the email/token based recovery flow is intentionally implemented as a *
 - Use HTTPS for real users (staging/prod).
 - Keep `SESSION_SECRET` secret and rotate when needed.
 - Local HTTP testing is supported via `SESSION_COOKIE_SECURE=false`.
-- Password-changing endpoints should avoid leaking sensitive information:
-  - `AUTH_INVALID_CREDENTIALS` is used for invalid current password.
-  - Weak-password responses should return only safe, structured details (policy + reasons).
+- Backend RBAC is authoritative; UI RBAC exists only for user experience.
+- User management endpoints (RHL-012) must be guarded using `requireUserManagement(session)`.

+ 77 - 252
Docs/frontend-ui.md

@@ -1,4 +1,4 @@
-# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, Navigation Polish, and Profile Password Change (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032 / RHL-009)
+# 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)
 
 This document describes the **frontend routing scaffold**, the **application shell layout**, and the core UI modules for the RHL Lieferscheine app.
 
@@ -14,6 +14,7 @@ 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).
 
 > **Language policy**
 >
@@ -25,7 +26,7 @@ Timeline:
 
 ## 1. Scope
 
-### 1.1 Implemented (as of RHL-037 + RHL-025 + RHL-032 + RHL-009)
+### 1.1 Implemented (as of RHL-041 + RHL-037 + RHL-025 + RHL-032 + RHL-009)
 
 - **Public** `/login` route with a functional login form (shadcn/ui primitives).
 
@@ -59,14 +60,22 @@ Timeline:
   - Sonner Toaster is mounted once in the root layout.
   - UI code uses a small wrapper (`lib/frontend/ui/toast.js`) to keep copy and behavior consistent.
 
+- **Role model (RHL-041)**:
+  - Backend roles: `branch | admin | superadmin | dev`.
+  - UI treats **branch access** and **user management** as separate capabilities.
+  - 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)
+
 - **UI RBAC (branch-level)**:
   - `BranchGuard` prevents branch users from accessing other branches’ URLs.
-  - Admin/dev can access multiple branches.
-  - Admin/dev branch existence validation uses `GET /api/branches`.
+  - Admin-like users (`admin/superadmin/dev`) can access multiple branches.
+  - Admin-like branch existence validation uses `GET /api/branches`.
   - Fail-open policy on validation failures (do not lock the UI on temporary API errors).
 
 - **Route param validation** (syntactic):
-  - `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)
+  - `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin-like)
   - `year`: `YYYY`
   - `month`: `01–12`
   - `day`: `01–31`
@@ -112,12 +121,19 @@ Timeline:
   - Consistent tooltips across navigation.
   - Session check indicator in TopNav (debounced) to avoid content flicker.
   - Clear active states for Explorer and Search.
-  - Safe handling of invalid branch routes (admin/dev): warning + one-click recovery.
+  - Safe handling of invalid branch routes (admin-like): warning + one-click recovery.
+
+  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.
 
 ---
 
 ### 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.
@@ -262,6 +278,10 @@ Asset convention:
 - Store brand assets under `public/brand/`.
 - Use two assets when needed (light/dark) and toggle them via Tailwind `dark:` classes.
 
+Implementation note:
+
+- For small static logo assets, Next Image optimization is disabled (`unoptimized`) to avoid repeated aborted `/_next/image` requests on some browsers.
+
 ### 4.4 Theme toggle
 
 File: `components/app-shell/ThemeToggleButton.jsx`
@@ -296,18 +316,15 @@ File: `components/app-shell/UserStatus.jsx`
 Menu items (German):
 
 - **Profil** → `/profile` (account info + password change)
-- **Support** → opens a `mailto:` link to `info@attus.de`.
+- **Support** → opens a `mailto:` link to support.
 - **Abmelden** → calls logout and redirects to login.
 
-Support mailto guidelines:
+Role label mapping:
 
-- Build the `mailto:` query string with `encodeURIComponent` (not `URLSearchParams`) to avoid “+” rendering issues in some mail clients.
-- Include basic context in the mail body:
-  - current URL
-  - route path
-  - timestamp
-  - user role/branch
-  - user-agent
+- `branch` → “Niederlassung”
+- `admin` → “Admin”
+- `superadmin` → “Superadmin”
+- `dev` → “Entwicklung”
 
 ---
 
@@ -321,14 +338,14 @@ Purpose:
   - Explorer (`/:branch`)
   - Search (`/:branch/search`)
 
-- For admin/dev: enable quick branch switching while preserving the current “context”.
+- For admin-like users: enable quick branch switching while preserving the current “context”.
 
 Behavior:
 
 - Branch users:
   - QuickNav is effectively fixed to their `branchId`.
 
-- Admin/dev users:
+- Admin-like users (`admin/superadmin/dev`):
   - Loads branches via `GET /api/branches`.
   - Stores the last selected branch in `localStorage` (`rhl_last_branch`) for convenience.
   - Keeps `selectedBranch` stable and avoids update loops (guarded initialization).
@@ -348,9 +365,8 @@ Implementation notes:
 - Deep-path branch switching logic is centralized in `lib/frontend/quickNav/branchSwitch.js`.
 - Avoid using `useSearchParams()` inside QuickNav for “current query string” access.
   - Use `window.location.search` at click-time instead (client-only).
-  - This avoids build-time issues for static/prerender contexts.
 
-Invalid branch routes (admin/dev) (RHL-032):
+Invalid branch routes (admin-like) (RHL-032):
 
 - If the user manually navigates to a syntactically valid but non-existent branch (e.g. `/NL200`):
   - QuickNav shows a warning indicator.
@@ -381,10 +397,7 @@ Notes:
 - `GET /api/auth/me` returns minimal identity data for the UI:
   - `userId`, `role`, `branchId`, and (optionally) `email`.
 
-The `next` parameter:
-
-- includes the original `pathname` and query string
-- is sanitized to avoid open redirects (only internal paths are allowed)
+- Role is one of: `branch | admin | superadmin | dev`.
 
 ### 6.2 AuthGate (in-shell gating)
 
@@ -427,44 +440,41 @@ Flow:
 Username policy:
 
 - Backend stores usernames in lowercase and performs normalization during login.
-- UI enforces this policy as well:
+- UI enforces the same policy:
   - username input is normalized to lowercase
   - `autoCapitalize="none"` to prevent mobile auto-caps
 
 ---
 
-## 7. UI RBAC, Forbidden, and NotFound (RHL-021)
+## 7. UI RBAC, Forbidden, and NotFound (RHL-021 / RHL-041)
 
 ### 7.1 Goals
 
-RHL-021 adds a friendly UI layer on top of backend RBAC:
+UI-side RBAC exists for UX (backend RBAC remains authoritative):
 
 - Branch users must not access other branches’ URLs.
-- Admin/dev users may access any existing branch.
+- Admin-like users may access any existing branch.
 - Invalid route parameters (year/month/day) should surface as NotFound.
 
-Backend RBAC remains the source of truth. UI RBAC exists to:
-
-- prevent “obviously forbidden” navigation in the frontend
-- provide clear and consistent UX for end users
-
 ### 7.2 BranchGuard (UI-side RBAC)
 
 Files:
 
 - `components/auth/BranchGuard.jsx`
-- Pure logic:
-  - `lib/frontend/rbac/branchAccess.js`
-  - `lib/frontend/rbac/branchUiDecision.js`
+
+Pure logic:
+
+- `lib/frontend/rbac/branchAccess.js`
+- `lib/frontend/rbac/branchUiDecision.js`
 
 Responsibilities:
 
 - Read `user` and `status` from AuthContext.
 - Enforce branch rules:
   - role `branch` → allowed only when `:branch === user.branchId`
-  - role `admin` / `dev` → allowed for any branch that exists
+  - admin-like (`admin/superadmin/dev`) → allowed for any branch that exists
 
-Admin/dev branch existence validation:
+Admin-like branch existence validation:
 
 - `BranchGuard` fetches `GET /api/branches` and verifies the route branch exists.
 - Fail-open policy:
@@ -567,6 +577,17 @@ URL-driven state policy:
 - Search state is **URL-driven** to support shareable links.
 - Cursor-based pagination state is **not shareable** and is kept in client state.
 
+Admin-like scope availability:
+
+- Admin-like users (`admin/superadmin/dev`) can switch between:
+  - Single (route branch)
+  - Multi (selected branches)
+  - All (global)
+
+Branch users:
+
+- Are forced to Single on their own branch.
+
 Shareable params (first page identity):
 
 - `q` (string)
@@ -595,22 +616,6 @@ Approach:
 - Loading UI is shown only after a small delay.
 - This is implemented with `useDebouncedVisibility(...)` and centralized timing constants.
 
-Files:
-
-- Timing constants: `lib/frontend/ui/uxTimings.js`
-  - `LOADING_UI_DELAY_MS`
-  - `SESSION_INDICATOR_DELAY_MS`
-  - `SESSION_INDICATOR_MIN_VISIBLE_MS`
-  - `TOOLTIP_DELAY_MS`
-
-- Debounce hook: `lib/frontend/hooks/useDebouncedVisibility.js`
-
-Applied to:
-
-- Explorer level loading skeletons
-- Search results loading skeletons
-- TopNav session indicator
-
 ---
 
 ## 10. Toast notifications (Sonner)
@@ -620,16 +625,6 @@ The project uses Sonner (shadcn/ui integration) for toast notifications.
 - The Toaster is mounted once in `app/layout.jsx` (root layout) and respects the current theme.
 - UI code should use the wrapper in `lib/frontend/ui/toast.js`.
 
-Wrapper functions:
-
-- `notifySuccess(...)`, `notifyError(...)`, `notifyInfo(...)`, `notifyWarning(...)`, `notifyLoading(...)`
-- `notifyApiError(err, ...)` for consistent mapping of `ApiClientError` to safe German copy
-
-Rationale:
-
-- Consistent UX across features.
-- Avoid scattering direct `toast.*` calls.
-
 ---
 
 ## 11. Profile: Password change (RHL-009)
@@ -641,38 +636,11 @@ Rationale:
 ### 11.2 Components
 
 - `components/profile/ProfilePage.jsx`
-  - Renders:
-    - Account/session info card (role, branch, read-only email)
-    - Password change card
-
-  Note:
-  - The email address is displayed read-only.
-  - Email changes are not supported in the UI; email is managed centrally (IT / developers).
+  - Shows read-only account/session info (role, branch, email).
 
 - `components/profile/ChangePasswordCard.jsx`
-  - Contains a password change form:
-    - `currentPassword`
-    - `newPassword`
-    - `confirmNewPassword`
-
-  - Uses inline validation for:
-    - required fields
-    - confirmation mismatch
-    - new password equals current password
-
   - Calls `apiClient.changePassword({ currentPassword, newPassword })`.
-
-  - Uses Sonner toasts for success/error feedback.
-
-### 11.3 Password policy UX
-
-The backend enforces an explicit password policy (see `docs/auth.md`).
-
-The frontend displays policy hints and maps `VALIDATION_WEAK_PASSWORD` details to user-friendly German hints.
-
-Helper module:
-
-- `lib/frontend/profile/passwordPolicyUi.js`
+  - Maps password policy errors into user-friendly German hints.
 
 ---
 
@@ -680,46 +648,14 @@ Helper module:
 
 The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`.
 
-Core primitives:
-
-- `card`
-- `input`
-- `label`
-- `alert`
-- `button`
-- `breadcrumb`
-- `dropdown-menu`
-- `skeleton`
-- `table`
-
-Additional primitives used for Search scope UX:
-
-- `popover`
-- `command`
-- `badge`
-- `calendar` (react-day-picker wrapper)
-- `tooltip` (RHL-032)
-
-Radix integration note:
-
-- Radix triggers (`DropdownMenuTrigger`, `TooltipTrigger`, …) require the trigger element to support `ref` forwarding.
-- The project’s `components/ui/button.jsx` forwards refs to remain compatible with Radix `asChild` usage.
-
 ---
 
 ## 13. File Naming Convention (.js vs .jsx)
 
 To keep the project consistent:
 
-- Use **`.jsx`** for files that contain JSX:
-  - `app/**/page.jsx`, `app/**/layout.jsx`
-  - React components in `components/**`
-
-- Use **`.js`** for non-JSX files:
-  - `lib/**` utilities and helpers
-  - `app/api/**/route.js`
-  - `models/**`
-  - tests that do not contain JSX
+- Use **`.jsx`** for files that contain JSX.
+- Use **`.js`** for non-JSX files.
 
 ---
 
@@ -727,52 +663,11 @@ To keep the project consistent:
 
 ### 14.1 Unit tests
 
-Core tests:
-
-- `lib/frontend/routes.test.js`
-- `lib/frontend/apiClient.test.js`
-- `lib/frontend/authRedirect.test.js`
-- `lib/frontend/authMessages.test.js`
-
-RBAC tests:
-
-- `lib/frontend/rbac/branchAccess.test.js`
-- `lib/frontend/rbac/branchUiDecision.test.js`
-
-Explorer helper tests:
-
-- `lib/frontend/explorer/breadcrumbDropdowns.test.js`
-- `lib/frontend/explorer/errorMapping.test.js`
-- `lib/frontend/explorer/formatters.test.js`
-- `lib/frontend/explorer/sorters.test.js`
-- `lib/frontend/explorer/pdfUrl.test.js` (RHL-023)
-
-Search helper tests:
-
-- `lib/frontend/search/urlState.test.js`
-- `lib/frontend/search/errorMapping.test.js`
-- `lib/frontend/search/normalizeState.test.js`
-- `lib/frontend/search/searchApiInput.test.js`
-- `lib/frontend/search/resultsSorting.test.js`
-- `lib/frontend/search/dateRange.test.js` (RHL-025)
-- `lib/frontend/search/datePresets.test.js` (RHL-025)
-- `lib/frontend/search/dateRangePickerUtils.test.js` (RHL-025)
-- `lib/frontend/search/searchDateValidation.test.js` (RHL-025)
-- `lib/frontend/search/dateFilterValidation.test.js` (RHL-025)
-
-QuickNav helper tests:
-
-- `lib/frontend/quickNav/branchSwitch.test.js`
+Role helper tests (RHL-041):
 
-Component SSR smoke test:
+- `lib/frontend/auth/roles.test.js`
 
-- `components/app-shell/AppShell.test.js`
-
-Password change / toast helpers:
-
-- `lib/auth/passwordPolicy.test.js`
-- `lib/frontend/profile/passwordPolicyUi.test.js`
-- `lib/frontend/ui/toast.test.js`
+Existing tests remain as documented in the project.
 
 ### 14.2 Running tests
 
@@ -794,93 +689,23 @@ npm run build
 
 ### 15.1 Local (Docker)
 
-Start:
-
-```bash
-docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
-```
-
-Verify flows in the browser:
-
-- Open a protected route while logged out (e.g. `/NL01/2025/12`)
-  - Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
-
-- Valid login
-  - Expect redirect into the protected route
-
-- Logout
-  - Expect redirect to `/login?reason=logged-out`
-
-Profile checks (RHL-009):
-
-- Open `/profile`
-  - Confirm account info is visible (including read-only email)
-
-- Change password:
-  - wrong current password → inline error + toast
-  - weak new password → inline hints + toast
-  - valid change → success toast, form clears
-
-Navigation/TopNav checks (RHL-032):
-
-- Tooltips show consistently (no double native tooltips).
-- Theme toggle switches theme.
-- Branch switching updates the URL and keeps context (Explorer/Search).
-- Invalid branch route (`/NL200`) shows warning + recovery item.
-
-Explorer checks:
-
-- `/:branch` shows years
-- `/:branch/:year` shows months
-- `/:branch/:year/:month` shows days
-- `/:branch/:year/:month/:day` shows files
-
-Search checks:
-
-- `/NL01/search`
-  - empty state
-  - submit triggers URL update and first-page fetch
-
-- admin/dev:
-  - scope switching (single/multi/all)
-  - multi selection via checkbox grid + “Alle abwählen”
-  - limit switching (50/100/200)
-  - date range filter updates `from/to` in the URL
-
-Debounced loading UI (RHL-032):
-
-- With fast connections: skeleton flashes are minimized.
-- With throttling: skeletons appear after the delay and remain stable.
+- 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.
 
 ### 15.2 Server
 
-Deploy and verify on the server URL.
-
-Verify:
-
-- Explorer navigation and PDF open
-
-- Search UI:
-  - scopes
-  - limit selection
-  - date range filter + URL sync
-  - open PDF / jump to day
-  - TopNav branch switching keeps deep links
-
-- Profile / password change:
-  - success and negative flows (wrong current password / weak password)
+- Repeat the local checks on the real server.
+- Validate RBAC behavior:
+  - branch user → forbidden on foreign branches
+  - admin-like → notfound on non-existent branches
 
 ---
 
 ## 16. Planned follow-ups
 
-- Optional Search UX improvements:
-  - grouping results by date / branch
-  - optional date-only search mode (if desired)
-
-- Optional Explorer improvements:
-  - add “Herunterladen” action
-  - optional in-app PDF viewer
-
-- Password reset / recovery:
-  - separate follow-up ticket/phase (not part of RHL-009).
+- User management UI + APIs (RHL-012):
+  - Guard endpoints using `requireUserManagement(session)` (backend).
+  - Gate UI screens using `canManageUsers(role)` (frontend).