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

758 lines
26 KiB
Markdown

# 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 <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
```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