2026-07-03 16:11:24 +02:00

26 KiB

Backend Routes Module Documentation

Overview

The Routes module defines all HTTP endpoints for the application. It uses a factory pattern for entity CRUD operations and custom routes for specialized functionality.

Directory: src/routes/

Files (25 total):

File Lines Pattern Description
auth.ts 327 Custom Authentication endpoints
file.ts 150 Custom File upload/download endpoints
publish.ts 107 Custom Publishing workflow endpoints
search.ts 64 Custom Global search endpoint
runtime-context.ts 16 Custom Runtime context inspection
projects.ts 46 Hybrid Projects CRUD + custom clone endpoint
users.ts 64 Hybrid Users CRUD via factory + sanitized GET by ID
tour_pages.ts 380 Manual CRUD Tour pages CRUD plus reorder, duplicate, reverse-video status
roles.ts 141 Factory Roles CRUD via factory
permissions.ts 188 Factory Permissions CRUD via factory
assets.ts 155 Factory Assets CRUD via factory
asset_variants.ts 147 Factory Asset variants CRUD via factory
access_logs.ts 145 Factory Access logs CRUD via factory
project_memberships.ts 145 Factory Project memberships CRUD via factory
project_audio_tracks.ts 151 Factory Project audio tracks CRUD via factory
global_transition_defaults.ts 145 Custom Runtime-readable global transition defaults
global_ui_control_defaults.ts 79 Custom Runtime-readable global UI-control defaults
project_transition_settings.ts 212 Custom Project/environment transition overrides
project_ui_control_settings.ts 125 Custom Project/environment global UI-control overrides
presigned_url_requests.ts 150 Factory Presigned URL requests CRUD via factory
publish_events.ts 157 Factory Publish events CRUD via factory
pwa_caches.ts 148 Factory PWA caches CRUD via factory
element_type_defaults.ts 12 Factory Element type defaults via factory
project_element_defaults.ts 92 Hybrid Project element defaults CRUD + custom (reset, diff)

Architecture Diagram

┌──────────────────────────────────────────────────────────────────────┐
│                         Route Patterns                               │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │ Factory Pattern (createEntityRouter)                            │ │
│  │                                                                 │ │
│  │  routes/roles.ts ──────────────┐                                │ │
│  │  routes/permissions.ts ────────┤                                │ │
│  │  routes/assets.ts ─────────────┼───▶ factories/router.factory.ts│ │
│  │  routes/asset_variants.ts ─────┤                                │ │
│  │  routes/access_logs.ts ────────┤     Generates:                 │ │
│  │  routes/pwa_caches.ts ─────────┤     • POST /                   │ │
│  │  routes/publish_events.ts ─────┤     • POST /bulk-import        │ │
│  │  routes/element_type_defaults.ts     • PUT /:id                 │ │
│  │  routes/presigned_url_requests.ts    • DELETE /:id              │ │
│  │  routes/project_memberships.ts       • POST /deleteByIds        │ │
│  │  routes/project_audio_tracks.ts      • GET /                    │ │
│  │  ... (11 entities)                   • GET /count               │ │
│  │                                      • GET /autocomplete        │ │
│  │                                      • GET /:id                 │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │ Hybrid Pattern (Factory + Custom Routes)                        │ │
│  │                                                                 │ │
│  │  routes/projects.ts ──────────────────┐                         │ │
│  │     • Standard CRUD (manual)          │                         │ │
│  │     + POST /:id/clone                 │                         │ │
│  │     + GET /:id/offline-manifest       │                         │ │
│  │                                       │                         │ │
│  │  routes/project_element_defaults.ts ──┘                         │ │
│  │     • Standard CRUD (factory)                                   │ │
│  │     + POST /:id/reset                                           │ │
│  │     + GET /:id/diff                                             │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │ Custom Pattern (Full Manual Implementation)                     │ │
│  │                                                                 │ │
│  │  routes/auth.ts ───────── Authentication flows                  │ │
│  │  routes/file.ts ───────── File upload/download                  │ │
│  │  routes/publish.ts ────── Publishing workflow                   │ │
│  │  routes/search.ts ─────── Global search                         │ │
│  │  routes/runtime-context.ts ─ Runtime inspection                 │ │
│  └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘

Route Factory

factories/router.factory.ts

Generates standardized CRUD routes for entities.

const router = createEntityRouter(
  'tour_pages',        // Entity name
  Tour_pagesService,   // Service class
  Tour_pagesDBApi,     // Database API class
  {
    permissionEntity: 'tour_pages',  // Permission entity name (optional)
    csvFields: ['id', 'name'],       // CSV export fields (optional)
    validation: {                    // Request validation overrides (optional)
      create: customCreateSchema,
      update: customUpdateSchema,
    },
    customRoutes: (router, Service, DBApi) => {  // Custom routes (optional)
      router.post('/custom', handler);
    }
  }
);

Generated Endpoints

Method Path Description
POST / Create new item
POST /bulk-import Bulk import items
PUT /:id Update item by ID
DELETE /:id Delete item by ID
POST /deleteByIds Delete multiple items
GET / List items (with pagination, filters)
GET /count Count items matching filters
GET /autocomplete Autocomplete search
GET /:id Get single item by ID

Factory Features

Permission Checking:

router.use(checkCrudPermissions(permissionEntity));
// Maps HTTP methods to permissions:
// GET → READ_ENTITY
// POST → CREATE_ENTITY
// PUT/PATCH → UPDATE_ENTITY
// DELETE → DELETE_ENTITY

Request Validation:

import { validateRequest } from '../middlewares/validate-request.ts';
import { crud as crudSchemas } from '../validators/request-schemas.ts';

router.get(
  '/:id',
  validateRequest(crudSchemas.findOne),
  wrapAsync(async (req, res) => {
    const payload = await DBApi.findBy({ id: req.params.id });
    res.status(200).send(payload);
  }),
);

Factory CRUD routes validate request bodies, params, and common query controls before service/DB calls. List and count routes validate limit, page, field, sort, and filetype while keeping existing entity filter query parameters. Entity routers can pass validation overrides for stricter contracts, as users and projects do.

CSV Export:

// GET /?filetype=csv
if (filetype === 'csv') {
  const csv = parse(payload.rows, { fields });
  res.status(200).attachment('export.csv').send(csv);
}

Runtime Context:

const runtimeContext = req.runtimeContext;
const payload = await DBApi.findAll(req.query, {
  currentUser,
  runtimeContext,
});

Route Mounting (index.js)

Routes are mounted with authentication and rate limiting:

// No auth required
app.use('/api/auth', authRoutes);
app.use('/api/runtime-context', runtimeContextRoutes);
app.get('/api/health', healthHandler);

// File routes (before body parser)
app.use('/api/file/download', downloadLimiter);
app.use('/api/file/presign', downloadLimiter);
app.use('/api/file/upload', uploadLimiter);
app.use('/api/file', fileRoutes);

// JWT auth required
app.use('/api/users', jwtAuth, usersRoutes);
app.use('/api/roles', jwtAuth, rolesRoutes);
app.use('/api/permissions', jwtAuth, permissionsRoutes);
app.use('/api/project_memberships', jwtAuth, project_membershipsRoutes);
app.use('/api/assets', jwtAuth, assetsRoutes);
app.use('/api/asset_variants', jwtAuth, asset_variantsRoutes);
app.use('/api/presigned_url_requests', jwtAuth, presigned_url_requestsRoutes);
app.use('/api/publish_events', jwtAuth, publish_eventsRoutes);
app.use('/api/pwa_caches', jwtAuth, pwa_cachesRoutes);
app.use('/api/access_logs', jwtAuth, access_logsRoutes);
app.use('/api/element-type-defaults', jwtAuth, element_type_defaultsRoutes);
app.use('/api/project-element-defaults', jwtAuth, project_element_defaultsRoutes);
app.use('/api/publish', jwtAuth, publishRoutes);

// JWT + Rate limiting
app.use('/api/search', jwtAuth, searchLimiter, searchRoutes);

// Runtime public access (production environment)
mountRuntimeEntityRoute('/api/projects', 'projects', projectsRoutes);
mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tour_pagesRoutes);
mountRuntimeEntityRoute('/api/project_audio_tracks', 'project_audio_tracks', ...);

