# 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). - 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`**. > > 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). --- ## 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/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 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 })` 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= \ --password= \ --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|dev", "branchId": "NL01", "email": "nl01@example.com" } } ``` Notes: - `email` is optional and may be `null`. Success (unauthenticated): ```json { "user": null } ``` #### `POST /api/auth/change-password` (RHL-009) Body: ```json { "currentPassword": "", "newPassword": "" } ``` 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. #### `GET /api/branches` Success: ```json { "branches": ["NL01", "NL02"] } ``` RBAC: - `branch` role: returns only its own branch - `admin`/`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: // Öffnen ``` 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": "", "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 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/dev users can use `scope=all` or `scope=multi` for cross-branch search. --- ## 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` Password management: - `VALIDATION_WEAK_PASSWORD` Validation: - `VALIDATION_MISSING_PARAM` - `VALIDATION_MISSING_QUERY` - `VALIDATION_INVALID_JSON` - `VALIDATION_INVALID_BODY` - `VALIDATION_MISSING_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` 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= \ --password= \ --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= \ --password= \ --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/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.