Răsfoiți Sursa

RHL-007-refactor(docs): standardize documentation language and error response formats across API and storage modules

Code_Uwe 7 ore în urmă
părinte
comite
ac0aca25be
4 a modificat fișierele cu 297 adăugiri și 36 ștergeri
  1. 182 18
      Docs/api.md
  2. 63 10
      Docs/auth.md
  3. 30 4
      Docs/runbook.md
  4. 22 4
      Docs/storage.md

+ 182 - 18
Docs/api.md

@@ -1,10 +1,10 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
+<!-- Folder: Docs -->
 
-<!-- Datei: api.md -->
+<!-- File: api.md -->
 
-<!-- Relativer Pfad: Docs/api.md -->
+<!-- Relative Path: Docs/api.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
@@ -53,7 +53,7 @@ To access protected endpoints:
 Notes:
 
 - In production-like setups, cookies should be `Secure` and the app should run behind HTTPS.
-- For local HTTP testing (`http://localhost:3000`), set `SESSION_COOKIE_SECURE=false` in your local docker env file.
+- 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)
 
@@ -64,19 +64,80 @@ RBAC is enforced on filesystem-related endpoints.
 
 ---
 
-## 3. General Conventions
+## 3. Error Handling & Conventions
 
-- All endpoints return JSON.
+### 3.1 Standard error response format
 
-- Error responses use:
+All endpoints return JSON.
 
-  ```json
-  { "error": "Human-readable error message" }
-  ```
+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).
+
+### 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 login 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):
 
-- Route handlers use Web `Request` / `Response` primitives.
+- Auth:
 
-- For dynamic routes, Next.js 16 resolves parameters asynchronously via `ctx.params`.
+  - `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`
+
+### 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.
 
 ---
 
@@ -124,10 +185,51 @@ Authenticate a user and set the session cookie.
 **Responses**
 
 - `200 { "ok": true }`
-- `400 { "error": "Invalid request body" }`
-- `400 { "error": "Missing username or password" }`
-- `401 { "error": "Invalid credentials" }`
-- `500 { "error": "Internal server error" }`
+
+- `400` (invalid JSON/body)
+
+  ```json
+  {
+  	"error": {
+  		"message": "Invalid request body",
+  		"code": "VALIDATION_INVALID_JSON"
+  	}
+  }
+  ```
+
+- `400` (missing username/password)
+
+  ```json
+  {
+  	"error": {
+  		"message": "Missing username or password",
+  		"code": "VALIDATION_MISSING_FIELD",
+  		"details": { "fields": ["username", "password"] }
+  	}
+  }
+  ```
+
+- `401` (invalid credentials)
+
+  ```json
+  {
+  	"error": {
+  		"message": "Invalid credentials",
+  		"code": "AUTH_INVALID_CREDENTIALS"
+  	}
+  }
+  ```
+
+- `500`
+
+  ```json
+  {
+  	"error": {
+  		"message": "Internal server error",
+  		"code": "INTERNAL_SERVER_ERROR"
+  	}
+  }
+  ```
 
 ---
 
@@ -143,6 +245,19 @@ Destroy the current session by clearing the cookie.
 
 - `200 { "ok": true }`
 
+**Error response (rare)**
+
+- `500`
+
+  ```json
+  {
+  	"error": {
+  		"message": "Internal server error",
+  		"code": "INTERNAL_SERVER_ERROR"
+  	}
+  }
+  ```
+
 ---
 
 ### 4.4 `GET /api/branches`
