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

All routes below are served under the `/api` prefix.

> Frontend developers: For practical usage examples and the small `apiClient` helper layer, read **`docs/frontend-api-usage.md`**. That document is the frontend-oriented single source of truth.

---

## 1. Configuration Dependencies

The API expects a valid server configuration.

Required environment variables:

- `MONGODB_URI` — database connection string (used by `lib/db.js`).
- `SESSION_SECRET` — JWT signing secret for session cookies.
- `NAS_ROOT_PATH` — NAS mount root for storage operations.

Optional environment variables:

- `SESSION_COOKIE_SECURE` — override for the cookie `Secure` flag (`true`/`false`).

### 1.1 Search provider configuration (RHL-016)

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.

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

**Normalization rule (A.1)**

- `lib/config/validateEnv.js` accepts these values case-insensitively.
- Runtime normalizes Qsirch values (trim + lowercase for `QSIRCH_DATE_FIELD` / `QSIRCH_MODE`, and trim for `QSIRCH_PATH_PREFIX`) so behavior matches validation.

The environment can be validated via:

- `lib/config/validateEnv.js`
- `scripts/validate-env.mjs`

In Docker/production-like runs, execute `node scripts/validate-env.mjs` before starting the server to fail fast.

> Note: You may see a Node warning like `MODULE_TYPELESS_PACKAGE_JSON` when running env validation.
> The validation still succeeds; the warning is harmless.

---

## 2. Authentication & Authorization

### 2.1 Sessions

Authentication uses a signed JWT stored in an HTTP-only cookie (`auth_session`).

To access protected endpoints:

1. `POST /api/auth/login` to obtain the cookie.
2. Send subsequent requests with that cookie.

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.
- Session payload includes `mustChangePassword` (boolean), used by frontend auth gating (RHL-044).

### 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 + 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** introduced the capability separation (branch access vs user management).
- **RHL-012** implements the actual user management endpoints under `/api/admin/users` and enforces this capability consistently.

---

## 3. Error Handling & Conventions

### 3.1 Standard error response format

Most endpoints return JSON.

Success responses keep their existing shapes (**unchanged**).

Error responses always use this standardized shape:

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

Notes:

- `error.message` is intended for humans (UI, logs).
- `error.code` is a stable machine-readable identifier (frontend handling, tests, monitoring).
- `error.details` is optional. When present, it must be a JSON object (e.g. validation info).

**Binary endpoints**

Some endpoints may return a non-JSON body on the **200** happy path (for example `application/pdf`).

Rules for such endpoints:

- On success: return the documented binary payload.
- On non-200 errors: return the standardized JSON error payload above.

### 3.2 Status code rules

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 / capability mismatch)
- `404` — resource not found (branch/year/month/day/file does not exist)
- `500` — unexpected server errors (internal failures)

### 3.3 Common error codes

The API uses these machine-readable codes (non-exhaustive list):

- Auth:
  - `AUTH_UNAUTHENTICATED`
  - `AUTH_INVALID_CREDENTIALS`
  - `AUTH_FORBIDDEN_BRANCH`
  - `AUTH_FORBIDDEN_USER_MANAGEMENT` (RHL-041; used by RHL-012)

- Validation (generic):
  - `VALIDATION_MISSING_PARAM`
  - `VALIDATION_MISSING_QUERY`
  - `VALIDATION_INVALID_JSON`
  - `VALIDATION_INVALID_BODY`
  - `VALIDATION_MISSING_FIELD`
  - `VALIDATION_INVALID_FIELD`

- Validation (password management):
  - `VALIDATION_WEAK_PASSWORD`

- Validation (filesystem route params):
  - `VALIDATION_BRANCH`
  - `VALIDATION_YEAR`
  - `VALIDATION_MONTH`
  - `VALIDATION_DAY`
  - `VALIDATION_FILENAME`
  - `VALIDATION_FILE_EXTENSION`
  - `VALIDATION_PATH_TRAVERSAL`

