|
@@ -0,0 +1,431 @@
|
|
|
|
|
+# Frontend API Usage (v1)
|
|
|
|
|
+
|
|
|
|
|
+This document is the **frontend-facing** single source of truth for consuming the 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 first UI.
|
|
|
|
|
+
|
|
|
|
|
+Non-goals:
|
|
|
|
|
+
|
|
|
|
|
+- New major features.
|
|
|
|
|
+- PDF streaming/viewer implementation details (see “Out of scope / planned”).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 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 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)`
|
|
|
|
|
+
|
|
|
|
|
+### 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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 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()`
|
|
|
|
|
+
|
|
|
|
|
+Navigation:
|
|
|
|
|
+
|
|
|
|
|
+- `getBranches()`
|
|
|
|
|
+- `getYears(branch)`
|
|
|
|
|
+- `getMonths(branch, year)`
|
|
|
|
|
+- `getDays(branch, year, month)`
|
|
|
|
|
+
|
|
|
|
|
+Files:
|
|
|
|
|
+
|
|
|
|
|
+- `getFiles(branch, year, month, day)`
|
|
|
|
|
+
|
|
|
|
|
+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.
|
|
|
|
|
+- Treat it as an opaque identifier for a future download/stream endpoint.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 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" } }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Success (unauthenticated):
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{ "user": null }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 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" }]
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4.4 Health
|
|
|
|
|
+
|
|
|
|
|
+#### `GET /api/health`
|
|
|
|
|
+
|
|
|
|
|
+Always returns `200` and reports partial system state:
|
|
|
|
|
+
|
|
|
|
|
+- database connectivity
|
|
|
|
|
+- NAS readability
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 5. Error handling
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 Standard error payload
|
|
|
|
|
+
|
|
|
|
|
+All 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`
|
|
|
|
|
+
|
|
|
|
|
+Validation:
|
|
|
|
|
+
|
|
|
|
|
+- `VALIDATION_MISSING_PARAM`
|
|
|
|
|
+- `VALIDATION_MISSING_QUERY`
|
|
|
|
|
+- `VALIDATION_INVALID_JSON`
|
|
|
|
|
+- `VALIDATION_INVALID_BODY`
|
|
|
|
|
+- `VALIDATION_MISSING_FIELD`
|
|
|
|
|
+
|
|
|
|
|
+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:
|
|
|
|
|
+
|
|
|
|
|
+- happy path drill-down
|
|
|
|
|
+- 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
|
|
|
|
|
+
|
|
|
|
|
+PDF delivery (download/stream) is not part of the current v1 surface documented above.
|
|
|
|
|
+
|
|
|
|
|
+Planned as additive change:
|
|
|
|
|
+
|
|
|
|
|
+- a dedicated endpoint to stream or download a PDF while enforcing RBAC server-side.
|