Custom Routes Detail

1. auth.ts (327 lines)

Authentication and account management.

Method Path Auth Description
POST /signin/local No Email/password login
POST /signup No Register new user
GET /me JWT Get current user
PUT /password-reset No Reset password with token
PUT /password-update JWT Change password
PUT /profile JWT Update user profile
PUT /verify-email No Verify email with token
POST /send-email-address-verification-email JWT Resend verification
POST /send-password-reset-email No Send reset email
GET /email-configured No Check email config
GET /signin/google No Google OAuth start
GET /signin/google/callback No Google OAuth callback
GET /signin/microsoft No Microsoft OAuth start
GET /signin/microsoft/callback No Microsoft OAuth callback

auth.ts is a typed ESM route boundary. It uses reusable request body/query contracts from backend/src/types/auth-routes.ts, the typed backend/src/services/auth.ts service, and the shared Passport helpers in backend/src/auth/passport-middleware.ts.


2. file.ts (150 lines)

File upload and download operations.

file.ts is a typed ESM route boundary. It calls the typed unified storage facade in backend/src/services/file.ts, while provider contracts and request/response shapes live in reusable backend/src/types/file.ts contracts. S3 and GCloud use official SDK-provided types instead of local SDK declarations.

Method Path Auth Description
GET /download No Download file by privateUrl
POST /presign No Generate presigned URLs (max 50)
POST /upload/:table/:field JWT Legacy single file upload
POST /upload-sessions/init JWT Initialize chunked upload
GET /upload-sessions/:sessionId JWT Get upload session status
PUT /upload-sessions/:sessionId/chunks/:chunkIndex JWT Upload chunk
POST /upload-sessions/:sessionId/finalize JWT Finalize chunked upload

Presigned URLs Request:

{
  "urls": ["assets/image.jpg", "assets/video.mp4"]
}

Presigned URLs Response:

{
  "presignedUrls": {
    "assets/image.jpg": "https://s3.../assets/image.jpg?X-Amz-...",
    "assets/video.mp4": "https://s3.../assets/video.mp4?X-Amz-..."
  }
}

3. publish.ts (107 lines)

Publishing workflow for Dev → Stage → Production.

Method Path Auth Description
POST / JWT Publish stage to production
POST /publish JWT Alias for publish
POST /save-to-stage JWT Save dev to stage

Publish Request:

{
  "projectId": "uuid",
  "title": "Release 1.0",
  "description": "Initial release"
}

Save to Stage Request:

{
  "projectId": "uuid"
}

4. search.ts (64 lines)

Global full-text search across entities.

Method Path Auth Description
POST / JWT Search across all entities

Request:

{
  "searchQuery": "my search term"
}

Response:

{
  "users": [...],
  "projects": [...],
  "tour_pages": [...]
}

5. runtime-context.ts (16 lines)

Runtime context inspection for debugging.

Method Path Auth Description
GET / No Get current runtime context

Response:

{
  "mode": "admin",
  "projectSlug": null,
  "headerEnvironment": "production",
  "headerProjectSlug": "my-tour"
}

Entity Routes Detail

projects.ts (46 lines)

Projects with factory CRUD plus a clone endpoint.

Standard CRUD Endpoints:

Method Path Description
POST / Create project
POST /bulk-import Bulk import projects
PUT /:id Update project
DELETE /:id Delete project
POST /deleteByIds Delete multiple projects
GET / List projects
GET /count Count projects
GET /autocomplete Autocomplete search
GET /:id Get project by ID
Method Path Description
POST /:id/clone Clone project with all pages

Custom Endpoints:

tour_pages.ts (380 lines)

Typed manual CRUD route for tour pages. In addition to standard create/update/delete/list/count/autocomplete/find-by-id operations, it owns page reorder, dev-page duplication, CSV export, and reverse-video status endpoints. The route uses reusable contracts from backend/src/types/tour-pages.ts.

Schema Fields:

  • source_key, name, slug
  • background_image_url, background_video_url, background_audio_url
  • ui_schema_json, sort_order

Endpoints:

Method Path Description
POST / Create page
POST /bulk-import Bulk import pages
POST /reorder Reorder dev pages in a project/environment
POST /:id/duplicate Duplicate a dev page
PUT /:id Update page
DELETE /:id Remove page
POST /deleteByIds Remove multiple pages
GET / List pages with reverse video URL population and optional CSV export
POST /reverse-video-status Check generated reverse-video variants
GET /count Count pages
GET /autocomplete Page autocomplete
GET /:id Get one page with reverse video URL population
  • POST /api/tour_pages/reorder accepts { data: { projectId, environment, orderedPageIds } }, validates a complete page list for the project/dev environment, and updates only sort_order.
  • POST /api/tour_pages/:id/duplicate accepts { data: { projectId, environment, name, slug } }, duplicates a dev page into a new independent dev page, appends it to the project order, deep-copies ui_schema_json, and regenerates inline element IDs.
  • Constructor page deletion uses the standard DELETE /api/tour_pages/:id route after a frontend confirmation modal; no separate delete endpoint is needed.
  • Custom routes are protected by Passport JWT like other tour page mutations and are implemented before /:id where needed so custom paths are not treated as entity IDs.
  • Non-dev environments are rejected for reorder and duplicate; stage and production changes only flow through Save to Stage and Publish.

element_type_defaults.ts (12 lines)

Minimal factory usage with permission override.

module.exports = createEntityRouter(
  'element_type_defaults',
  Element_type_defaultsService,
  Element_type_defaultsDBApi,
  {
    permissionEntity: 'page_elements',  // Uses PAGE_ELEMENTS permissions
  },
);

URL Aliases:

  • /api/element-type-defaults (primary)
  • /api/ui-elements (backwards compatibility)

project_element_defaults.ts (92 lines)

Factory-generated routes plus custom endpoints for resetting and comparing defaults.

const baseRouter = createEntityRouter(
  'project_element_defaults',
  Project_element_defaultsService,
  Project_element_defaultsDBApi,
  {
    permissionEntity: 'page_elements',
  },
);

// Add custom endpoints
baseRouter.post('/:id/reset', ...);
baseRouter.get('/:id/diff', ...);

Standard CRUD Endpoints: (via factory)

Method Path Description
POST / Create project element default
PUT /:id Update project element default
DELETE /:id Delete project element default
GET / List project element defaults
GET /:id Get project element default by ID

Custom Endpoints:

Method Path Description
POST /:id/reset Reset project element default to global
GET /:id/diff Get diff from global element type default

URL Alias:

  • /api/project-element-defaults (primary)

Request/Response Patterns

List Endpoint (GET /)

Query Parameters:

Param Type Description
page number Page number (0-indexed)
limit number Items per page
field string Sort field
sort string Sort direction (asc/desc)
filetype string Export format (csv)
[fieldName] string Filter by field value
[fieldName]Range array Filter by range [start, end]

Response:

{
  "rows": [...],
  "count": 150
}

Create Endpoint (POST /)

Request:

{
  "data": {
    "name": "New Item",
    "field1": "value1"
  }
}

Response:

{
  "id": "uuid",
  "name": "New Item",
  "field1": "value1",
  "createdAt": "2024-01-01T00:00:00.000Z"
}

Update Endpoint (PUT /:id)

Request:

{
  "id": "uuid",
  "data": {
    "name": "Updated Name"
  }
}

Response:

true

Delete Endpoints

Single Delete (DELETE /:id):

true

Bulk Delete (POST /deleteByIds):

{
  "data": ["uuid1", "uuid2", "uuid3"]
}

Swagger Documentation

Swagger UI is served at GET /api-docs. The OpenAPI document is maintained in backend/src/openapi/document.ts, not in route-local JSDoc comments.

const specs = createOpenApiDocument({
  serverUrl: config.server.swaggerServerUrl,
});

The OpenAPI module contains:

  • Shared schemas for auth, users, projects, tour pages, assets, publishing, runtime access, file upload, UI controls, transition settings, and audit entities.
  • A reusable factory CRUD path generator covering POST /, POST /bulk-import, PUT /:id, DELETE /:id, POST /deleteByIds, GET /, GET /count, GET /autocomplete, and GET /:id.
  • Explicit path definitions for custom routes such as auth flows, publishing, file upload/download, runtime access/context, project clone, tour page reorder and duplicate, transition/UI-control settings, and project element default reset/diff.

