api.md 17 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.

2.2 RBAC (Branch-Level)

RBAC is enforced on filesystem-related endpoints.

  • 401 Unauthorized: no valid session
  • 403 Forbidden: session exists but branch access is not allowed

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 / branch 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
  • Validation (generic):

    • VALIDATION_MISSING_PARAM
    • VALIDATION_MISSING_QUERY
    • VALIDATION_INVALID_JSON
    • VALIDATION_INVALID_BODY
    • VALIDATION_MISSING_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
  • 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)

    {
    	"error": {
    		"message": "Invalid request body",
    		"code": "VALIDATION_INVALID_JSON"
    	}
    }
    
  • 400 (missing username/password)

    {
    	"error": {
    		"message": "Missing username or password",
    		"code": "VALIDATION_MISSING_FIELD",
    		"details": { "fields": ["username", "password"] }
    	}
    }
    
  • 401 (invalid credentials)

    {
    	"error": {
    		"message": "Invalid credentials",
    		"code": "AUTH_INVALID_CREDENTIALS"
    	}
    }
    
  • 500

    {
    	"error": {
    		"message": "Internal server error",
    		"code": "INTERNAL_SERVER_ERROR"
    	}
    }
    

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 }

Error response (rare)

  • 500

    {
    	"error": {
    		"message": "Internal server error",
    		"code": "INTERNAL_SERVER_ERROR"
    	}
    }
    

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 } } when authenticated

This avoids using 401 as control-flow for basic "am I logged in?" checks.


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

Purpose

Change the password for the currently authenticated user.

Authentication: required.

Request body (JSON)

{
	"currentPassword": "<string>",
	"newPassword": "<string>"
}

Response 200

{ "ok": true }

Behavior notes

  • The endpoint validates JSON and required fields.
  • The endpoint verifies currentPassword against the stored passwordHash.
  • The endpoint enforces an explicit password policy (see docs/auth.md).
  • On success, the endpoint also clears password-related flags:
    • mustChangePassword = false
    • passwordResetToken = null
    • passwordResetExpiresAt = null

Error responses

  • 401 when no session exists or the session is invalid:

    { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
    
  • 401 when currentPassword does not match:

    {
    	"error": {
    		"message": "Invalid credentials",
    		"code": "AUTH_INVALID_CREDENTIALS"
    	}
    }
    
  • 400 when the JSON body is invalid:

    {
    	"error": {
    		"message": "Invalid request body",
    		"code": "VALIDATION_INVALID_JSON"
    	}
    }
    
  • 400 when required fields are missing:

    {
    	"error": {
    		"message": "Missing currentPassword or newPassword",
    		"code": "VALIDATION_MISSING_FIELD",
    		"details": { "fields": ["currentPassword", "newPassword"] }
    	}
    }
    
  • 400 when newPassword violates the password policy:

    {
    	"error": {
    		"message": "Weak password",
    		"code": "VALIDATION_WEAK_PASSWORD",
    		"details": {
    			"minLength": 8,
    			"requireLetter": true,
    			"requireNumber": true,
    			"disallowSameAsCurrent": true,
    			"reasons": ["MIN_LENGTH", "MISSING_NUMBER"]
    		}
    	}
    }
    
  • 500 for unexpected errors:

    {
    	"error": {
    		"message": "Internal server error",
    		"code": "INTERNAL_SERVER_ERROR"
    	}
    }
    

4.6 GET /api/branches

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

Authentication: required.

RBAC behavior

  • branch role → only own branch
  • admin/dev → all branches

Response 200

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

Error responses

  • 401

    { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
    
  • 500

    {
    	"error": { "message": "Internal server error", "code": "FS_STORAGE_ERROR" }
    }
    

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

Example: /api/branches/NL01/years

Authentication: required.

Response 200

{ "branch": "NL01", "years": ["2023", "2024"] }

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

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

Authentication: required.

Response 200

{ "branch": "NL01", "year": "2024", "months": ["01", "02", "10"] }

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

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

Authentication: required.

Response 200

{ "branch": "NL01", "year": "2024", "month": "10", "days": ["01", "23"] }

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

Example:

/api/files?branch=NL01&year=2024&month=10&day=23

Authentication: required.

Response 200

{
	"branch": "NL01",
	"year": "2024",
	"month": "10",
	"day": "23",
	"files": [{ "name": "test.pdf", "relativePath": "NL01/2024/10/23/test.pdf" }]
}

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.

Authentication: required.

RBAC behavior

  • 401 AUTH_UNAUTHENTICATED when no valid session exists.
  • 403 AUTH_FORBIDDEN_BRANCH when the session is not allowed to access :branch.

URL params

  • branch: NL + digits (e.g. NL01)
  • year: YYYY (4 digits)
  • month: MM (0112)
  • day: DD (0131)
  • filename: PDF file name (must be a simple file name; no path segments)

Query params (optional)

  • download=1 or download=true
    • Forces Content-Disposition: attachment (download)
    • Default is inline (open in browser)

Success response (200)

  • Body: raw PDF bytes (not JSON)
  • Headers (example):
    • Content-Type: application/pdf
    • Cache-Control: no-store

Error responses (JSON)

Even though the happy path is binary, error responses remain standardized JSON.


4.12 GET /api/search

Purpose

Search delivery note content across PDFs.

The endpoint returns search hits with enough metadata to:

  • navigate to the correct Explorer day folder
  • open the PDF via the binary file endpoint

Authentication: required.

RBAC behavior

  • branch role:

    • results are limited to the user’s branchId
    • attempting to query other branches may return 403 AUTH_FORBIDDEN_BRANCH
  • admin/dev role:

    • can search across branches using explicit scope parameters

Query params

  • q (optional)

    • Text query string.
  • scope (optional)

    • branch — single branch scope (admin/dev only; branch users are forced to their own branch)
    • all — all branches (admin/dev only)
    • multi — only the branches listed in branches (admin/dev only)
  • branch (optional)

    • Single branch for scope=branch.
    • Branch users may omit branch and are forced to their own branch.
  • branches (optional)

    • Comma-separated branch list for scope=multi.
  • from, to (optional)

    • Inclusive date filter in YYYY-MM-DD format.
  • limit (optional)

    • Page size.
    • Default: 100
    • Allowed: 50..200
  • cursor (optional)

    • Pagination cursor returned by the previous response.
    • Treat as opaque.

Filter rule (important)

To avoid accidental "match everything" queries (especially dangerous in scope=all), the backend requires at least one of:

  • q
  • from
  • to

If all three are missing, the API returns:

  • 400 VALIDATION_SEARCH_MISSING_FILTER

Response 200 (example)

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

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. Use lib/storage for filesystem listing/navigation access.
  4. Enforce RBAC (getSession() + canAccessBranch() as needed).
  5. Use the standardized error contract (prefer withErrorHandling + ApiError helpers).
  6. Add route tests (Vitest).
  7. Update this document.