- Validation (search):
  - `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 (RHL-012):
  - `USER_NOT_FOUND`

- Storage:
  - `FS_NOT_FOUND`
  - `FS_STORAGE_ERROR`

- Search (backend/provider):
  - `SEARCH_BACKEND_UNAVAILABLE` (provider misconfiguration/unavailability)

- Internal:
  - `INTERNAL_SERVER_ERROR`

### 3.4 Implementation notes

Route handlers use shared helpers:

- `lib/api/errors.js` (standard error payloads + `withErrorHandling`)
- `lib/api/storageErrors.js` (maps filesystem errors like ENOENT to 404 vs 500)

### 3.5 Testing note

- For realistic `Secure` cookie behavior, prefer HTTPS.
- For local testing on `http://localhost`, many tools/browsers treat localhost as a special-case “secure context”. Behavior may vary between environments.

### 3.6 Caching & Freshness (RHL-006)

This project reads delivery-note PDFs from a NAS mount. New scans can appear at any time.

The caching strategy is designed to:

- show new files predictably
- reduce filesystem load where possible
- avoid any accidental shared caching for auth-protected responses

#### 3.6.1 HTTP caching policy

All JSON API responses explicitly disable HTTP caching:

- `Cache-Control: no-store`

This is applied centrally by `lib/api/errors.js` (the `json()` / `jsonError()` helpers). The policy applies to both success and error responses.

For binary endpoints (e.g. PDF streaming), the route handler must explicitly set:

- `Cache-Control: no-store`

#### 3.6.2 Next.js route handler execution

All API route handlers are forced to execute dynamically at request time:

- each route exports `export const dynamic = "force-dynamic";`

This avoids any static/ISR-like behavior for NAS-dependent endpoints and keeps auth/RBAC behavior safe.

#### 3.6.3 Storage micro-cache (server-side TTL)

To reduce repeated `fs.readdir()` calls, `lib/storage.js` implements a small process-local TTL cache **after** RBAC is enforced (RBAC is checked in the route handlers).

TTL defaults:

- `listBranches()` / `listYears()` → **60 seconds**
- `listMonths()` / `listDays()` / `listFiles()` → **15 seconds**

Important:

- TTL is a **max staleness** guarantee (new files may appear immediately; they must appear after TTL).
- Cache is **process-local** (not shared across multiple app instances).

#### 3.6.4 Frontend fetch guidelines

Frontend code should call these endpoints with explicit “fresh data” settings:

- Use credentials:

  ```js
  fetch(url, { credentials: "include", cache: "no-store" });
  ```

- Do not rely on `next: { revalidate: ... }` for these endpoints. Freshness is controlled via:
  - `Cache-Control: no-store` (HTTP)
  - server-side storage TTL micro-cache

---

## 4. Endpoints

### 4.1 `GET /api/health`

**Purpose**

Health check endpoint:

- Verifies database connectivity (`db.command({ ping: 1 })`).
- Verifies readability of `NAS_ROOT_PATH`.

**Authentication**: not required.

**Response 200 (example)**

```json
{
	"db": "ok",
	"nas": {
		"path": "/mnt/niederlassungen",
		"entriesSample": ["@Recently-Snapshot", "NL01", "NL02"]
	}
}
```

---

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

**Purpose**

Authenticate a user and set the session cookie.

**Authentication**: not required.

**Request body (JSON)**

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

**Responses**

- `200 { "ok": true }`

- `400` (invalid JSON/body)

- `400` (missing username/password)

- `401` (invalid credentials)

- `500`

---

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

**Purpose**

Destroy the current session by clearing the cookie.

**Authentication**: recommended (but endpoint is idempotent).

**Response**

- `200 { "ok": true }`

---

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

**Purpose**

Provide the current session identity for frontend consumers.

Semantics (frontend-friendly):

- 200 with `{ user: null }` when unauthenticated
- 200 with `{ user: { userId, role, branchId, email, mustChangePassword } }` when authenticated

Notes:

- `role` is one of: `branch | admin | superadmin | dev`.
- `email` is optional and may be `null`.
- `mustChangePassword` is always returned as a boolean.

