api.md 18 KB

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

    {
    	"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:

{
	"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:

    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)

{
	"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)

{ "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

{ "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
  • branchIdNLxx (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

{
	"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)

{
	"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

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

Duplicate handling (common):

  • Username exists:

    {
    	"error": {
    		"message": "Username already exists",
    		"code": "VALIDATION_INVALID_FIELD",
    		"details": { "field": "username" }
    	}
    }
    
  • Email exists:

    {
    	"error": {
    		"message": "Email already exists",
    		"code": "VALIDATION_INVALID_FIELD",
    		"details": { "field": "email" }
    	}
    }
    
  • Username and email exist:

    {
    	"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

{ "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

{
	"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

{
	"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.