39948-vm/backend/docs/api-endpoints.md
2026-07-03 16:11:24 +02:00

1672 lines
38 KiB
Markdown

# Backend API Endpoints Documentation
Complete reference for all API endpoints in the Tour Builder Platform backend.
## Overview
- **Base URL**: `http://localhost:3000/api` for local development
- **Content-Type**: `application/json`
- **Authentication**: JWT Bearer Token (except public endpoints)
- **API Documentation UI**: `http://localhost:3000/api-docs` (Swagger, local development)
**Standard VM note:** the VM `dev_stage` backend listens on port `3000`, while
the frontend listens on `3001` and Apache serves the public domain on port `80`.
Use `http://127.0.0.1:3000/api/...` for direct VM backend checks. Protected
routes returning `401 Unauthorized` without JWT are healthy. See
[`deployment-vm.md`](../../documentation/deployment-vm.md).
---
## Table of Contents
1. [Authentication](#1-authentication)
2. [Health & System](#2-health--system)
3. [Users](#3-users)
4. [Roles](#4-roles)
5. [Permissions](#5-permissions)
6. [Projects](#6-projects)
7. [Project Memberships](#7-project-memberships)
8. [Tour Pages](#8-tour-pages)
9. [Assets](#9-assets)
10. [Asset Variants](#10-asset-variants)
11. [Project Audio Tracks](#11-project-audio-tracks)
12. [Project Transition Settings](#12-project-transition-settings)
13. [Global UI Controls](#13-global-ui-controls)
14. [Element Defaults](#14-element-defaults)
15. [Publishing](#15-publishing)
16. [File Management](#16-file-management)
17. [Search](#17-search)
20. [Access Logs](#20-access-logs)
21. [Publish Events](#21-publish-events)
22. [PWA Caches](#22-pwa-caches)
23. [Presigned URL Requests](#23-presigned-url-requests)
---
## Common Patterns
### Authentication Header
```http
Authorization: Bearer <jwt_token>
```
### Query Parameters (List Endpoints)
| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | number | Page number (0-indexed) |
| `limit` | number | Items per page (default: 10) |
| `field` | string | Field to sort by |
| `sort` | string | Sort direction: `asc` or `desc` |
| `filetype` | string | Set to `csv` for CSV export |
| `<field>` | string | Text search filter (ILIKE) |
| `<field>Range` | string | Range filter: `[min,max]` |
### Standard List Response
```json
{
"rows": [...],
"count": 100
}
```
### Rate Limits
| Endpoint Type | Limit | Window |
|---------------|-------|--------|
| Auth | 10 requests | 15 minutes |
| Signup | 5 requests | 1 hour |
| Password Reset | 5 requests | 1 hour |
| API (general) | 100 requests | 1 minute |
| Upload | 10 requests | 1 minute |
| Download | 200 requests | 1 minute |
| Search | 30 requests | 1 minute |
| AI | 20 requests | 1 minute |
### Rate Limit Headers
```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 2026-03-30T12:00:00.000Z
Retry-After: 60 # (only on 429)
```
---
## 1. Authentication
### POST /api/auth/signin/local
Sign in with email and password.
**Authentication**: None
**Rate Limit**: 10/15min
**Request:**
```json
{
"email": "user@example.com",
"password": "password123"
}
```
**Response (200):**
```json
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Errors:**
- `400`: Invalid credentials
---
### Self-Registration
Self-registration is disabled. `POST /api/auth/signup` is not registered.
New users are created through the authenticated Users API/UI and receive an
invitation/setup link.
---
### GET /api/auth/me
Get current authenticated user.
**Authentication**: Required
**Response (200):**
```json
{
"id": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"emailVerified": true,
"disabled": false,
"app_role": {
"id": "uuid",
"name": "User",
"globalAccess": false
},
"custom_permissions": []
}
```
---
### PUT /api/auth/password-reset
Reset password using token.
**Authentication**: None
**Request:**
```json
{
"token": "reset-token-from-email",
"password": "newPassword123"
}
```
**Response (200):** `true`
---
### POST /api/auth/send-password-reset-email
Send password reset email.
**Authentication**: None
**Rate Limit**: 5/hour
**Request:**
```json
{
"email": "user@example.com"
}
```
**Response (200):** `true`
---
### PUT /api/auth/password-update
Update password for authenticated user.
**Authentication**: Required
**Request:**
```json
{
"currentPassword": "oldPassword",
"newPassword": "newPassword123"
}
```
**Response (200):** `true`
---
### PUT /api/auth/profile
Update user profile.
**Authentication**: Required
**Request:**
```json
{
"profile": {
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890"
}
}
```
**Response (200):** `true`
---
### PUT /api/auth/verify-email
Verify email address.
**Authentication**: None
**Request:**
```json
{
"token": "verification-token-from-email"
}
```
**Response (200):** JWT token
---
### GET /api/auth/email-configured
Check if email service is configured.
**Authentication**: None
**Response (200):** `true` or `false`
---
### GET /api/auth/signin/google
Initiate Google OAuth flow.
**Authentication**: None
**Query Parameters:**
- `app`: Optional state parameter
**Response**: Redirect to Google
---
### GET /api/auth/signin/google/callback
Google OAuth callback.
**Response**: Redirect to frontend with token
---
### GET /api/auth/signin/microsoft
Initiate Microsoft OAuth flow.
**Authentication**: None
**Response**: Redirect to Microsoft
---
### GET /api/auth/signin/microsoft/callback
Microsoft OAuth callback.
**Response**: Redirect to frontend with token
---
## 2. Health & System
### GET /api/health
Health check endpoint.
**Authentication**: None
**Response (200):**
```json
{
"status": "ok",
"timestamp": "2026-03-30T12:00:00.000Z",
"uptime": 12345.678,
"environment": "production",
"database": "connected"
}
```
**Response (503):** (when database is down)
```json
{
"status": "degraded",
"timestamp": "2026-03-30T12:00:00.000Z",
"uptime": 12345.678,
"environment": "production",
"database": "disconnected",
"databaseError": "Connection refused"
}
```
---
### GET /api/runtime-context
Get current runtime context (environment detection).
**Authentication**: None
**Response (200):**
```json
{
"mode": "workspace",
"projectSlug": "my-project",
"headerEnvironment": "production"
}
```
---
## 3. Users
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/users` | Create user |
| GET | `/api/users` | List users |
| GET | `/api/users/count` | Count users |
| GET | `/api/users/autocomplete` | Autocomplete search |
| GET | `/api/users/:id` | Get user by ID |
| PUT | `/api/users/:id` | Update user |
| DELETE | `/api/users/:id` | Delete user |
| POST | `/api/users/deleteByIds` | Bulk delete |
| POST | `/api/users/bulk-import` | Bulk import |
**Authentication**: Required
**Permissions**: `CREATE_USERS`, `READ_USERS`, `UPDATE_USERS`, `DELETE_USERS`
### POST /api/users
**Request:**
```json
{
"data": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneNumber": "+1234567890",
"app_role": "uuid-of-role",
"custom_permissions": ["uuid-1", "uuid-2"],
"password": "password123",
"disabled": false
}
}
```
---
### GET /api/users
**Query Parameters:**
- `firstName`: Filter by first name
- `lastName`: Filter by last name
- `email`: Filter by email
- `app_role`: Filter by role ID
- Standard pagination params
**Response:**
```json
{
"rows": [
{
"id": "uuid",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneNumber": "+1234567890",
"disabled": false,
"emailVerified": true,
"app_role": { "id": "uuid", "name": "User" }
}
],
"count": 1
}
```
---
## 4. Roles
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/roles` | Create role |
| GET | `/api/roles` | List roles |
| GET | `/api/roles/count` | Count roles |
| GET | `/api/roles/autocomplete` | Autocomplete search |
| GET | `/api/roles/:id` | Get role by ID |
| PUT | `/api/roles/:id` | Update role |
| DELETE | `/api/roles/:id` | Delete role |
| POST | `/api/roles/deleteByIds` | Bulk delete |
**Permissions**: `CREATE_ROLES`, `READ_ROLES`, `UPDATE_ROLES`, `DELETE_ROLES`
### POST /api/roles
**Request:**
```json
{
"data": {
"name": "Editor",
"globalAccess": false,
"permissions": ["uuid-1", "uuid-2"]
}
}
```
---
## 5. Permissions
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/permissions` | Create permission |
| GET | `/api/permissions` | List permissions |
| GET | `/api/permissions/count` | Count permissions |
| GET | `/api/permissions/autocomplete` | Autocomplete search |
| GET | `/api/permissions/:id` | Get permission by ID |
| PUT | `/api/permissions/:id` | Update permission |
| DELETE | `/api/permissions/:id` | Delete permission |
| POST | `/api/permissions/deleteByIds` | Bulk delete |
**Permissions**: `CREATE_PERMISSIONS`, `READ_PERMISSIONS`, `UPDATE_PERMISSIONS`, `DELETE_PERMISSIONS`
### POST /api/permissions
**Request:**
```json
{
"data": {
"name": "READ_CUSTOM_FEATURE"
}
}
```
---
## 6. Projects
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/projects` | Create project |
| GET | `/api/projects` | List projects |
| GET | `/api/projects/count` | Count projects |
| GET | `/api/projects/autocomplete` | Autocomplete search |
| GET | `/api/projects/:id` | Get project by ID |
| PUT | `/api/projects/:id` | Update project |
| DELETE | `/api/projects/:id` | Delete project |
| POST | `/api/projects/deleteByIds` | Bulk delete |
**Permissions**: `CREATE_PROJECTS`, `READ_PROJECTS`, `UPDATE_PROJECTS`, `DELETE_PROJECTS`
**Runtime Public Access**: GET endpoints accessible without auth in production mode.
### POST /api/projects
**Request:**
```json
{
"data": {
"name": "My Tour",
"slug": "my-tour",
"description": "A virtual tour of the museum",
"logo_url": "assets/logo.png",
"favicon_url": "assets/favicon.ico",
"og_image_url": "assets/og-image.jpg"
}
}
```
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Project display name |
| `slug` | string | URL-safe identifier (auto-generated if not provided) |
| `description` | string | Project description |
| `logo_url` | string | Path to project logo |
| `favicon_url` | string | Path to favicon |
| `og_image_url` | string | Path to Open Graph image |
---
### POST /api/projects/:id/clone
Clone an existing project with all pages and settings.
**Request:** None (uses URL parameter)
**Response:**
```json
{
"id": "new-project-uuid",
"name": "My Tour (Copy)",
"slug": "my-tour-copy"
}
```
---
### GET /api/projects/:id/offline-manifest
Get PWA offline manifest for a project.
**Query Parameters:**
- `variant`: `mobile` or `desktop` (default: `desktop`)
**Response:**
```json
{
"projectId": "uuid",
"version": "2026-03-30T12:00:00.000Z",
"assets": [
{
"url": "assets/image.jpg",
"type": "image",
"size": 1024000
}
],
"pages": [
{
"slug": "home",
"name": "Home Page"
}
]
}
```
---
## 7. Project Memberships
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/project_memberships` | Create membership |
| GET | `/api/project_memberships` | List memberships |
| GET | `/api/project_memberships/count` | Count memberships |
| GET | `/api/project_memberships/:id` | Get membership by ID |
| PUT | `/api/project_memberships/:id` | Update membership |
| DELETE | `/api/project_memberships/:id` | Delete membership |
**Permissions**: `CREATE_PROJECT_MEMBERSHIPS`, `READ_PROJECT_MEMBERSHIPS`, etc.
### POST /api/project_memberships
**Request:**
```json
{
"data": {
"user": "user-uuid",
"project": "project-uuid",
"membershipRole": "editor"
}
}
```
**Membership Roles:**
- `owner` - Full access including delete
- `editor` - Can edit content
- `viewer` - Read-only access
---
## 8. Tour Pages
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/tour_pages` | Create page |
| GET | `/api/tour_pages` | List pages |
| GET | `/api/tour_pages/count` | Count pages |
| GET | `/api/tour_pages/:id` | Get page by ID |
| PUT | `/api/tour_pages/:id` | Update page |
| DELETE | `/api/tour_pages/:id` | Delete page |
| POST | `/api/tour_pages/deleteByIds` | Bulk delete |
| POST | `/api/tour_pages/reorder` | Reorder pages by updating `sort_order` only |
| POST | `/api/tour_pages/:id/duplicate` | Duplicate a dev page as a new independent dev page |
**Permissions**: `CREATE_TOUR_PAGES`, `READ_TOUR_PAGES`, etc.
**Runtime Public Access**: GET endpoints accessible without auth in production mode.
### POST /api/tour_pages/reorder
Reorders pages for a project in the constructor environment.
**Auth:** Required
**Request:**
```json
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"orderedPageIds": ["page-uuid-1", "page-uuid-2", "page-uuid-3"]
}
}
```
**Response:**
```json
[
{ "id": "page-uuid-1", "sort_order": 1 },
{ "id": "page-uuid-2", "sort_order": 2 },
{ "id": "page-uuid-3", "sort_order": 3 }
]
```
**Rules:**
- Only `environment: "dev"` is accepted. Stage and production are updated by
publishing, not by direct reorder writes.
- `orderedPageIds` must include every page in the project/dev environment
exactly once.
- The endpoint updates only `tour_pages.sort_order`; it does not alter
`ui_schema_json`, slugs, backgrounds, media, navigation, or transitions.
- The visible stage order changes after Save to Stage. The visible production
order changes after Publish.
**Validation failures:**
- Missing `projectId`
- Empty or invalid `orderedPageIds`
- Duplicate page IDs
- Missing pages from the ordered list
- Unknown page IDs or page IDs outside the project/environment
- Any environment other than `dev`
### POST /api/tour_pages/:id/duplicate
Duplicates a constructor/dev page into a new independent dev page.
**Auth:** Required
**Request:**
```json
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"name": "Lobby Copy",
"slug": "lobby-copy"
}
}
```
**Rules:**
- Only source pages in `environment: "dev"` can be duplicated.
- The requested target environment must also be `dev`.
- The source page must belong to the requested project.
- The new page is appended to constructor order with
`sort_order = max(project dev sort_order) + 1`.
- The duplicate receives a new page ID, unique slug, and empty `source_key`.
- Page settings, background media fields, design dimensions, `requires_auth`,
and `ui_schema_json` are copied.
- Inline `ui_schema_json.elements[]` IDs and nested gallery/carousel/info-panel
item IDs are regenerated for independence.
- Asset URLs, transition URLs, and navigation `targetPageSlug` references are
preserved.
- Reverse-video processing uses the existing `TourPagesService` path.
**Validation failures:**
- Invalid source page ID
- Source page not found
- Source page outside requested project
- Source or target environment other than `dev`
### POST /api/tour_pages
**Request:**
```json
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"name": "Home Page",
"slug": "home",
"sort_order": 1,
"background_image_url": "assets/bg.jpg",
"background_video_url": null,
"background_audio_url": "assets/ambient.mp3",
"background_loop": true,
"requires_auth": false,
"ui_schema_json": {
"elements": [],
"canvasSettings": {}
}
}
}
```
**ui_schema_json Structure:**
```json
{
"elements": [
{
"id": "element-uuid",
"type": "button",
"props": {
"x": 100,
"y": 200,
"width": 150,
"height": 50,
"label": "Click Me",
"targetPageSlug": "next-page",
"transitionVideo": "assets/transition.mp4"
}
}
],
"canvasSettings": {
"width": 1920,
"height": 1080
}
}
```
---
## 9. Assets
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/assets` | Create asset record |
| GET | `/api/assets` | List assets |
| GET | `/api/assets/count` | Count assets |
| GET | `/api/assets/autocomplete` | Autocomplete search |
| GET | `/api/assets/:id` | Get asset by ID |
| PUT | `/api/assets/:id` | Update asset |
| DELETE | `/api/assets/:id` | Delete asset |
| POST | `/api/assets/deleteByIds` | Bulk delete |
**Permissions**: `CREATE_ASSETS`, `READ_ASSETS`, etc.
### POST /api/assets
Create an asset record with MIME type validation.
**Request:**
```json
{
"data": {
"name": "Hero Image",
"asset_type": "image",
"type": "background",
"cdn_url": "https://cdn.example.com/assets/hero.jpg",
"storage_key": "assets/hero.jpg",
"mime_type": "image/jpeg",
"size_mb": 2.5,
"width_px": 1920,
"height_px": 1080,
"duration_sec": null,
"checksum": "abc123...",
"is_public": true,
"project": "project-uuid"
}
}
```
**Asset Types:** `image`, `video`, `audio`, `document`, `other`
**Type (Usage):** `background`, `transition`, `element`, `general`
**MIME Type Validation:**
The `mime_type` must match the `asset_type`:
| asset_type | Valid mime_type prefixes |
|------------|-------------------------|
| `image` | `image/` (jpeg, png, gif, webp, svg, etc.) |
| `video` | `video/` (mp4, webm, mov, etc.) |
| `audio` | `audio/` (mp3, wav, ogg, etc.) |
Other asset types (`document`, `other`, `file`) skip MIME validation.
**Error Response (400):**
```json
{
"message": "Invalid file type for image. Expected image (jpeg, png, gif, webp, svg, etc.), got \"video/mp4\""
}
```
---
## 10. Asset Variants
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/asset_variants` | Create variant |
| GET | `/api/asset_variants` | List variants |
| GET | `/api/asset_variants/:id` | Get variant by ID |
| PUT | `/api/asset_variants/:id` | Update variant |
| DELETE | `/api/asset_variants/:id` | Delete variant |
**Permissions**: `CREATE_ASSET_VARIANTS`, `READ_ASSET_VARIANTS`, etc.
### POST /api/asset_variants
**Request:**
```json
{
"data": {
"asset": "asset-uuid",
"variant_type": "mobile",
"cdn_url": "https://cdn.example.com/assets/hero-mobile.jpg",
"width_px": 768,
"height_px": 432,
"size_mb": 0.8
}
}
```
**Variant Types:** `desktop`, `mobile`, `thumbnail`
---
## 11. Project Audio Tracks
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/project_audio_tracks` | Create audio track |
| GET | `/api/project_audio_tracks` | List audio tracks |
| GET | `/api/project_audio_tracks/:id` | Get audio track |
| PUT | `/api/project_audio_tracks/:id` | Update audio track |
| DELETE | `/api/project_audio_tracks/:id` | Delete audio track |
**Runtime Public Access**: GET endpoints accessible without auth in production mode.
### POST /api/project_audio_tracks
**Request:**
```json
{
"data": {
"projectId": "project-uuid",
"environment": "dev",
"name": "Background Music",
"slug": "background-music",
"url": "assets/music.mp3",
"loop": true,
"volume": 0.5,
"sort_order": 1,
"is_enabled": true
}
}
```
---
## 12. Project Transition Settings
Environment-aware CSS transition settings for page navigation.
### Authentication Model
Uses **URL-path-based public access** - no headers required:
| Endpoint | Method | Environment | Auth Required |
|----------|--------|-------------|---------------|
| `/project/:id/env/production` | GET | production | **No** (public) |
| `/project/:id/env/dev` | GET | dev | JWT + READ_PAGE_ELEMENTS |
| `/project/:id/env/stage` | GET | stage | JWT + READ_PAGE_ELEMENTS |
| `/project/:id/env/*` | PUT/DELETE | any | JWT + UPDATE_PAGE_ELEMENTS |
| Standard CRUD | all | n/a | JWT + PAGE_ELEMENTS CRUD permission |
This allows public presentations (`/p/[slug]`) to fetch production transition settings without authentication in incognito mode.
### Standard CRUD Endpoints (All Require Auth)
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/project-transition-settings` | Create settings |
| GET | `/api/project-transition-settings` | List all settings |
| GET | `/api/project-transition-settings/:id` | Get by ID |
| PUT | `/api/project-transition-settings/:id` | Update by ID |
| DELETE | `/api/project-transition-settings/:id` | Delete by ID |
### Environment-Specific Endpoints
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/project/:projectId/env/production` | None | Get production settings (public) |
| GET | `/project/:projectId/env/dev` | JWT | Get dev settings |
| GET | `/project/:projectId/env/stage` | JWT | Get stage settings |
| PUT | `/project/:projectId/env/:environment` | JWT + UPDATE_PAGE_ELEMENTS | Create or update (upsert) |
| DELETE | `/project/:projectId/env/:environment` | JWT + UPDATE_PAGE_ELEMENTS | Reset settings to global defaults |
### GET /api/project-transition-settings/project/:projectId/env/:environment
**Parameters:**
- `projectId` (path): Project UUID
- `environment` (path): `dev`, `stage`, or `production`
**Authentication:**
- `production`: None required (public access)
- `dev`/`stage`: `Authorization: Bearer {token}`
**Response (200):**
```json
{
"id": "uuid",
"projectId": "project-uuid",
"environment": "production",
"transition_type": "fade",
"duration_ms": 700,
"easing": "ease-in-out",
"overlay_color": "#000000",
"createdAt": "2026-05-01T12:00:00Z",
"updatedAt": "2026-05-01T12:00:00Z"
}
```
**Response (200):** `null` when no settings exist (use global/fallback defaults)
**Response (401):** Authentication required (for dev/stage without JWT)
### PUT /api/project-transition-settings/project/:projectId/env/:environment
Creates or updates settings for a project/environment combination.
**Authentication**: Required (JWT + UPDATE_PAGE_ELEMENTS)
**Request:**
```json
{
"transition_type": "fade",
"duration_ms": 1000,
"easing": "ease-out",
"overlay_color": "#1a1a1a"
}
```
**Response (200):** Created or updated settings object
---
## 13. Global UI Controls
Configurable fullscreen, global sound, and offline buttons. Settings cascade
from global defaults to project/environment overrides and then page overrides.
### Global Defaults
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/global-ui-control-defaults` | None | Get singleton global UI-control defaults |
| GET | `/api/global-ui-control-defaults/:id` | None | Get defaults by ID |
| PUT | `/api/global-ui-control-defaults/:id` | JWT + UPDATE_PAGE_ELEMENTS | Update singleton defaults |
### Project/Environment Overrides
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/project-ui-control-settings/project/:projectId/env/production` | None or JWT for private production | Get production project overrides |
| GET | `/api/project-ui-control-settings/project/:projectId/env/dev` | JWT | Get dev project overrides |
| GET | `/api/project-ui-control-settings/project/:projectId/env/stage` | JWT | Get stage project overrides |
| PUT | `/api/project-ui-control-settings/project/:projectId/env/:environment` | JWT + UPDATE_PAGE_ELEMENTS | Upsert project overrides |
| DELETE | `/api/project-ui-control-settings/project/:projectId/env/:environment` | JWT + UPDATE_PAGE_ELEMENTS | Reset project overrides to global defaults |
`production` reads follow private-production presentation rules: public
presentations are public-readable, private presentations require JWT and an
allowlist/staff access check.
### Settings JSON
```json
{
"offline": {
"enabled": true,
"hidden": false,
"xPercent": 89.5,
"yPercent": 6,
"anchor": "center",
"buttonSizePercent": 2.6,
"iconSizePercent": 1.35,
"defaultIconUrl": "",
"activeIconUrl": "",
"defaultBackgroundColor": "#2563EB",
"activeBackgroundColor": "#059669",
"hoverBackgroundColor": "#1D4ED8",
"color": "#FFFFFF",
"defaultBorderColor": "#2563EB",
"activeBorderColor": "#059669",
"borderRadiusPercent": 0.42,
"opacity": 1,
"boxShadow": "",
"zIndex": 900,
"order": 1
}
}
```
---
## 14. Element Defaults
### Global Transition Defaults
Platform-wide default transition settings (singleton).
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/api/global-transition-defaults` | **None** | Get defaults (public) |
| GET | `/api/global-transition-defaults/:id` | **None** | Get by ID (public) |
| PUT | `/api/global-transition-defaults/:id` | JWT + UPDATE_PAGE_ELEMENTS | Update defaults |
**Authentication Model**: GET is always public (for runtime presentations), PUT requires JWT.
**GET /api/global-transition-defaults**
```http
GET /api/global-transition-defaults
# No authentication required
```
**Response (200):**
```json
{
"id": "uuid",
"transition_type": "fade",
"duration_ms": 700,
"easing": "ease-in-out",
"overlay_color": "#000000"
}
```
**PUT /api/global-transition-defaults/:id**
```http
PUT /api/global-transition-defaults/:id
Authorization: Bearer {token}
Content-Type: application/json
{
"data": {
"transition_type": "fade",
"duration_ms": 1000,
"easing": "ease-out"
}
}
```
---
### Element Type Defaults (Global)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/element-type-defaults` | List global defaults |
| GET | `/api/element-type-defaults/:id` | Get default by ID |
| PUT | `/api/element-type-defaults/:id` | Update default |
**Alternative Path:** `/api/ui-elements` (backwards compatibility)
**Element Types (11 predefined):**
- `button`, `hotspot`, `tooltip`, `gallery`
- `media_player`, `text_block`, `popup`
- `logo`, `spot`, `hamburger_menu`, `image`
---
### Project Element Defaults
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/project-element-defaults` | List project defaults |
| GET | `/api/project-element-defaults/:id` | Get default by ID |
| PUT | `/api/project-element-defaults/:id` | Update default |
| POST | `/api/project-element-defaults/:id/reset` | Reset to global |
| GET | `/api/project-element-defaults/:id/diff` | Compare with global |
### POST /api/project-element-defaults/:id/reset
Reset project element default to current global settings.
**Response:**
```json
{
"id": "uuid",
"element_type": "button",
"projectId": "project-uuid",
"default_props_json": {...}
}
```
---
### GET /api/project-element-defaults/:id/diff
Compare project default with global default.
**Response:**
```json
{
"hasDifferences": true,
"projectDefault": {...},
"globalDefault": {...},
"differences": {
"backgroundColor": {
"project": "#FF0000",
"global": "#0000FF"
}
}
}
```
---
## 15. Publishing
### POST /api/publish
Publish from stage to production.
**Authentication**: Required
**Permissions**: `CREATE_PUBLISH_EVENTS`
**Request:**
```json
{
"projectId": "project-uuid",
"title": "Version 1.0 Release",
"description": "Initial public release with all pages"
}
```
**Response:**
```json
{
"success": true,
"publishEventId": "event-uuid",
"summary": {
"pages_copied": 10,
"audios_copied": 3
}
}
```
---
### POST /api/publish/save-to-stage
Copy dev content to stage environment.
**Authentication**: Required
**Permissions**: `CREATE_PUBLISH_EVENTS`
**Request:**
```json
{
"projectId": "project-uuid"
}
```
**Response:**
```json
{
"success": true,
"publishEventId": "event-uuid",
"summary": {
"pages_copied": 10,
"audios_copied": 3
}
}
```
**Errors:**
- `400`: Publish already in progress
- `404`: Project not found
---
## 16. File Management
### GET /api/file/download
Download a file from storage. Supports automatic client disconnect handling via AbortController.
**Authentication**: None (uses private URL)
**Rate Limit**: 200/min
**Query Parameters:**
- `privateUrl`: Storage key (e.g., `assets/image.jpg`)
**Response:** File stream with appropriate Content-Type
**Error Responses:**
| HTTP Status | Condition | S3 Error Types |
|-------------|-----------|----------------|
| 400 | Missing privateUrl parameter | - |
| 401 | Expired credentials | ExpiredToken |
| 403 | Access denied | AccessDenied, InvalidAccessKeyId |
| 404 | File not found | NoSuchKey, NotFound, NoSuchBucket |
| 429 | Rate limited | ThrottlingException |
| 500 | Internal server error | InternalError |
| 503 | Service unavailable | NetworkingError, ServiceUnavailable |
| 504 | Gateway timeout | TimeoutError, RequestTimeout |
**Error Response Format:**
```json
{
"message": "Could not download the file. NoSuchKey: The specified key does not exist."
}
```
**Client Disconnect:** When a client disconnects during download, the backend automatically aborts the S3 request to prevent wasted bandwidth.
---
### POST /api/file/presign
Generate presigned URLs for direct S3 access. Includes path validation to prevent directory traversal attacks.
**Authentication**: None
**Rate Limit**: 200/min
**Request:**
```json
{
"urls": [
"assets/image1.jpg",
"assets/image2.jpg",
"assets/video.mp4"
]
}
```
**Response:**
```json
{
"presignedUrls": {
"assets/image1.jpg": "https://bucket.s3.amazonaws.com/assets/image1.jpg?...",
"assets/image2.jpg": "https://bucket.s3.amazonaws.com/assets/image2.jpg?...",
"assets/video.mp4": "https://bucket.s3.amazonaws.com/assets/video.mp4?..."
}
}
```
**Limits:**
- Maximum 50 URLs per request
- Presigned URLs expire in 1 hour (configurable via `AWS_S3_PRESIGN_EXPIRY`)
**Path Validation:**
URLs are validated to prevent path traversal and ensure security:
- Must be non-empty strings
- Must not contain `..` (parent directory traversal)
- Must not start with `/` (absolute paths)
- Must not contain null bytes
**Error Responses:**
| HTTP Status | Condition |
|-------------|-----------|
| 400 | Missing `urls` array |
| 400 | `urls` array is empty |
| 400 | `urls` exceeds maximum of 50 |
| 400 | Invalid URL format (contains `..`, starts with `/`, etc.) |
| 500 | S3 presigning failed |
| 503 | S3 service unavailable |
**Error Response Format:**
```json
{
"message": "Invalid URL format",
"code": "INVALID_PATH",
"details": { "invalidUrls": ["../etc/passwd", "/root/secret"] }
}
```
---
### POST /api/file/upload/:table/:field
Upload a file (single request).
**Authentication**: Required
**Rate Limit**: 10/min
**Content-Type**: `multipart/form-data`
**Form Fields:**
- `file`: Binary file data
- `filename`: Target filename
**Response:**
```json
{
"message": "Uploaded the file successfully: assets/hero.jpg",
"url": "https://bucket.s3.amazonaws.com/assets/hero.jpg"
}
```
---
### Chunked Upload (Large Files)
For files larger than a few MB, use chunked upload:
#### 1. Initialize Session
**POST /api/file/upload-sessions/init**
**Request:**
```json
{
"folder": "assets",
"filename": "large-video.mp4",
"totalChunks": 10,
"size": 104857600,
"contentType": "video/mp4"
}
```
**Response:**
```json
{
"sessionId": "session-uuid",
"uploadedChunks": [],
"totalChunks": 10
}
```
---
#### 2. Upload Chunks
**PUT /api/file/upload-sessions/:sessionId/chunks/:chunkIndex**
**Content-Type**: `application/octet-stream`
**Body**: Raw binary chunk data
**Response:**
```json
{
"sessionId": "session-uuid",
"chunkIndex": 0,
"uploadedChunks": 1,
"totalChunks": 10
}
```
---
#### 3. Check Session Status
**GET /api/file/upload-sessions/:sessionId**
**Response:**
```json
{
"sessionId": "session-uuid",
"totalChunks": 10,
"uploadedChunks": [0, 1, 2, 3, 4],
"status": "uploading"
}
```
---
#### 4. Finalize Upload
**POST /api/file/upload-sessions/:sessionId/finalize**
**Response:**
```json
{
"message": "Uploaded the file successfully: assets/large-video.mp4",
"privateUrl": "assets/large-video.mp4",
"url": "https://bucket.s3.amazonaws.com/assets/large-video.mp4"
}
```
---
## 17. Search
### POST /api/search
Global full-text search across entities.
**Authentication**: Required
**Rate Limit**: 30/min
**Permissions**: `READ_SEARCH` (implicit via entity permissions)
**Request:**
```json
{
"searchQuery": "museum"
}
```
**Response:**
```json
[
{
"id": "uuid",
"name": "Museum Tour",
"tableName": "projects",
"matchAttribute": ["name", "description"]
},
{
"id": "uuid",
"slug": "museum-entrance",
"tableName": "tour_pages",
"matchAttribute": ["slug"]
}
]
```
**Searchable Tables:**
| Table | Text Fields | Numeric Fields |
|-------|-------------|----------------|
| `users` | firstName, lastName, phoneNumber, email | - |
| `projects` | name, slug, description, logo_url, favicon_url, og_image_url | - |
| `assets` | name, cdn_url, storage_key, mime_type, checksum | size_mb, width_px, height_px, duration_sec |
| `asset_variants` | cdn_url | width_px, height_px, size_mb |
| `presigned_url_requests` | requested_key, mime_type, status | requested_size_mb |
| `tour_pages` | source_key, name, slug, background_image_url, background_video_url, background_audio_url, ui_schema_json | sort_order |
| `project_audio_tracks` | source_key, name, slug, url | volume, sort_order |
| `publish_events` | error_message | pages_copied, transitions_copied, audios_copied |
| `pwa_caches` | cache_version, manifest_json, asset_list_json | - |
| `access_logs` | path, ip_address, user_agent | - |
---
## 20. Access Logs
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/access_logs` | List access logs |
| GET | `/api/access_logs/count` | Count access logs |
| GET | `/api/access_logs/:id` | Get access log by ID |
**Permissions**: `READ_ACCESS_LOGS`
### GET /api/access_logs
**Query Parameters:**
- `path`: Filter by path
- `ip_address`: Filter by IP
- `user_agent`: Filter by user agent
- Standard pagination params
**Response:**
```json
{
"rows": [
{
"id": "uuid",
"path": "/p/my-tour/",
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"project": { "id": "uuid", "name": "My Tour" },
"createdAt": "2026-03-30T12:00:00.000Z"
}
],
"count": 100
}
```
---
## 21. Publish Events
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/publish_events` | List publish events |
| GET | `/api/publish_events/count` | Count publish events |
| GET | `/api/publish_events/:id` | Get publish event by ID |
**Permissions**: `READ_PUBLISH_EVENTS`
### GET /api/publish_events
**Query Parameters:**
- `project`: Filter by project ID
- `status`: Filter by status (queued, running, success, failed)
- `from_environment`: Filter by source (dev, stage)
- `to_environment`: Filter by target (stage, production)
- Standard pagination params
**Response:**
```json
{
"rows": [
{
"id": "uuid",
"title": "Version 1.0",
"description": "Initial release",
"from_environment": "stage",
"to_environment": "production",
"status": "success",
"pages_copied": 10,
"audios_copied": 3,
"started_at": "2026-03-30T12:00:00.000Z",
"finished_at": "2026-03-30T12:00:05.000Z",
"project": { "id": "uuid", "name": "My Tour" },
"user": { "id": "uuid", "firstName": "John" }
}
],
"count": 5
}
```
---
## 22. PWA Caches
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/pwa_caches` | Create PWA cache record |
| GET | `/api/pwa_caches` | List PWA caches |
| GET | `/api/pwa_caches/:id` | Get PWA cache by ID |
| PUT | `/api/pwa_caches/:id` | Update PWA cache |
| DELETE | `/api/pwa_caches/:id` | Delete PWA cache |
**Permissions**: `CREATE_PWA_CACHES`, `READ_PWA_CACHES`, etc.
### POST /api/pwa_caches
**Request:**
```json
{
"data": {
"project": "project-uuid",
"cache_version": "v1.0.0",
"manifest_json": {
"name": "My Tour",
"short_name": "Tour"
},
"asset_list_json": [
"assets/image1.jpg",
"assets/image2.jpg"
]
}
}
```
---
## 23. Presigned URL Requests
### Standard CRUD Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/presigned_url_requests` | Create presign request |
| GET | `/api/presigned_url_requests` | List presign requests |
| GET | `/api/presigned_url_requests/:id` | Get presign request |
| PUT | `/api/presigned_url_requests/:id` | Update presign request |
| DELETE | `/api/presigned_url_requests/:id` | Delete presign request |
**Permissions**: `CREATE_PRESIGNED_URL_REQUESTS`, `READ_PRESIGNED_URL_REQUESTS`, etc.
### POST /api/presigned_url_requests
**Request:**
```json
{
"data": {
"requested_key": "assets/new-upload.jpg",
"mime_type": "image/jpeg",
"requested_size_mb": 2.5,
"status": "pending",
"project": "project-uuid",
"user": "user-uuid"
}
}
```
---
## Error Codes Reference
| Code | Message | Description |
|------|---------|-------------|
| 400 | Bad Request | Invalid input data or validation error |
| 401 | Unauthorized | Missing or invalid JWT token |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Resource conflict (e.g., duplicate) |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
---
## Headers Reference
### Request Headers
| Header | Description | Required |
|--------|-------------|----------|
| `Authorization` | `Bearer <token>` | For protected endpoints |
| `Content-Type` | `application/json` | For JSON bodies |
| `X-Runtime-Environment` | `dev`, `stage`, or `production` | For runtime context |
| `X-Request-Id` | UUID for request tracing | Optional |
### Response Headers
| Header | Description |
|--------|-------------|
| `X-Request-Id` | Request ID for tracing |
| `X-RateLimit-Limit` | Rate limit maximum |
| `X-RateLimit-Remaining` | Remaining requests |
| `X-RateLimit-Reset` | Reset timestamp |
| `Retry-After` | Seconds until rate limit resets (on 429) |
| `Cross-Origin-Resource-Policy` | `cross-origin` (on file downloads) |