---

### 4.5 `POST /api/auth/change-password` (RHL-009)

Change password for the currently authenticated user.

Behavior summary:

- Validates input and password policy.
- Replaces `passwordHash`.
- Clears:
  - `mustChangePassword`
  - `passwordResetToken`
  - `passwordResetExpiresAt`
- Refreshes the session cookie so `mustChangePassword=false` is visible immediately to `/api/auth/me`.

---

### 4.6 `GET /api/branches`

Returns the list of branches (e.g. `['NL01', 'NL02']`).

**Authentication**: required.

**RBAC behavior**

- `branch` role → only own branch
- `admin` / `superadmin` / `dev` → all branches

**Response 200**

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

---

### 4.7 `GET /api/branches/[branch]/years`

Example: `/api/branches/NL01/years`

**Authentication**: required.

---

### 4.8 `GET /api/branches/[branch]/[year]/months`

Example: `/api/branches/NL01/2024/months`

**Authentication**: required.

---

### 4.9 `GET /api/branches/[branch]/[year]/[month]/days`

Example: `/api/branches/NL01/2024/10/days`

**Authentication**: required.

---

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

Returns files for a given day.

**Authentication**: required.

---

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

**Purpose**

Stream (or download) a single PDF file from the NAS while enforcing authentication and branch-level RBAC.

---

### 4.12 `GET /api/search`

**Purpose**

Search delivery note content across PDFs.

**Authentication**: required.

**RBAC behavior**

- `branch` role:
  - results are limited to the user’s `branchId`
  - attempting to query other branches returns `403 AUTH_FORBIDDEN_BRANCH`

- `admin` / `superadmin` / `dev` role:
  - can search across branches using explicit scope parameters

Filter rule:

- The backend requires at least one of: `q`, `from`, `to`.
- If all three are missing, the API returns `400 VALIDATION_SEARCH_MISSING_FILTER`.

---

### 4.13 `GET /api/admin/users` (RHL-012 + RHL-043)

List user accounts (cursor-based pagination with optional sort modes).

**Authentication**: required.

**Authorization**: user management capability required (`superadmin` / `dev`).

**Query params (optional)**

- `q` — free-text filter (matches username/email)
- `role` — one of `branch | admin | superadmin | dev`
- `branchId` — `NLxx` (filters branch users)
- `limit` — page size (default `50`, min `1`, max `200`)
- `sort` — one of:
  - `default` (existing baseline order)
  - `role_rights` (`superadmin > dev > admin > branch`, deterministic tie-breakers)
  - `branch_asc` (`NL` numeric ascending, users without NL at the end)
- `cursor` — opaque cursor from the previous response

**Response 200**

```json
{
	"items": [
		{
			"id": "...",
			"username": "...",
			"email": "...",
			"role": "branch",
			"branchId": "NL01",
			"mustChangePassword": true,
			"createdAt": "2026-02-01T10:00:00.000Z",
			"updatedAt": "2026-02-02T10:00:00.000Z"
		}
	],
	"nextCursor": "..."
}
```

Notes:

- `nextCursor` is `null` when there are no more results.
- All returned user objects are safe (no password hash/token fields).
- Invalid `sort` returns `400 VALIDATION_INVALID_FIELD` with `details.field = "sort"` and allowed values.
- Cursor safety by sort mode:
  - cursors are tied to the active sort mode,
  - reusing a cursor with another sort mode returns `400 VALIDATION_INVALID_FIELD` (`details.field = "cursor"`).

---

### 4.14 `POST /api/admin/users` (RHL-012)

Create a new user.

**Authentication**: required.

**Authorization**: user management capability required (`superadmin` / `dev`).

**Request body (JSON)**

```json
{
	"username": "branchuser",
	"email": "nl01@example.com",
	"role": "branch",
	"branchId": "NL01",
	"initialPassword": "StrongPassword123"
}
```

Rules:

- `branchId` is required **only** when `role === "branch"`.
- `initialPassword` is validated using the password policy.
- The created user is initialized with `mustChangePassword = true`.

