# 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 ``` ### 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 | | `` | string | Text search filter (ILIKE) | | `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 ` | 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) |