@@ -162,6 +277,22 @@ Returns the list of branches (e.g. `["NL01", "NL02"]`).
 { "branches": ["NL01", "NL02"] }
 ```
 
+**Error responses**
+
+- `401`
+
+  ```json
+  { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
+  ```
+
+- `500`
+
+  ```json
+  {
+  	"error": { "message": "Internal server error", "code": "FS_STORAGE_ERROR" }
+  }
+  ```
+
 ---
 
 ### 4.5 `GET /api/branches/[branch]/years`
@@ -176,6 +307,14 @@ Example: `/api/branches/NL01/years`
 { "branch": "NL01", "years": ["2023", "2024"] }
 ```
 
+**Error responses (common)**
+
+- `401` → `AUTH_UNAUTHENTICATED`
+- `403` → `AUTH_FORBIDDEN_BRANCH`
+- `400` → `VALIDATION_MISSING_PARAM`
+- `404` → `FS_NOT_FOUND`
+- `500` → `FS_STORAGE_ERROR` / `INTERNAL_SERVER_ERROR`
+
 ---
 
 ### 4.6 `GET /api/branches/[branch]/[year]/months`
@@ -190,6 +329,14 @@ Example: `/api/branches/NL01/2024/months`
 { "branch": "NL01", "year": "2024", "months": ["01", "02", "10"] }
 ```
 
+**Error responses (common)**
+
+- `401` → `AUTH_UNAUTHENTICATED`
+- `403` → `AUTH_FORBIDDEN_BRANCH`
+- `400` → `VALIDATION_MISSING_PARAM`
+- `404` → `FS_NOT_FOUND`
+- `500` → `FS_STORAGE_ERROR` / `INTERNAL_SERVER_ERROR`
+
 ---
 
 ### 4.7 `GET /api/branches/[branch]/[year]/[month]/days`
@@ -204,6 +351,14 @@ Example: `/api/branches/NL01/2024/10/days`
 { "branch": "NL01", "year": "2024", "month": "10", "days": ["01", "23"] }
 ```
 
+**Error responses (common)**
+
+- `401` → `AUTH_UNAUTHENTICATED`
+- `403` → `AUTH_FORBIDDEN_BRANCH`
+- `400` → `VALIDATION_MISSING_PARAM`
+- `404` → `FS_NOT_FOUND`
+- `500` → `FS_STORAGE_ERROR` / `INTERNAL_SERVER_ERROR`
+
 ---
 
 ### 4.8 `GET /api/files?branch=&year=&month=&day=`
@@ -228,6 +383,14 @@ Example:
 }
 ```
 
+**Error responses (common)**
+
+- `401` → `AUTH_UNAUTHENTICATED`
+- `403` → `AUTH_FORBIDDEN_BRANCH`
+- `400` → `VALIDATION_MISSING_QUERY`
+- `404` → `FS_NOT_FOUND`
+- `500` → `FS_STORAGE_ERROR` / `INTERNAL_SERVER_ERROR`
+
 ---
 
 ## 5. Adding New Endpoints
@@ -238,5 +401,6 @@ When adding new endpoints:
 2. Implement a `route.js` under `app/api/...`.
 3. Use `lib/storage` for filesystem access.
 4. Enforce RBAC (`getSession()` + `canAccessBranch()` as needed).
-5. Add route tests (Vitest).
-6. Update this document.
+5. Use the standardized error contract (prefer `withErrorHandling` + `ApiError` helpers).
+6. Add route tests (Vitest).
+7. Update this document.

+ 63 - 10
Docs/auth.md

@@ -1,10 +1,10 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
+<!-- Folder: Docs -->
 
-<!-- Datei: auth.md -->
+<!-- File: auth.md -->
 
-<!-- Relativer Pfad: Docs/auth.md -->
+<!-- Relative Path: Docs/auth.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
@@ -31,7 +31,7 @@ The main goals of the authentication and authorization system are:
 - Branch users can only see delivery notes for **their own branch**.
 - Admin and dev users can access data across branches.
 - Passwords are never stored in plaintext.
-- Sessions are stored as signed JWTs in HTTP-only cookies.
+- Sessions are stored in signed JWTs in HTTP-only cookies.
 
 This document covers:
 
@@ -128,16 +128,28 @@ RBAC is enforced on branch-related filesystem APIs.
 
 ### 4.1 Response semantics
 
+Error responses use the standardized API error payload:
+
+```json
+{
+	"error": {
+		"message": "Human readable message",
+		"code": "SOME_MACHINE_CODE",
+		"details": {}
+	}
+}
+```
+
 - **401 Unauthorized**: no valid session (`getSession()` returns `null`).
 
   ```json