When adding routes, update backend/src/openapi/document.ts alongside the route implementation. For factory-backed entities, add a schema and CrudResource entry instead of copying per-route Swagger boilerplate.

Access Swagger UI: http://localhost:3000/api-docs in local development.

On the standard VM, the backend runs in NODE_ENV=dev_stage on port 3000; Apache serves the public domain on port 80. See deployment-vm.md for VM health checks.


Error Handling

wrapAsync Helper

All async route handlers use wrapAsync for error propagation:

router.post(
  '/',
  wrapAsync(async (req, res) => {
    // Errors thrown here are caught and passed to error handler
    const payload = await Service.create({
      data: req.body.data,
      currentUser: req.currentUser,
      runtimeContext: req.runtimeContext,
    });
    res.status(200).send(payload);
  }),
);

commonErrorHandler

Each route file includes error handler:

router.use('/', require('../helpers').commonErrorHandler);

Error Response Mapping:

Status Code Description
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Entity
500 Internal Server Error

Route Categories

Public Routes (No Auth)

Route Description
GET /api/health Health check
POST /api/auth/signin/local Login
GET /api/auth/signin/google Google OAuth
GET /api/auth/signin/microsoft Microsoft OAuth
GET /api/file/download File download
POST /api/file/presign Generate presigned URLs
GET /api/runtime-context Runtime context

Runtime Public Routes (Production Environment)

Route Description
GET /api/projects List projects (sanitized)
GET /api/tour_pages List tour pages (sanitized)
GET /api/project_audio_tracks List audio tracks (sanitized)

Authenticated Routes (JWT Required)

All other routes require JWT authentication via:

app.use('/api/users', jwtAuth, usersRoutes);

Rate Limited Routes

Route Limiter Config
/api/auth/signin/local authLimiter 10/15min
/api/auth/send-password-reset-email passwordResetLimiter 5/hour
/api/file/upload* uploadLimiter 10/min
/api/file/download, /presign downloadLimiter 200/min
/api/search searchLimiter 30/min

Dependencies

Package Purpose
express Router and middleware
json2csv CSV export functionality
passport JWT authentication
body-parser JSON body parsing

Testing

Test Entity CRUD

# Create
curl -X POST http://localhost:3000/api/projects \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"data": {"name": "Test Project", "slug": "test"}}'

# List
curl http://localhost:3000/api/projects \
  -H "Authorization: Bearer <JWT>"

# Get by ID
curl http://localhost:3000/api/projects/<uuid> \
  -H "Authorization: Bearer <JWT>"

# Update
curl -X PUT http://localhost:3000/api/projects/<uuid> \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{"id": "<uuid>", "data": {"name": "Updated Name"}}'

# Delete
curl -X DELETE http://localhost:3000/api/projects/<uuid> \
  -H "Authorization: Bearer <JWT>"

Test Public Runtime

curl http://localhost:3000/api/projects \
  -H "X-Runtime-Environment: production"

Test Presigned URLs

curl -X POST http://localhost:3000/api/file/presign \
  -H "Content-Type: application/json" \
  -d '{"urls": ["assets/test.jpg"]}'

Summary

The Routes module provides:

  1. Factory Pattern - 12 entities with standardized CRUD (createEntityRouter)
  2. Hybrid Pattern - 2 entities with factory CRUD + custom endpoints (projects, project_element_defaults)
  3. Custom Routes - 7 specialized route files for auth, files, search, etc.
  4. Swagger Documentation - centralized OpenAPI document for all endpoints
  5. Permission Checking - RBAC via checkCrudPermissions middleware
  6. Runtime Public Access - Production environment public read access
  7. Rate Limiting - Per-endpoint rate limiting
  8. CSV Export - Export capability via ?filetype=csv
  9. Request Validation - Joi schemas via validateRequest before service calls
  10. Error Handling - Centralized via wrapAsync and commonErrorHandler

Route Statistics:

  • 26 route files
  • ~50+ unique endpoints
  • 11 factory-generated entity routers
  • 3 hybrid entity routers (factory + custom)
  • 7 custom route implementations