# Frontend API Usage (v1)

This document is the **frontend-facing** single source of truth for consuming the RHL Lieferscheine backend APIs.

Scope:

- Stable **API v1** contracts (URLs, params, response shapes).
- The minimal frontend `apiClient` helper layer (`lib/frontend/apiClient.js`).
- 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).
- Search **recent history** behavior (frontend-only storage policy, RHL-042).
- Password change for authenticated users (RHL-009).
- Must-change-password enforcement flow (RHL-044).
- 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`**.
>
> For UI navigation, prefer centralized route builders from `lib/frontend/routes.js` instead of hardcoding path strings.

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:

- 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).
- The Search recent-history dropdown is frontend-only (`localStorage`) and does not require backend changes (RHL-042).
- The protected UI enforces `mustChangePassword` via centralized auth gating (RHL-044).
- User management endpoints are implemented under `/api/admin/users` and are guarded by the user-management capability (RHL-041 + RHL-012).

---

## 1. Quickstart

### 1.1 Recommended calling pattern

For the first UI, prefer calling the backend from **browser/client-side code** so the HTTP-only session cookie is naturally sent with requests.

Key fetch requirements:

- Always send cookies: `credentials: "include"`
- Always request fresh data: `cache: "no-store"`

The project provides a tiny wrapper to enforce those defaults: `lib/frontend/apiClient.js`.

### 1.2 Happy-path navigation flow

The core Explorer UI flow is a simple drill-down:

1. `login({ username, password })`
2. `getMe()` (optional but recommended)
3. `getBranches()`
4. `getYears(branch)`
5. `getMonths(branch, year)`
6. `getDays(branch, year, month)`
7. `getFiles(branch, year, month, day)`
8. Open a PDF via the binary endpoint (see section 4.3)

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)

### 1.3 Example usage (client-side)

```js
import {
	login,
	getMe,
	getBranches,
	getYears,
	getMonths,
	getDays,
	getFiles,
	ApiClientError,
} from "@/lib/frontend/apiClient";

export async function runExampleFlow() {
	try {
		await login({ username: "nl01user", password: "secret" });

		const me = await getMe();
		if (!me.user) throw new Error("Expected to be logged in");

		const { branches } = await getBranches();
		const branch = branches[0];

		const { years } = await getYears(branch);
		const year = years[years.length - 1];

		const { months } = await getMonths(branch, year);
		const month = months[months.length - 1];

		const { days } = await getDays(branch, year, month);
		const day = days[days.length - 1];

		const { files } = await getFiles(branch, year, month, day);
		return { branch, year, month, day, files };
	} catch (err) {
		if (err instanceof ApiClientError) {
			// Use err.code for UI decisions.
			// Example: AUTH_UNAUTHENTICATED -> redirect to login
			throw err;
		}
		throw err;
	}
}
```

### 1.4 Frontend route helpers (UI navigation)

File:

- `lib/frontend/routes.js`

Purpose:

- Centralize URL building so UI code does not scatter hardcoded strings.
- Encode dynamic segments defensively.

Example:

```js
import {
	branchPath,
	dayPath,
	searchPath,
	loginPath,
} from "@/lib/frontend/routes";