**Response 200**

```json
{ "ok": true, "user": { "id": "...", "username": "...", "email": "..." } }
```

Duplicate handling (common):

- Username exists:

  ```json
  {
  	"error": {
  		"message": "Username already exists",
  		"code": "VALIDATION_INVALID_FIELD",
  		"details": { "field": "username" }
  	}
  }
  ```

- Email exists:

  ```json
  {
  	"error": {
  		"message": "Email already exists",
  		"code": "VALIDATION_INVALID_FIELD",
  		"details": { "field": "email" }
  	}
  }
  ```

- Username and email exist:

  ```json
  {
  	"error": {
  		"message": "Username and email already exist",
  		"code": "VALIDATION_INVALID_FIELD",
  		"details": { "fields": ["username", "email"] }
  	}
  }
  ```

---

### 4.15 `PATCH /api/admin/users/:userId` (RHL-012)

Update a user.

**Authentication**: required.

**Authorization**: user management capability required (`superadmin` / `dev`).

**Route params**

- `userId` — MongoDB ObjectId

**Request body (JSON)**

Supports patching (any subset):

- `username`
- `email`
- `role`
- `branchId` (nullable)
- `mustChangePassword` (boolean)

Role/branch consistency rule:

- If the resulting role is `branch`, a valid `branchId` is required.
- For non-branch roles, `branchId` is cleared.

**Response 200**

```json
{ "ok": true, "user": { "id": "...", "username": "...", "email": "..." } }
```

Not found:

- `404 USER_NOT_FOUND`

---

### 4.16 `POST /api/admin/users/:userId` (RHL-043)

Reset a user password and return a temporary plaintext password.

**Authentication**: required.

**Authorization**: user management capability required (`superadmin` / `dev`).

**Route params**

- `userId` — MongoDB ObjectId

Behavior:

- Generates a new temporary password server-side.
- Stores only the bcrypt hash in the database.
- Forces password rotation (`mustChangePassword = true`).
- Clears legacy reset-token fields (`passwordResetToken`, `passwordResetExpiresAt`).

Safety rules:

- Resetting the currently authenticated user via this endpoint is forbidden:
  - `400 VALIDATION_INVALID_FIELD`
  - `details.reason = "SELF_PASSWORD_RESET_FORBIDDEN"`

**Response 200**

```json
{
	"ok": true,
	"user": {
		"id": "...",
		"username": "...",
		"email": "...",
		"role": "branch",
		"branchId": "NL02",
		"mustChangePassword": true
	},
	"temporaryPassword": "TempPass123!"
}
```

Notes:

- `temporaryPassword` is returned only in this response and should be handled as sensitive data.
- Not found returns `404 USER_NOT_FOUND`.

---

### 4.17 `DELETE /api/admin/users/:userId` (RHL-012)

Delete a user.

**Authentication**: required.

**Authorization**: user management capability required (`superadmin` / `dev`).

**Route params**

- `userId` — MongoDB ObjectId

**Response 200**

```json
{
	"ok": true,
	"user": {
		"id": "...",
		"username": "...",
		"email": "...",
		"role": "branch",
		"branchId": "NL01",
		"mustChangePassword": true
	}
}
```

Not found:

- `404 USER_NOT_FOUND`

Safety rule:

- Deleting the currently authenticated user via this endpoint is forbidden:
  - `400 VALIDATION_INVALID_FIELD`
  - `details.reason = "SELF_DELETE_FORBIDDEN"`

---

## 5. API v1 freeze (RHL-008)

The endpoints and response shapes documented here (and in `docs/frontend-api-usage.md`) are considered **API v1** for the first frontend implementation.

Rules:

- Avoid breaking changes to existing endpoints, parameters, or response shapes.
- Prefer additive changes (new endpoints, new optional fields).
- If a breaking change becomes necessary, introduce a new endpoint rather than modifying the v1 contract.

---

## 6. Adding New Endpoints

When adding new endpoints:

1. Define URL + method.

2. Implement a `route.js` under `app/api/...`.

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.
