Răsfoiți Sursa

feat(auth): add password change endpoint and update documentation

Code_Uwe 6 zile în urmă
părinte
comite
c36089675f
4 a modificat fișierele cu 437 adăugiri și 182 ștergeri
  1. 122 142
      Docs/api.md
  2. 114 25
      Docs/auth.md
  3. 81 0
      Docs/frontend-api-usage.md
  4. 120 15
      Docs/frontend-ui.md

+ 122 - 142
Docs/api.md

@@ -1,23 +1,3 @@
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Ordner: Docs -->
-
-<!-- Datei: api.md -->
-
-<!-- Relativer Pfad: Docs/api.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Ordner: docs -->
-
-<!-- Datei: api.md -->
-
-<!-- Relativer Pfad: docs/api.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
 # API Overview
 
 This document describes the HTTP API exposed by the application using Next.js **Route Handlers** in the App Router (`app/api/*/route.js`).
@@ -47,19 +27,16 @@ Optional environment variables:
 The Search API can run with different provider backends.
 
 - `SEARCH_PROVIDER` (optional)
-
   - 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).
 
 If `SEARCH_PROVIDER=qsirch`, these variables are required:
 
 - `QSIRCH_BASE_URL` — base URL of the Qsirch service (must be reachable from inside the app container).
-
   - Example: `http://192.168.0.22:8080`
 
 - `QSIRCH_ACCOUNT` — QTS/Qsirch account used for server-to-server search.
@@ -67,23 +44,19 @@ If `SEARCH_PROVIDER=qsirch`, these variables are required:
 - `QSIRCH_PASSWORD` — password for the account.
 
 - `QSIRCH_PATH_PREFIX` — path prefix that contains the branch folders.
-
   - Example: `/Niederlassungen`
 
 Optional Qsirch tuning:
 
 - `QSIRCH_DATE_FIELD`
-
   - Allowed values: `modified` | `created` (case-insensitive)
   - Default: `modified`
 
 - `QSIRCH_MODE`
-
   - Allowed values: `sync` | `async` | `auto` (case-insensitive)
   - Default: `sync`
 
   Notes:
-
   - The current implementation is **sync-first**.
   - `auto` currently behaves like `sync` (placeholder for a later async implementation).
 
@@ -169,7 +142,7 @@ Rules for such endpoints:
 The API uses the following status codes consistently:
 
 - `400` — invalid/missing parameters, validation errors
-- `401` — unauthenticated (missing/invalid session) or invalid login credentials
+- `401` — unauthenticated (missing/invalid session) or invalid credentials
 - `403` — authenticated but not allowed (RBAC / branch mismatch)
 - `404` — resource not found (branch/year/month/day/file does not exist)
 - `500` — unexpected server errors (internal failures)
@@ -179,21 +152,21 @@ The API uses the following status codes consistently:
 The API uses these machine-readable codes (non-exhaustive list):
 
 - Auth:
-
   - `AUTH_UNAUTHENTICATED`
   - `AUTH_INVALID_CREDENTIALS`
   - `AUTH_FORBIDDEN_BRANCH`
 
 - Validation (generic):
-
   - `VALIDATION_MISSING_PARAM`
   - `VALIDATION_MISSING_QUERY`
   - `VALIDATION_INVALID_JSON`
   - `VALIDATION_INVALID_BODY`
   - `VALIDATION_MISSING_FIELD`
 
-- Validation (filesystem route params):
+- Validation (password management):
+  - `VALIDATION_WEAK_PASSWORD`
 
+- Validation (filesystem route params):
   - `VALIDATION_BRANCH`
   - `VALIDATION_YEAR`
   - `VALIDATION_MONTH`
@@ -203,7 +176,6 @@ The API uses these machine-readable codes (non-exhaustive list):
   - `VALIDATION_PATH_TRAVERSAL`
 
 - Validation (search):
-
   - `VALIDATION_SEARCH_SCOPE`
   - `VALIDATION_SEARCH_BRANCH`
   - `VALIDATION_SEARCH_BRANCHES`
@@ -214,16 +186,13 @@ The API uses these machine-readable codes (non-exhaustive list):
   - `VALIDATION_SEARCH_MISSING_FILTER`
 
 - Storage:
-
   - `FS_NOT_FOUND`
   - `FS_STORAGE_ERROR`
 
 - Search (backend/provider):
-
   - `SEARCH_BACKEND_UNAVAILABLE` (provider misconfiguration/unavailability)
 
 - Internal:
-
   - `INTERNAL_SERVER_ERROR`
 
 ### 3.4 Implementation notes
@@ -293,7 +262,6 @@ Frontend code should call these endpoints with explicit “fresh data” setting
   ```
 
 - Do not rely on `next: { revalidate: ... }` for these endpoints. Freshness is controlled via:
-
   - `Cache-Control: no-store` (HTTP)
   - server-side storage TTL micro-cache
 
@@ -433,7 +401,113 @@ This avoids using 401 as control-flow for basic "am I logged in?" checks.
 
 ---
 
-### 4.5 `GET /api/branches`
+### 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"
+  	}
+  }
+  ```
+
+---
+
+### 4.6 `GET /api/branches`
 
 Returns the list of branches (e.g. `["NL01", "NL02"]`).
 
@@ -468,7 +542,7 @@ Returns the list of branches (e.g. `["NL01", "NL02"]`).
 
 ---
 
-### 4.6 `GET /api/branches/[branch]/years`
+### 4.7 `GET /api/branches/[branch]/years`
 
 Example: `/api/branches/NL01/years`
 
@@ -482,7 +556,7 @@ Example: `/api/branches/NL01/years`
 
 ---
 
-### 4.7 `GET /api/branches/[branch]/[year]/months`
+### 4.8 `GET /api/branches/[branch]/[year]/months`
 
 Example: `/api/branches/NL01/2024/months`
 
@@ -496,7 +570,7 @@ Example: `/api/branches/NL01/2024/months`
 
 ---
 
-### 4.8 `GET /api/branches/[branch]/[year]/[month]/days`
+### 4.9 `GET /api/branches/[branch]/[year]/[month]/days`
 
 Example: `/api/branches/NL01/2024/10/days`
 
@@ -510,7 +584,7 @@ Example: `/api/branches/NL01/2024/10/days`
 
 ---
 
-### 4.9 `GET /api/files?branch=&year=&month=&day=`
+### 4.10 `GET /api/files?branch=&year=&month=&day=`
 
 Example:
 
@@ -534,7 +608,7 @@ Example:
 
 ---
 
-### 4.10 `GET /api/files/:branch/:year/:month/:day/:filename`
+### 4.11 `GET /api/files/:branch/:year/:month/:day/:filename`
 
 **Purpose**
 
@@ -558,7 +632,6 @@ Stream (or download) a single PDF file from the NAS while enforcing authenticati
 **Query params (optional)**
 
 - `download=1` or `download=true`
-
   - Forces `Content-Disposition: attachment` (download)
   - Default is `inline` (open in browser)
 
@@ -566,91 +639,22 @@ Stream (or download) a single PDF file from the NAS while enforcing authenticati
 
 - Body: raw PDF bytes (not JSON)
 - Headers (example):
-
   - `Content-Type: application/pdf`
-
   - `Cache-Control: no-store`
 
-  - `Content-Disposition`:
-
-    - Default: `inline`
-    - When `download=1`: `attachment`
-
-    For filename handling (Unicode-safe):
-
-    - `filename="..."` is an ASCII fallback (safe for header values)
-    - `filename*=UTF-8''...` contains the UTF-8 encoded original name
-
-    Example:
-
-    ```text
-    Content-Disposition: inline; filename="Euro _.pdf"; filename*=UTF-8''Euro%20%E2%82%AC.pdf
-    ```
-
 **Error responses (JSON)**
 
 Even though the happy path is binary, error responses remain standardized JSON.
 
-Common error codes:
-
-- `400` validation errors:
-
-  - `VALIDATION_MISSING_PARAM`
-  - `VALIDATION_BRANCH`
-  - `VALIDATION_YEAR`
-  - `VALIDATION_MONTH`
-  - `VALIDATION_DAY`
-  - `VALIDATION_FILENAME`
-  - `VALIDATION_FILE_EXTENSION`
-  - `VALIDATION_PATH_TRAVERSAL`
-
-- `401`
-
-  ```json
-  { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
-  ```
-
-- `403`
-
-  ```json
-  { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } }
-  ```
-
-- `404` (file not found)
-
-  ```json
-  {
-  	"error": {
-  		"message": "Not found",
-  		"code": "FS_NOT_FOUND",
-  		"details": {
-  			"branch": "NL01",
-  			"year": "2024",
-  			"month": "10",
-  			"day": "23",
-  			"filename": "example.pdf"
-  		}
-  	}
-  }
-  ```
-
-- `500`
-
-  ```json
-  {
-  	"error": { "message": "Internal server error", "code": "FS_STORAGE_ERROR" }
-  }
-  ```
-
 ---
 