-  { "error": "Unauthorized" }
+  { "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
   ```
 
 - **403 Forbidden**: session exists but the user is not allowed to access the requested branch.
 
   ```json
-  { "error": "Forbidden" }
+  { "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } }
   ```
 
 ### 4.2 Permission helpers
@@ -210,10 +222,51 @@ Authenticate a user and set the session cookie.
 Responses:
 
 - `200 { "ok": true }`
-- `400 { "error": "Invalid request body" }`
-- `400 { "error": "Missing username or password" }`
-- `401 { "error": "Invalid credentials" }`
-- `500 { "error": "Internal server error" }`
+
+- `400` (invalid JSON/body)
+
+  ```json
+  {
+  	"error": {
+  		"message": "Invalid request body",
+  		"code": "VALIDATION_INVALID_JSON"
+  	}
+  }
+  ```
+
+- `400` (missing username/password)
+
+  ```json
+  {
+  	"error": {
+  		"message": "Missing username or password",
+  		"code": "VALIDATION_MISSING_FIELD",
+  		"details": { "fields": ["username", "password"] }
+  	}
+  }
+  ```
+
+- `401` (invalid credentials)
+
+  ```json
+  {
+  	"error": {
+  		"message": "Invalid credentials",
+  		"code": "AUTH_INVALID_CREDENTIALS"
+  	}
+  }
+  ```
+
+- `500`
+
+  ```json
+  {
+  	"error": {
+  		"message": "Internal server error",
+  		"code": "INTERNAL_SERVER_ERROR"
+  	}
+  }
+  ```
 
 ### 6.2 `GET /api/auth/logout`
 

+ 30 - 4
Docs/runbook.md

@@ -1,10 +1,10 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
+<!-- Folder: Docs -->
 
-<!-- Datei: runbook.md -->
+<!-- File: runbook.md -->
 
-<!-- Relativer Pfad: Docs/runbook.md -->
+<!-- Relative Path: Docs/runbook.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
@@ -194,6 +194,7 @@ ssh administrator@192.168.0.23
 ### 3.2 Prerequisites on the server
 
 - Docker and Docker Compose installed.
+
 - The real NAS share is mounted at:
 
   - `/mnt/niederlassungen`
@@ -222,13 +223,38 @@ Use the base compose file only (no local override):
 ENV_FILE=.env.server docker compose -f docker-compose.yml up -d --build
 ```
 
+### 3.4.1 Optional: Persist ENV_FILE selection via `.env`
+
+If you want a simpler startup command (and to avoid forgetting `ENV_FILE=...`), you can create a small `.env` file **on the server only** that defines which env file Compose should use.
+
+Create `./.env` in the project root:
+
+```bash
+printf "ENV_FILE=.env.server
+" > .env
+```
+
+After that, you can start the stack with:
+
+```bash
+docker compose -f docker-compose.yml up -d --build
+```
+
+Notes:
+
+- Keep `.env` server-local (do not commit it).
+- `.env.server` still contains secrets and must not be committed.
+- Always run `docker compose` from the project root so Compose picks up the correct `.env` file.
+
+````
+
 ### 3.5 Verify
 
 On the server:
 
 ```bash
 curl -s http://localhost:3000/api/health
-```
+````
 
 Expected:
 

+ 22 - 4
Docs/storage.md

@@ -1,10 +1,10 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
+<!-- Folder: Docs -->
 
-<!-- Datei: storage.md -->
+<!-- File: storage.md -->
 
-<!-- Relativer Pfad: Docs/storage.md -->
+<!-- Relative Path: Docs/storage.md -->
 
 <!-- --------------------------------------------------------------------------- -->
 
@@ -19,6 +19,7 @@ All code that needs to read from the NAS should go through this module instead o
 ## 1. High-Level Responsibilities
 
 - Resolve paths under the NAS root (`NAS_ROOT_PATH`).
+
 - Provide intention-revealing helpers:
 
   - `listBranches()` → `['NL01', 'NL02', ...]`
@@ -28,6 +29,7 @@ All code that needs to read from the NAS should go through this module instead o
   - `listFiles(branch, year, month, day)` → `[{ name, relativePath }, ...]`
 
 - Enforce **read-only** behavior.
+
 - Use async filesystem APIs (`fs/promises`).
 
 ---
@@ -106,7 +108,23 @@ Rules:
 
 ## 5. Error Handling
 
+### 5.1 Storage layer behavior
+
 `lib/storage` does not swallow errors:
 
 - If a folder does not exist or is not accessible, `fs.promises.readdir` throws.
-- API route handlers catch and convert errors into HTTP responses.
+- `lib/storage` remains intentionally small and focused on filesystem reads.
+
+### 5.2 API-level mapping
+
+API routes map filesystem errors into standardized HTTP responses:
+
+- If a requested path does not exist (e.g. `ENOENT`) and the NAS root is accessible:
+
+  - `404` with `FS_NOT_FOUND`
+
+- If the NAS root itself is missing/unreachable or other unexpected filesystem errors occur:
+
+  - `500` with `FS_STORAGE_ERROR`
+
+This mapping is implemented in `lib/api/storageErrors.js` and used by route handlers.