Skip to content

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.ts and server/api/health/ have direct DB access for health checks only

Architecture

mermaid
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):

FilePurpose
api-auth.tsJWT validation, admin role enforcement, audit logging of denied access
csrf.tsDouble-submit cookie CSRF protection for state-changing mutations
okta-session.tsExchanges an Okta OIDC session (via nuxt-oidc-auth) for a backend JWT if no auth_token cookie is present
rate-limit.ts5 attempts / 15-minute window on auth POST endpoints
security-headers.tsSets CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy

See Security for the detailed implementation of each middleware.

Server plugins

FileRunsPurpose
server/plugins/validate-config.tsServer startupValidates required runtime config keys on startup; logs warnings for missing values

Server utilities

Located in server/utils/:

FilePurpose
proxy.tsproxyTo(), proxyWithParams(), proxyToWithBody() — all proxy to the backend API
backend.tsbackendUrl(path) and resolveAuthHeader(event) — build backend URLs and extract auth tokens
jwt.tssignToken() and verifyToken() — HMAC-SHA256 JWT creation and verification
csrf.tsCSRF token generation helper
audit-logger.tsAppend-to-JSONL audit logging with Azure SWA read-only FS fallback
validate.tsvalidateBody(event, schema) — Zod validation wrapper for request bodies
database-validation.tsDirect DB connection health check (health endpoints only)
bt-company-identifiers.tsBluetooth company identifier lookup table

proxyTo(path, options?) — the standard proxy pattern

Almost all server routes follow this pattern:

typescript
// server/api/zones/index.get.ts
export default proxyTo('zones')

proxyTo handles:

  1. Reads auth_token httpOnly cookie via resolveAuthHeader()
  2. Percent-decodes Azure SWA-encoded cookie values
  3. Rejects "Bearer null" / "Bearer undefined" before forwarding
  4. Forwards query string, method, and body
  5. Returns 204 with null for no-content responses
  6. 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():

typescript
// 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)
})
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 undefined

Server 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

DomainKey routes
Authauth/login.post, auth/logout.post, auth/backend-login.post, auth/backend-me.get, auth/csrf-token.get
Usersusers/index.{get,post}, users/[id].{put,delete}, users/[id]/reset-password.post, users/[id]/unlock.post, users/roles.get
Buildingsbuildings/index.{get,post}, buildings/[id].{get,put,delete}, buildings/order.put, buildings/gateway-health.get
Floorsfloors/index.post, floors/[id].{put,delete}, floors/[floorId]/upload.post, floors/[floorId]/calibrate.post, floors/[floorId]/calibrate/accept.post
Zoneszones/index.{get,post}, zones/[id].{get,put,delete}, zones/emergency.get
Receiversreceivers/index.{get,post}, receivers/[id].{get,put,delete}, receivers/bulk.post, receivers/discover.get
Personnelpersonnel/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
Assetsassets/index.{get,post}, assets/[id].{get,put,delete}, assets/[id]/history.get
Emergencyemergency/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}
Dashboarddashboard/index.get, dashboard/summary.get, dashboard/buildings.get, dashboard/floors.get, dashboard/floors/[id].get, dashboard/departments.get, dashboard/personnel.get
Batterybattery/stats.get, battery/alerts.get, battery/thresholds.{get,put}, battery/device/[deviceId].get, battery/history/[deviceId].get
Weatherweather/conditions.get, weather/forecast.get, weather/alerts.get, weather/settings.get, weather/settings/[severity].put, weather/refresh.post
Transmitterstransmitters/index.{get,post}, transmitters/[id].{get,put,patch}, transmitters/assign.post, transmitters/bulk.post, transmitters/unassigned.get
Phone beaconphone-beacon/[token].get, phone-beacon/onboarding-token.post, phone-beacon/complete-enrollment.post, phone-beacon/irk-records.get
Auditaudit/logs.get, audit/summary.get, audit/export.get
Managementmgmt/beacons/*.{get,post,put,delete}, mgmt/departments/*.{get,post,put,delete}, mgmt/settings/{index,ble,alerts,general}.*, mgmt/zone-types/*
Contextcontext/index.get, context/floor/[floorId].get
Diagnosticsdiagnostics/signal-monitor.get
Gatewaysgateways/sync-status.post
Positionspositions/history.get
Realtimenegotiate.{get,post}
Healthhealth/database.ts
Activityactivity.get
Proxyproxy/error-log.post
Dev-onlyserver/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():

FilePurpose
personnel.tsPersonnel CRUD route OpenAPI metadata
_responses.tsShared error/auth/success response shapes
index.tsRe-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:

typescript
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':

RoutePurpose
POST /dev-loginAccepts any credentials and returns a stub JWT — never in production
GET /dev-userReturns the current dev stub user — never in production
GET /ble/scanReturns BLE scanner status
GET /ble/statusReturns BLE device list

Useful development commands

bash
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

NISC Muster Tracking Documentation