# 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. ```javascript 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:** ```javascript router.use(checkCrudPermissions(permissionEntity)); // Maps HTTP methods to permissions: // GET → READ_ENTITY // POST → CREATE_ENTITY // PUT/PATCH → UPDATE_ENTITY // DELETE → DELETE_ENTITY ``` **Request Validation:** ```typescript 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:** ```javascript // GET /?filetype=csv if (filetype === 'csv') { const csv = parse(payload.rows, { fields }); res.status(200).attachment('export.csv').send(csv); } ``` **Runtime Context:** ```javascript 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: ```javascript // 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:** ```json { "urls": ["assets/image.jpg", "assets/video.mp4"] } ``` **Presigned URLs Response:** ```json { "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:** ```json { "projectId": "uuid", "title": "Release 1.0", "description": "Initial release" } ``` **Save to Stage Request:** ```json { "projectId": "uuid" } ``` --- ### 4. search.ts (64 lines) Global full-text search across entities. | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/` | JWT | Search across all entities | **Request:** ```json { "searchQuery": "my search term" } ``` **Response:** ```json { "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:** ```json { "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 | **Custom Endpoints:** | Method | Path | Description | |--------|------|-------------| | POST | `/:id/clone` | Clone project with all pages | --- ### 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. ```javascript 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. ```javascript 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:** ```json { "rows": [...], "count": 150 } ``` ### Create Endpoint (POST /) **Request:** ```json { "data": { "name": "New Item", "field1": "value1" } } ``` **Response:** ```json { "id": "uuid", "name": "New Item", "field1": "value1", "createdAt": "2024-01-01T00:00:00.000Z" } ``` ### Update Endpoint (PUT /:id) **Request:** ```json { "id": "uuid", "data": { "name": "Updated Name" } } ``` **Response:** ```json true ``` ### Delete Endpoints **Single Delete (DELETE /:id):** ```json true ``` **Bulk Delete (POST /deleteByIds):** ```json { "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. ```javascript 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`](../../../documentation/deployment-vm.md) for VM health checks. --- ## Error Handling ### wrapAsync Helper All async route handlers use `wrapAsync` for error propagation: ```javascript 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: ```javascript 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: ```javascript 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 ```bash # Create curl -X POST http://localhost:3000/api/projects \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"data": {"name": "Test Project", "slug": "test"}}' # List curl http://localhost:3000/api/projects \ -H "Authorization: Bearer " # Get by ID curl http://localhost:3000/api/projects/ \ -H "Authorization: Bearer " # Update curl -X PUT http://localhost:3000/api/projects/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"id": "", "data": {"name": "Updated Name"}}' # Delete curl -X DELETE http://localhost:3000/api/projects/ \ -H "Authorization: Bearer " ``` ### Test Public Runtime ```bash curl http://localhost:3000/api/projects \ -H "X-Runtime-Environment: production" ``` ### Test Presigned URLs ```bash 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