const loginUrl = loginPath();
const branchUrl = branchPath("NL01");
const dayUrl = dayPath("NL01", "2025", "12", "31");
const searchUrl = searchPath("NL01");
```

---

## 2. The `apiClient` helper

File:

- `lib/frontend/apiClient.js`

Design goals:

- Enforce `credentials: "include"` and `cache: "no-store"`.
- Parse JSON automatically.
- Convert standardized backend errors into a single error type: `ApiClientError`.

### 2.1 `ApiClientError`

When the backend returns the standardized error payload:

```json
{ "error": { "message": "...", "code": "...", "details": {} } }
```

…the client throws:

- `name = "ApiClientError"`
- `status` (HTTP status)
- `code` (machine-readable error code)
- `message` (safe human-readable message)
- optional `details`

### 2.2 Provided helpers

Auth:

- `login({ username, password })`
- `logout()`
- `getMe()`
- `changePassword({ currentPassword, newPassword })` (RHL-009)

Navigation:

- `getBranches()`
- `getYears(branch)`
- `getMonths(branch, year)`
- `getDays(branch, year, month)`

Files:

- `getFiles(branch, year, month, day)`

Search:

- `search({ q, branch, scope, branches, from, to, limit, cursor })`

Admin user management (RHL-012 + RHL-043):

- `adminListUsers({ q, role, branchId, sort, limit, cursor })`
- `adminCreateUser({ username, email, role, branchId, initialPassword })`
- `adminUpdateUser(userId, patch)`
- `adminDeleteUser(userId)`
- `adminResetUserPassword(userId)`

Low-level:

- `apiFetch(path, options)`

### 2.3 Server-side usage note (Node / scripts)

Node’s `fetch` does **not** include a cookie jar automatically.

For manual verification we provide a script that includes a minimal cookie jar:

- `scripts/manual-api-client-flow.mjs`

For server checks, run it **inside the app container**:

```bash
docker compose exec app node scripts/manual-api-client-flow.mjs \
  --baseUrl=http://127.0.0.1:3000 \
  --username=<user> \
  --password=<pw> \
  --branch=NL01
