This document describes the authentication and authorization model for the internal delivery note browser.
The system uses:
branch, admin, dev).NOTE: This document is a living document. As we extend the auth system (sessions, routes, policies, password flows), we will update this file.
The main goals of the authentication and authorization system are:
This document covers:
Auth depends on the following environment variables:
SESSION_SECRET (required)
Auth endpoints also require DB connectivity:
MONGODB_URI (required)SESSION_COOKIE_SECURE (optional)
Secure cookie flag.true or false.Default behavior:
Secure cookie is enabled when NODE_ENV === "production".Local HTTP testing (e.g. http://localhost:3000 with Docker + next start):
SESSION_COOKIE_SECURE=false in your local .env.docker.Staging/Production:
SESSION_COOKIE_SECURE unset (or true) and run the app behind TLS.If the application is served over plain HTTP (no TLS), many clients will not send
Securecookies back. In that case, logins will appear to “work” (Set-Cookie is present), but subsequent requests will still be unauthenticated.
The repo provides centralized env validation:
lib/config/validateEnv.js validates required env vars and basic sanity checks.scripts/validate-env.mjs runs validation against process.env.In Docker, run validation before starting the server:
node scripts/validate-env.mjs && npm run start
branchbranchId (e.g. "NL01").Intended access pattern:
adminbranchId = null).Intended access pattern:
devbranchId = null).Intended access pattern:
RBAC is enforced on branch-related filesystem APIs.
Error responses use the standardized API error payload:
{
"error": {
"message": "Human readable message",
"code": "SOME_MACHINE_CODE",
"details": {}
}
}
401 Unauthorized: no valid session (getSession() returns null).
{ "error": { "message": "Unauthorized", "code": "AUTH_UNAUTHENTICATED" } }
403 Forbidden: session exists but the user is not allowed to access the requested branch.
{ "error": { "message": "Forbidden", "code": "AUTH_FORBIDDEN_BRANCH" } }
RBAC rules live in lib/auth/permissions.js:
canAccessBranch(session, branchId)filterBranchesForSession(session, branchIds)These endpoints require a valid session:
GET /api/branchesGET /api/branches/[branch]/yearsGET /api/branches/[branch]/[year]/monthsGET /api/branches/[branch]/[year]/[month]/daysGET /api/files?branch=&year=&month=&day=Sessions are implemented as signed JWTs stored in HTTP-only cookies.
{
"userId": "<MongoDB ObjectId as string>",
"role": "branch | admin | dev",
"branchId": "NL01 | null",
"iat": 1700000000,
"exp": 1700003600
}
HS256.SESSION_SECRET.SESSION_MAX_AGE_SECONDS = 8 hours.Cookie name: auth_session
Attributes:
httpOnly: truesecure: resolved via NODE_ENV + optional SESSION_COOKIE_SECURE overridesameSite: "lax"path: "/"maxAge: 8 hoursImplementation lives in lib/auth/session.js:
createSession({ userId, role, branchId })getSession()destroySession()POST /api/auth/loginAuthenticate a user and set the session cookie.
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"
}
}
GET /api/auth/logoutClears the session cookie.
200 { "ok": true } on success.SESSION_SECRET secret and rotate when needed.SESSION_COOKIE_SECURE=false.