-### 4.11 `GET /api/search`
+### 4.12 `GET /api/search`
 
 **Purpose**
 
 Search delivery note content across PDFs.
 
-The endpoint returns _search hits_ with enough metadata to:
+The endpoint returns search hits with enough metadata to:
 
 - navigate to the correct Explorer day folder
 - open the PDF via the binary file endpoint
@@ -660,59 +664,48 @@ The endpoint returns _search hits_ with enough metadata to:
 **RBAC behavior**
 
 - `branch` role:
-
   - results are limited to the user’s `branchId`
   - attempting to query other branches may return `403 AUTH_FORBIDDEN_BRANCH`
 
 - `admin`/`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:
+To avoid accidental "match everything" queries (especially dangerous in `scope=all`), the backend requires at least one of:
 
-- at least one of:
-
-  - `q`
-  - `from`
-  - `to`
+- `q`
+- `from`
+- `to`
 
 If all three are missing, the API returns:
 
@@ -739,19 +732,6 @@ If all three are missing, the API returns:
 }
 ```
 
-Notes:
-
-- Result order is typically relevance-based.
-- `nextCursor` is `null` when there are no more results.
-- `total` is the total number of matching items for the current query. It may be `null` if the backend/provider cannot provide a reliable total.
-
-**Error responses**
-
-- `400` for missing/invalid parameters (returns a `VALIDATION_*` code)
-- `401` for unauthenticated access
-- `403` for branch violations (`AUTH_FORBIDDEN_BRANCH`)
-- `500` for internal search/index failures (`INTERNAL_SERVER_ERROR` or `SEARCH_BACKEND_UNAVAILABLE`)
-
 ---
 
 ## 5. API v1 freeze (RHL-008)

+ 114 - 25
Docs/auth.md

@@ -1,15 +1,3 @@
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- Ordner: Docs -->
-
-<!-- Datei: auth.md -->
-
-<!-- Relativer Pfad: Docs/auth.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
 # Authentication & Authorization
 
 This document describes the authentication and authorization model for the internal delivery note browser.
@@ -21,8 +9,6 @@ The system uses:
 - Role-aware access control (`branch`, `admin`, `dev`).
 - Branch-level RBAC enforcement for filesystem-related APIs.
 
-> NOTE: This document is a living document. As we extend the auth system (sessions, routes, policies, password flows), we will update this file.
-
 ---
 
 ## 1. Goals & Scope
@@ -40,7 +26,12 @@ This document covers:
 - Environment variables related to auth.
 - Roles and RBAC rules.
 - Session payload and cookie configuration.
-- Login/logout/me endpoints.
+- Auth endpoints (login/logout/me).
+- Password change (authenticated users).
+
+Non-goals (for this document):
+
+- Email-based password recovery (documented as a follow-up ticket/phase).
 
 ---
 
@@ -51,7 +42,6 @@ This document covers:
 Auth depends on the following environment variables:
 
 - `SESSION_SECRET` (required)
-
   - Strong, random string used to sign and verify JWT session tokens.
   - Minimum length: **32 characters**.
   - Must be kept secret.
@@ -64,7 +54,6 @@ Auth endpoints also require DB connectivity:
 ### 2.2 Optional variables
 
 - `SESSION_COOKIE_SECURE` (optional)
-
   - Overrides the `Secure` cookie flag.
   - Allowed values: `true` or `false`.
 
@@ -74,7 +63,7 @@ Default behavior:
 
 Local HTTP testing (e.g. `http://localhost:3000` with Docker + `next start`):
 
-- Set `SESSION_COOKIE_SECURE=false` in your local `.env.docker`.
+- Set `SESSION_COOKIE_SECURE=false` in your local docker env file.
 
 Staging/Production:
 
@@ -105,7 +94,6 @@ node scripts/validate-env.mjs && npm run start
 - Represents a user who belongs to a specific branch/location.
 - Must have a valid `branchId` (e.g. `"NL01"`).
 - Intended access pattern:
-
   - Can only access delivery notes for their own branch.
   - Cannot access other branches.
 
@@ -114,7 +102,6 @@ node scripts/validate-env.mjs && npm run start
 - Administrator account.
 - Typically not bound to any single branch (`branchId = null`).
 - Intended access pattern:
