Nuxt Server Layer
Purpose
nuxt-frontend/server/ is the Nitro server layer that runs inside the Nuxt application. It acts as a secure proxy between the browser and the Azure Functions API backend.
Key design constraints:
- Nuxt server routes must never access the database directly
- All data access goes through the backend API via proxy utilities
- The Nitro layer is responsible for session management, cookie handling, CSRF protection, and request validation
- Exceptions:
server/utils/database-validation.tsandserver/api/health/have direct DB access for health checks only
Architecture
flowchart LR
Browser --> NitroMiddleware[Nitro middleware stack]
NitroMiddleware --> NitroRoute[Nuxt server/api route]
NitroRoute --> ProxyUtil[proxyTo / proxyToWithBody]
ProxyUtil --> BackendAPI[Azure Functions API]
BackendAPI --> PostgreSQL[(PostgreSQL)]The proxy utilities read the httpOnly auth_token cookie server-side and forward it as Authorization: Bearer — client JS cannot read the cookie or forge the header.
Server middleware stack
Files in server/middleware/ execute on every request in alphabetical order (Nitro convention):
| File | Purpose |
|---|---|
api-auth.ts | JWT validation, admin role enforcement, audit logging of denied access |
csrf.ts | Double-submit cookie CSRF protection for state-changing mutations |
okta-session.ts | Exchanges an Okta OIDC session (via nuxt-oidc-auth) for a backend JWT if no auth_token cookie is present |
rate-limit.ts | 5 attempts / 15-minute window on auth POST endpoints |
security-headers.ts | Sets CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy |
See Security for the detailed implementation of each middleware.
Server plugins
| File | Runs | Purpose |
|---|---|---|
server/plugins/validate-config.ts | Server startup | Validates required runtime config keys on startup; logs warnings for missing values |
Server utilities
Located in server/utils/:
| File | Purpose |
|---|---|
proxy.ts | proxyTo(), proxyWithParams(), proxyToWithBody() — all proxy to the backend API |
backend.ts | backendUrl(path) and resolveAuthHeader(event) — build backend URLs and extract auth tokens |
jwt.ts | signToken() and verifyToken() — HMAC-SHA256 JWT creation and verification |
csrf.ts | CSRF token generation helper |
audit-logger.ts | Append-to-JSONL audit logging with Azure SWA read-only FS fallback |
validate.ts | validateBody(event, schema) — Zod validation wrapper for request bodies |
database-validation.ts | Direct DB connection health check (health endpoints only) |
bt-company-identifiers.ts | Bluetooth company identifier lookup table |
proxyTo(path, options?) — the standard proxy pattern
Almost all server routes follow this pattern:
// server/api/zones/index.get.ts
export default proxyTo('zones')proxyTo handles:
- Reads
auth_tokenhttpOnly cookie viaresolveAuthHeader() - Percent-decodes Azure SWA-encoded cookie values
- Rejects
"Bearer null"/"Bearer undefined"before forwarding - Forwards query string, method, and body
- Returns 204 with
nullfor no-content responses - Passes backend error bodies through with the original status code
proxyToWithBody(path, validatedBody, options?) — for mutation routes
Used when the request body has already been consumed by validateBody():
// server/api/personnel/index.post.ts
import { personnelCreateSchema } from '~/shared/schemas'
export default defineEventHandler(async (event) => {
const body = await validateBody(event, personnelCreateSchema)
return proxyToWithBody('personnel', body)
})resolveAuthHeader(event) — cookie-priority auth
1. Read auth_token httpOnly cookie (preferred — reliable on Azure SWA)
2. Decode percent-encoded values
3. Fall back to Authorization header from client
4. Reject "Bearer null" / "Bearer undefined" strings
5. Return "Bearer <token>" or undefinedServer API routes
The server/api/ tree contains ~120 typed Nitro route files. Each route proxies to the corresponding backend API endpoint. The catch-all server/api/[...path].ts forwards any unmatched route directly.
Route domains and coverage
| Domain | Key routes |
|---|---|
| Auth | auth/login.post, auth/logout.post, auth/backend-login.post, auth/backend-me.get, auth/csrf-token.get |
| Users | users/index.{get,post}, users/[id].{put,delete}, users/[id]/reset-password.post, users/[id]/unlock.post, users/roles.get |
| Buildings | buildings/index.{get,post}, buildings/[id].{get,put,delete}, buildings/order.put, buildings/gateway-health.get |
| Floors | floors/index.post, floors/[id].{put,delete}, floors/[floorId]/upload.post, floors/[floorId]/calibrate.post, floors/[floorId]/calibrate/accept.post |
| Zones | zones/index.{get,post}, zones/[id].{get,put,delete}, zones/emergency.get |
| Receivers | receivers/index.{get,post}, receivers/[id].{get,put,delete}, receivers/bulk.post, receivers/discover.get |
| Personnel | personnel/index.{get,post}, personnel/[id].{put,delete}, personnel/[id]/photo.{post,delete}, personnel/[id]/beacons.get, personnel/export/{csv,pdf}.get, personnel/batch-delete.post, personnel/bulk-{activate,deactivate}.post |
| Assets | assets/index.{get,post}, assets/[id].{get,put,delete}, assets/[id]/history.get |
| Emergency | emergency/index.{get,post}, emergency/[id].{get,put}, emergency/trigger.post, emergency/active.get, emergency/events.get, emergency/muster.get, emergency/contacts.{get,post}, emergency/contacts/[contactId].{put,delete} |
| Dashboard | dashboard/index.get, dashboard/summary.get, dashboard/buildings.get, dashboard/floors.get, dashboard/floors/[id].get, dashboard/departments.get, dashboard/personnel.get |
| Battery | battery/stats.get, battery/alerts.get, battery/thresholds.{get,put}, battery/device/[deviceId].get, battery/history/[deviceId].get |
| Weather | weather/conditions.get, weather/forecast.get, weather/alerts.get, weather/settings.get, weather/settings/[severity].put, weather/refresh.post |
| Transmitters | transmitters/index.{get,post}, transmitters/[id].{get,put,patch}, transmitters/assign.post, transmitters/bulk.post, transmitters/unassigned.get |
| Phone beacon | phone-beacon/[token].get, phone-beacon/onboarding-token.post, phone-beacon/complete-enrollment.post, phone-beacon/irk-records.get |
| Audit | audit/logs.get, audit/summary.get, audit/export.get |
| Management | mgmt/beacons/*.{get,post,put,delete}, mgmt/departments/*.{get,post,put,delete}, mgmt/settings/{index,ble,alerts,general}.*, mgmt/zone-types/* |
| Context | context/index.get, context/floor/[floorId].get |
| Diagnostics | diagnostics/signal-monitor.get |
| Gateways | gateways/sync-status.post |
| Positions | positions/history.get |
| Realtime | negotiate.{get,post} |
| Health | health/database.ts |
| Activity | activity.get |
| Proxy | proxy/error-log.post |
| Dev-only | server/routes/dev-login.post, server/routes/dev-user.get, server/routes/ble/{scan,status}.get |
Server schemas
server/schemas/ wraps shared Zod schemas with OpenAPI metadata for defineRouteMeta():
| File | Purpose |
|---|---|
personnel.ts | Personnel CRUD route OpenAPI metadata |
_responses.ts | Shared error/auth/success response shapes |
index.ts | Re-exports |
The shared/schemas.ts file contains pure Zod schemas usable from both server routes and Vue components without circular import issues.
OpenAPI compliance requirements
Every Nitro server route must call defineRouteMeta() at module scope:
import { personnelListMeta } from '~/server/schemas/personnel'
defineRouteMeta(personnelListMeta) // compile-time macro — must be at module scope
export default defineEventHandler(async (event) => { ... })Catch-all [...path].ts proxy routes are exempt. The QC pipeline (scripts/check-openapi-compliance.sh) blocks commits that omit defineRouteMeta on non-exempt routes.
Dev-only routes
Routes in server/routes/ are only active when NODE_ENV !== 'production':
| Route | Purpose |
|---|---|
POST /dev-login | Accepts any credentials and returns a stub JWT — never in production |
GET /dev-user | Returns the current dev stub user — never in production |
GET /ble/scan | Returns BLE scanner status |
GET /ble/status | Returns BLE device list |
Useful development commands
cd nuxt-frontend
# Unit tests (covers all server/api routes and middleware)
npm run test:unit
# Run a specific server route test
npx vitest run server/__tests__/csrf.test.ts
# Run with coverage
npm run test:unit:coverage