```

---

## 3. API v1 conventions

### 3.1 Identifiers

- `branch`: `NL01`, `NL02`, ...
- `year`: `"YYYY"` (4 digits)
- `month`: `"MM"` (2 digits, `01`–`12`)
- `day`: `"DD"` (2 digits, `01`–`31`)

### 3.2 Sorting

Current server responses are sorted **ascending**:

- branches: ascending by branch number (`NL01`, `NL02`, ...)
- years/months/days: numeric ascending
- files: lexicographic ascending by file name

If the UI needs “newest first”, reverse the arrays in the UI.

### 3.3 File entries

`getFiles()` returns:

```json
{
	"files": [{ "name": "...pdf", "relativePath": "NL01/2025/12/19/...pdf" }]
}
```

Notes:

- `relativePath` is **relative to `NAS_ROOT_PATH`** inside the container.
- The PDF stream endpoint uses **route segments** (`branch/year/month/day/filename`).
- For v1 UI usage, treat `files[].name` as the canonical filename and build the stream URL from the current Explorer route segments.

---

## 4. Endpoint contracts (v1)

All routes are served under `/api`.

### 4.1 Auth

#### `POST /api/auth/login`

Body:

```json
{ "username": "example.user", "password": "plain" }
```

Success:

```json
{ "ok": true }
```

Errors:

- `400 VALIDATION_*`
- `401 AUTH_INVALID_CREDENTIALS`

#### `GET /api/auth/logout`

Success:

```json
{ "ok": true }
```

#### `GET /api/auth/me`

Success (authenticated):

```json
{
	"user": {
		"userId": "...",
		"role": "branch|admin|superadmin|dev",
		"branchId": "NL01",
		"email": "nl01@example.com",
		"mustChangePassword": true
	}
}
```

Notes:

- `email` is optional and may be `null`.
- `mustChangePassword` is always returned as a boolean.

Success (unauthenticated):

```json
{ "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.
- On success, the backend refreshes the session cookie with `mustChangePassword=false`.

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, clear form state, and revalidate session
		// (e.g., call getMe() or trigger the central auth retry).
		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.

#### `mustChangePassword` frontend flow (RHL-044)

Recommended behavior:

1. Read `user.mustChangePassword` from `getMe()`.
2. If `true`, gate protected routes and redirect to `/profile`.
3. Allow password change only on `/profile`.
4. After successful `changePassword(...)`, revalidate auth state (`getMe`) so the gate lifts immediately.
5. Optional UX: preserve and resume a previous target route (`next`) after the gate is lifted.

### 4.2 Branch navigation

All endpoints below require a valid session.

#### `GET /api/branches`

Success:

```json
{ "branches": ["NL01", "NL02"] }
```

RBAC:

- `branch` role: returns only its own branch
- `admin`/`superadmin`/`dev`: returns all branches

#### `GET /api/branches/:branch/years`

Success:

```json
{ "branch": "NL01", "years": ["2024", "2025"] }
```

#### `GET /api/branches/:branch/:year/months`

Success:

```json
{ "branch": "NL01", "year": "2025", "months": ["01", "02"] }
```

#### `GET /api/branches/:branch/:year/:month/days`

Success:

```json
{ "branch": "NL01", "year": "2025", "month": "12", "days": ["18", "19"] }
```

### 4.3 Files

#### `GET /api/files?branch=&year=&month=&day=`

Success:

```json
{
	"branch": "NL01",
	"year": "2025",
	"month": "12",
	"day": "19",
	"files": [{ "name": "test.pdf", "relativePath": "NL01/2025/12/19/test.pdf" }]
}
```

#### `GET /api/files/:branch/:year/:month/:day/:filename`

This endpoint returns **binary PDF data** on the happy path (not JSON).

Frontend rules:

- **Do not call this endpoint via `apiClient.apiFetch()`**.
  - `apiClient` is JSON-centric and will try to parse the response.

- Prefer opening the endpoint URL in a **new tab** so the browser handles PDF rendering.

##### 4.3.1 Centralized URL builder

File:

- `lib/frontend/explorer/pdfUrl.js`

Exports:

- `buildPdfUrl({ branch, year, month, day, filename })`
- `buildPdfDownloadUrl({ branch, year, month, day, filename })` (adds `?download=1`)

Why it exists:

- Keeps URL construction consistent.
- Ensures the `filename` segment is encoded correctly.

##### 4.3.2 Recommended UI usage pattern

In the Explorer (and Search results table), we open PDFs via navigation:

- Use a normal anchor element with `target="_blank"`.
- This avoids popup-blocker issues and is semantically correct.

Example:

```js
import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";

const href = buildPdfUrl({ branch, year, month, day, filename });

// In JSX:
// <a href={href} target="_blank" rel="noopener noreferrer">Öffnen</a>
```

Force download:

```js
import { buildPdfDownloadUrl } from "@/lib/frontend/explorer/pdfUrl";

const href = buildPdfDownloadUrl({ branch, year, month, day, filename });
```

##### 4.3.3 Important filename and cookie notes

- Use the exact `files[].name` returned by `getFiles()` (case-sensitive on Linux).

- Filenames with special characters must be URL-encoded.
  - In particular, `#` **must** be encoded as `%23`.
    Otherwise the browser treats it as a fragment and the server receives a truncated filename.

- Host consistency matters for cookies:
  - If you are logged in on `http://localhost:3000`, also open the PDF on `http://localhost:3000`.
  - Switching to `http://127.0.0.1:3000` will not send the cookie (different host) and results in `401`.

### 4.4 Search

#### `GET /api/search`

This is a **JSON** endpoint.

In the frontend, prefer the dedicated wrapper:

- `apiClient.search(...)`

Query params:

- `q` (optional)

Optional filters:

- `scope`: `branch | all | multi`
- `branch`: single branch
- `branches`: comma-separated branch list (for `scope=multi`)
- `from`, `to`: `YYYY-MM-DD` (inclusive)
- `limit`: page size (default `100`, allowed `50..200`)
- `cursor`: pagination cursor returned by the previous response

Filter rule:

- The backend requires **at least one filter** to avoid accidental “match everything” searches:
  - `q` OR `from` OR `to`

If all three are missing, the API returns:

- `400 VALIDATION_SEARCH_MISSING_FILTER`

Response shape:

```json
{
	"items": [
		{
			"branch": "NL20",
			"date": "2025-12-18",
			"year": "2025",
			"month": "12",
			"day": "18",
			"filename": "...pdf",
			"relativePath": "NL20/2025/12/18/...pdf",
			"snippet": "..."
		}
	],
	"nextCursor": "<opaque>",
	"total": 123
}
```

Notes:

- `nextCursor` is `null` when there are no more results.
- `total` is the total number of matches for the current query and can be shown as “x of y loaded”.
- `total` may be `null` if the provider cannot provide a reliable total.

##### 4.4.1 Date filters (RHL-025)

The Search UI supports **optional** date filtering:

- `from` / `to` are inclusive ISO date strings (`YYYY-MM-DD`).
- `from === to` is valid (single-day filter).
- `from > to` is invalid and must be rejected.

Frontend responsibilities:

- Validate locally for fast feedback (UI should not send invalid date ranges).
- Treat backend validation as authoritative (backend may still return `VALIDATION_SEARCH_DATE` / `VALIDATION_SEARCH_RANGE`).

Relevant frontend helpers:

- `lib/frontend/search/dateRange.js` (pure ISO date helpers + German formatting)
- `lib/frontend/search/searchDateValidation.js` (canonical date-range validation)
- `lib/frontend/search/dateFilterValidation.js` (builds a local `ApiClientError` for UI rendering)

##### 4.4.2 URL sync (shareable)

For Search v1, the first-page identity is URL-driven (shareable).

- `q`, `scope`, `branches`, `limit`, `from`, `to` are part of the shareable state.
- `cursor` is intentionally **not** part of the shareable URL and stays in client state.

##### 4.4.3 Recent search history (frontend-only, RHL-042)

The recent-search feature is intentionally frontend-only and stores compact first-page identities in browser `localStorage`.

Storage policy:

- Key format:
  - `rhl.searchHistory.v1.<userId>`
- Max entries:
  - `10` (LRU; reusing a search moves it to the top)
- Stored fields:
  - `routeBranch`, `q`, `scope`, `branches`, `limit`, `from`, `to`, `createdAt`
- Not stored:
  - `cursor`

Write trigger:

- Persist only after a successful first-page search response.
- Do not persist keystrokes or draft query changes.

Restore rule:

- Build an internal `/:branch/search` href from the stored entry.
- Keep shareable first-page params only.
- Never restore pagination cursor from history.

##### 4.4.4 Recommended usage (client-side)

```js
import { search, ApiClientError } from "@/lib/frontend/apiClient";

export async function searchDeliveryNotesExample() {
	try {
		const res = await search({
			q: "bridgestone",
			branch: "NL20",
			from: "2025-12-01",
			to: "2025-12-31",
			limit: 100,
		});

		return {
			items: res.items,
			nextCursor: res.nextCursor,
			total: res.total,
		};
	} catch (err) {
		if (err instanceof ApiClientError) {
			// Example:
			// - AUTH_UNAUTHENTICATED -> redirect to login
			// - AUTH_FORBIDDEN_BRANCH -> show forbidden
			// - VALIDATION_* -> show a friendly input message
		}
		throw err;
	}
}
```

Date-range-only example (no `q`):

- The API allows this as long as at least one of `from` / `to` is provided.
- The current v1 UI intentionally requires `q` to trigger a search to avoid accidental broad queries.

```js
import { search } from "@/lib/frontend/apiClient";

export async function searchByDateRangeOnly() {
	const res = await search({
		scope: "all",
		from: "2025-12-01",
		to: "2025-12-31",
		limit: 100,
	});

	return res;
}
```

Using a hit to navigate/open:

- Navigate to the day folder:
  - `dayPath(hit.branch, hit.year, hit.month, hit.day)`

- Open the PDF:
  - `buildPdfUrl({ branch: hit.branch, year: hit.year, month: hit.month, day: hit.day, filename: hit.filename })`

Pagination:

- Store `nextCursor` from the response.
- Pass it back as `cursor=` for the next page.
- Treat the cursor as opaque.

RBAC note:

- Branch users will only get results for their own branch.
- Admin/superadmin/dev users can use `scope=all` or `scope=multi` for cross-branch search.

### 4.5 Admin: User management (RHL-012 + RHL-043)

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, sort, limit, cursor })`

Sort modes:

- `default`
- `role_rights`
- `branch_asc`

Example:

```js
import { adminListUsers } from "@/lib/frontend/apiClient";