-
   - Can access delivery notes across all branches.
 
 ### 3.3 `dev`
@@ -122,7 +109,6 @@ node scripts/validate-env.mjs && npm run start
 - Development/engineering account.
 - Typically not bound to any single branch (`branchId = null`).
 - Intended access pattern:
-
   - Full or near-full access.
 
 ---
@@ -226,7 +212,9 @@ Implementation lives in `lib/auth/session.js`:
 
 ---
 
-## 6. Core Auth Endpoints
+## 6. Auth Endpoints
+
+All endpoints below are implemented as Next.js App Router Route Handlers in `app/api/**/route.js`.
 
 ### 6.1 `POST /api/auth/login`
 
@@ -316,10 +304,111 @@ Security note:
 
 ---
 
-## 7. Security Notes
+## 7. Password Management
+
+### 7.1 Password storage
+
+- Passwords are stored as a `bcrypt` hash in `users.passwordHash`.
+- The API never returns `passwordHash`.
+
+### 7.2 Password policy (current)
+
+The password policy is intentionally explicit and testable.
+
+Current policy:
+
+- Minimum length: **8** characters
+- Must contain at least **1 letter** (`A–Z`)
+- Must contain at least **1 number** (`0–9`)
+- New password must be different from the current password
+
+The canonical policy implementation lives in `lib/auth/passwordPolicy.js`.
+
+### 7.3 `POST /api/auth/change-password` (RHL-009)
+
+Change the password for the currently authenticated user.
+
+Authentication:
+
+- Requires a valid session cookie.
+- If no session exists: `401 AUTH_UNAUTHENTICATED`.
+
+Request body (JSON):
+
+```json
+{
+	"currentPassword": "<string>",
+	"newPassword": "<string>"
+}
+```
+
+Response:
+
+- `200 { "ok": true }`
+
+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`.
+
+3. Verify that `currentPassword` matches `passwordHash`.
+   - If mismatch: return `401 AUTH_INVALID_CREDENTIALS`.
+
+4. Validate `newPassword` against the password policy.
+   - 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:
+
+- `mustChangePassword`
+- `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.
+
+---
+
+## 8. Security Notes
 
 - 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).

+ 81 - 0
Docs/frontend-api-usage.md

@@ -9,6 +9,7 @@ Scope:
 - Practical examples for building the UI.
 - 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).
 
 > UI developers: For the app shell layout and frontend route scaffold (public vs protected routes, placeholder pages), see **`docs/frontend-ui.md`**.
 >
@@ -18,6 +19,7 @@ Non-goals:
 
 - New major features.
 - An in-app PDF viewer UI (beyond opening the browser’s PDF viewer in a new tab).
+- Email-based password recovery (separate follow-up ticket/phase).
 
 Notes:
 
@@ -58,6 +60,10 @@ Optional (admin/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 (authenticated users):
+
+- change password via `changePassword({ currentPassword, newPassword })` (RHL-009)
+
 ### 1.3 Example usage (client-side)
 
 ```js
@@ -168,6 +174,7 @@ Auth:
 - `login({ username, password })`
 - `logout()`
 - `getMe()`
+- `changePassword({ currentPassword, newPassword })` (RHL-009)
 
 Navigation:
 
@@ -292,6 +299,72 @@ Success (unauthenticated):
 { "user": null }
 ```
 
+#### `POST /api/auth/change-password` (RHL-009)
+
+Body:
+
+```json
+{ "currentPassword": "<string>", "newPassword": "<string>" }
+```
+
+Success:
+
+```json
+{ "ok": true }
+```
+
+Error codes (common):
+
+- `401 AUTH_UNAUTHENTICATED` (no/invalid session)
+- `401 AUTH_INVALID_CREDENTIALS` (wrong current password)
+- `400 VALIDATION_MISSING_FIELD` (missing `currentPassword` / `newPassword`)
+- `400 VALIDATION_WEAK_PASSWORD` (policy violation)
+
+Weak password details:
+
+- The backend returns structured `details` for `VALIDATION_WEAK_PASSWORD`, including `minLength`, `requireLetter`, `requireNumber`, `disallowSameAsCurrent`, and `reasons`.
+- Frontend UIs should use these fields to show actionable user feedback.
+
+Example UI usage:
+
+```js
+import { changePassword, ApiClientError } from "@/lib/frontend/apiClient";
+
+export async function changePasswordExample() {
+	try {
+		await changePassword({
+			currentPassword: "old-password",
+			newPassword: "NewPassw0rd",
+		});
+
+		// Success UI: show a toast and clear form state
+		return { ok: true };
+	} catch (err) {
+		if (err instanceof ApiClientError) {
+			if (err.code === "AUTH_UNAUTHENTICATED") {
+				// redirect to /login?reason=expired&next=...
+			}
+
+			if (err.code === "AUTH_INVALID_CREDENTIALS") {
+				// show: “Current password is wrong”
+			}
+
+			if (err.code === "VALIDATION_WEAK_PASSWORD") {
+				// show hints based on err.details.reasons
+				// err.details.minLength etc.
+			}
+		}
+
+		throw err;
+	}
+}
+```
+
+Security note:
+
+- Never log passwords.
+- Avoid storing passwords in persistent client state.
+
 ### 4.2 Branch navigation
 
 All endpoints below require a valid session.
@@ -596,6 +669,10 @@ Auth:
 - `AUTH_INVALID_CREDENTIALS`
 - `AUTH_FORBIDDEN_BRANCH`
 
+Password management:
+
+- `VALIDATION_WEAK_PASSWORD`
+
 Validation:
 
 - `VALIDATION_MISSING_PARAM`
@@ -709,3 +786,7 @@ Rules:
 - Optional Explorer UX polish:
   - add a dedicated “Herunterladen” UI action (download variant)
   - optional in-app PDF viewer experience (instead of a new tab)
+
+- Password reset / recovery:
+  - The reset flow (request/reset via token/email) is intentionally planned as a separate follow-up ticket/phase.
+  - Rationale: higher security surface (token handling, non-leakage), external SMTP/IT dependencies, and separate rate limiting ticket (RHL-031) to avoid scope creep.

+ 120 - 15
Docs/frontend-ui.md

@@ -1,4 +1,4 @@
-# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, and Navigation Polish (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032)
+# 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)
 
 This document describes the **frontend routing scaffold**, the **application shell layout**, and the core UI modules for the RHL Lieferscheine app.
 
@@ -13,6 +13,7 @@ Timeline:
 - **RHL-025**: Search date range filter (from/to) with a calendar popover, presets, local validation, and URL sync.
 - **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).
 
 > **Language policy**
 >
@@ -24,7 +25,7 @@ Timeline:
 
 ## 1. Scope
 
-### 1.1 Implemented (as of RHL-037 + RHL-025 + RHL-032)
+### 1.1 Implemented (as of RHL-037 + RHL-025 + RHL-032 + RHL-009)
 
 - **Public** `/login` route with a functional login form (shadcn/ui primitives).
 
@@ -44,6 +45,19 @@ Timeline:
 - **Logout**:
   - Logout action calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
 
+- **User dropdown menu (RHL-032)**:
+  - Profile / Support / Logout
+
+- **Profile password change (RHL-009)**:
+  - Route: `/profile` (protected)
+  - Password change form calls `apiClient.changePassword({ currentPassword, newPassword })`.
+  - Uses inline validation for actionable form errors.
+  - Uses toast notifications (Sonner) for success/error feedback.
+
+- **Global toast notifications (Sonner)**:
+  - 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.
+
 - **UI RBAC (branch-level)**:
   - `BranchGuard` prevents branch users from accessing other branches’ URLs.
   - Admin/dev can access multiple branches.
@@ -103,14 +117,18 @@ Timeline:
 
 ### 1.2 Still out of scope / planned
 
+- 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.
+
 - Optional Search UX improvements:
   - grouping results by date and/or branch
   - debounced “typeahead” search (current v1 is explicit submit)
   - optional **date-only search mode** (allow searches with `from/to` even when `q` is empty) if desired later
 
 - Optional Explorer improvements:
-  - “Herunterladen” action (download variant) next to “Öffnen”
-  - a dedicated in-app PDF viewer UI (instead of a new tab)
+  - add a dedicated “Herunterladen” UI action (download variant)
+  - optional in-app PDF viewer experience (instead of a new tab)
 
 ---
 
@@ -136,6 +154,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)              |
 | `/:branch`                   | Explorer: years             | Example: `/NL01`                                           |
 | `/:branch/:year`             | Explorer: months            | Example: `/NL01/2025`                                      |
 | `/:branch/:year/:month`      | Explorer: days              | Example: `/NL01/2025/12`                                   |