export async function loadFirstPage() {
	return adminListUsers({
		q: "max",
		role: "branch",
		branchId: "NL01",
		sort: "role_rights",
		limit: 50,
	});
}
```

Response shape:

```json
{ "items": [{ "id": "...", "username": "..." }], "nextCursor": "..." }
```

Notes:

- Pagination is cursor-based. Keep `nextCursor` in client state.
- Cursors are mode-aware. Do not reuse a cursor across different `sort` modes.

#### 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`.

#### 4.5.5 Reset temporary password

Use:

- `adminResetUserPassword(userId)`

Behavior:

- Resets the target user password on the server.
- Returns a temporary plaintext password in the response payload.
- Sets `mustChangePassword=true` for the target user.

Safety rules:

- Resetting your own currently authenticated user via this endpoint is blocked:
  - `400 VALIDATION_INVALID_FIELD`
  - `details.reason = "SELF_PASSWORD_RESET_FORBIDDEN"`

---

## 5. Error handling

### 5.1 Standard error payload

All JSON error responses use:

```json
{
	"error": {
		"message": "Human readable message",
		"code": "SOME_MACHINE_CODE",
		"details": {}
	}
}
```

### 5.2 Common codes used by the UI

Auth:

- `AUTH_UNAUTHENTICATED`
- `AUTH_INVALID_CREDENTIALS`
- `AUTH_FORBIDDEN_BRANCH`
- `AUTH_FORBIDDEN_USER_MANAGEMENT`