@@ -160,7 +179,9 @@ Responsibilities:
 - Global CSS imports (`app/globals.css`).
 - Theme provider setup (shadcn/ui + next-themes wrapper).
 - Base HTML/body structure.
-- **Icons / favicon** (RHL-032): metadata icons can be configured in the root layout.
+- Mount global UI providers:
+  - theme provider
+  - Sonner Toaster (global notifications)
 
 ### 3.2 Public layout
 
@@ -273,7 +294,7 @@ File: `components/app-shell/UserStatus.jsx`
 
 Menu items (German):
 
-- **Profil** → `/profile` (placeholder page until profile editing is implemented).
+- **Profil** → `/profile` (account info + password change)
 - **Support** → opens a `mailto:` link to `info@attus.de`.
 - **Abmelden** → calls logout and redirects to login.
 
@@ -346,6 +367,7 @@ File: `components/auth/AuthProvider.jsx`
 Behavior:
 
 1. On mount, call `apiClient.getMe()`.
+
 2. If `{ user: { ... } }`:
    - set auth state to `authenticated`
    - render protected UI
@@ -382,11 +404,13 @@ Files:
 Flow:
 
 1. Login page parses query params using `parseLoginParams(...)`.
+
 2. If `reason` is present:
    - `expired` → show “session expired” banner (German)
    - `logged-out` → show “logged out” banner (German)
 
 3. On submit, the form calls `apiClient.login({ username, password })`.
+
 4. On success:
    - redirect to `next` if present
    - otherwise redirect to `/`
@@ -583,7 +607,66 @@ Applied to:
 
 ---
 
-## 10. UI primitives (shadcn/ui)
+## 10. Toast notifications (Sonner)
+
+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)
+
+### 11.1 Route
+
+- `/profile` (protected)
+
+### 11.2 Components
+
+- `components/profile/ProfilePage.jsx`
+  - Renders:
+    - Account/session info card
+    - Email placeholder card
+    - Password change card
+
+- `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`
+
+---
+
+## 12. UI primitives (shadcn/ui)
 
 The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`.
 
@@ -614,7 +697,7 @@ Radix integration note:
 
 ---
 
-## 11. File Naming Convention (.js vs .jsx)
+## 13. File Naming Convention (.js vs .jsx)
 
 To keep the project consistent:
 
@@ -630,9 +713,9 @@ To keep the project consistent:
 
 ---
 
-## 12. Tests
+## 14. Tests
 
-### 12.1 Unit tests
+### 14.1 Unit tests
 
 Core tests:
 
@@ -675,7 +758,13 @@ Component SSR smoke test:
 
 - `components/app-shell/AppShell.test.js`
 
-### 12.2 Running tests
+Password change / toast helpers:
+
+- `lib/auth/passwordPolicy.test.js`
+- `lib/frontend/profile/passwordPolicyUi.test.js`
+- `lib/frontend/ui/toast.test.js`
+
+### 14.2 Running tests
 
 From the repo root:
 
@@ -691,9 +780,9 @@ npm run build
 
 ---
 
-## 13. Manual verification checklist
+## 15. Manual verification checklist
 
-### 13.1 Local (Docker)
+### 15.1 Local (Docker)
 
 Start:
 
@@ -712,6 +801,16 @@ Verify flows in the browser:
 - Logout
   - Expect redirect to `/login?reason=logged-out`
 
+Profile checks (RHL-009):
+
+- Open `/profile`
+  - Confirm account info is visible
+
+- 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).
@@ -743,7 +842,7 @@ Debounced loading UI (RHL-032):
 - With fast connections: skeleton flashes are minimized.
 - With throttling: skeletons appear after the delay and remain stable.
 
-### 13.2 Server
+### 15.2 Server
 
 Deploy and verify on the server URL.
 
@@ -757,9 +856,12 @@ Verify:
   - open PDF / jump to day
   - TopNav branch switching keeps deep links
 
+- Profile / password change:
+  - success and negative flows (wrong current password / weak password)
+
 ---
 
-## 14. Planned follow-ups
+## 16. Planned follow-ups
 
 - Optional Search UX improvements:
   - grouping results by date / branch
@@ -768,3 +870,6 @@ Verify:
 - Optional Explorer improvements:
   - add “Herunterladen” action
   - optional in-app PDF viewer
+
+- Password reset / recovery:
+  - separate follow-up ticket/phase (not part of RHL-009).