Password management:

- `VALIDATION_WEAK_PASSWORD`

Validation:

- `VALIDATION_MISSING_PARAM`
- `VALIDATION_MISSING_QUERY`
- `VALIDATION_INVALID_JSON`
- `VALIDATION_INVALID_BODY`
- `VALIDATION_MISSING_FIELD`
- `VALIDATION_INVALID_FIELD`

Search validation:

- `VALIDATION_SEARCH_SCOPE`
- `VALIDATION_SEARCH_BRANCH`
- `VALIDATION_SEARCH_BRANCHES`
- `VALIDATION_SEARCH_DATE`
- `VALIDATION_SEARCH_RANGE`
- `VALIDATION_SEARCH_LIMIT`
- `VALIDATION_SEARCH_CURSOR`
- `VALIDATION_SEARCH_MISSING_FILTER`

User management:

- `USER_NOT_FOUND`

Storage:

- `FS_NOT_FOUND`
- `FS_STORAGE_ERROR`

Internal:

- `INTERNAL_SERVER_ERROR`

---

## 6. Caching & freshness

The backend reads from a NAS where new scans can appear at any time.

Backend rules:

- All route handlers are `dynamic = "force-dynamic"`.
- All JSON responses include `Cache-Control: no-store`.
- A small process-local TTL cache exists in `lib/storage.js`:
  - branches/years: 60s
  - months/days/files: 15s

Frontend guidance:

- Use `credentials: "include"` and `cache: "no-store"`.
- Do not rely on Next.js ISR/revalidate for these endpoints.

---

## 7. Manual verification (RHL-008)

The repository contains a manual smoke test script that exercises:

- authentication
- drill-down navigation (branches -> years -> months -> days -> files)
- negative cases (401/403/400/404)

Script:

- `scripts/manual-api-client-flow.mjs`

Local:

```bash
node scripts/manual-api-client-flow.mjs \
  --baseUrl=http://localhost:3000 \
  --username=<user> \
  --password=<pw> \
  --branch=NL01
```

Server (recommended from within container):

```bash
docker compose exec app node scripts/manual-api-client-flow.mjs \
  --baseUrl=http://127.0.0.1:3000 \
  --username=<user> \
  --password=<pw> \
  --branch=NL01
```

---

## 8. API v1 freeze policy

As of RHL-008, the endpoints and response shapes documented here are considered **API v1**.

Rules:

- Avoid breaking changes to existing URLs, parameters, or response fields.

- Prefer additive changes:
  - add new endpoints
  - add optional fields

- If a breaking change becomes necessary, introduce a new endpoint rather than modifying the existing contract.

---

## 9. Out of scope / planned additions

- **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.

- Optional Search UX improvements:
  - grouping results by date / branch
  - debounced search (optional; current v1 is explicit submit)

- 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 and non-leakage), external SMTP/IT dependencies, and separate rate limiting ticket (RHL-031) to avoid scope creep.
