feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: FlatRender Studio Service (internal)
|
||||
version: 1.0.0
|
||||
description: |
|
||||
User's saved projects (the editor's state). Includes voiceover +
|
||||
audio mix settings.
|
||||
|
||||
servers:
|
||||
- url: http://studio-svc.internal/v1
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- ServiceToken: []
|
||||
|
||||
tags:
|
||||
- name: SavedProjects
|
||||
- name: SavedScenes
|
||||
- name: Audio
|
||||
- name: Internal
|
||||
|
||||
paths:
|
||||
|
||||
/saved-projects:
|
||||
get:
|
||||
tags: [SavedProjects]
|
||||
summary: List user's saved projects
|
||||
parameters:
|
||||
- { name: q, in: query, schema: { type: string } }
|
||||
- { name: type, in: query, schema: { type: string, enum: [Draft, Active, Archived, Trash] } }
|
||||
- { name: page, in: query, schema: { type: integer } }
|
||||
- { name: page_size, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { type: array, items: { $ref: '#/components/schemas/SavedProjectSummary' } }
|
||||
meta: { $ref: '#/components/schemas/PaginationMeta' }
|
||||
|
||||
post:
|
||||
tags: [SavedProjects]
|
||||
summary: Create new saved project from a template
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [original_project_id]
|
||||
properties:
|
||||
original_project_id: { type: string, format: uuid }
|
||||
name: { type: string }
|
||||
preset_story_id: { type: string, format: uuid }
|
||||
copy_default_values: { type: boolean, default: true }
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProject' }
|
||||
|
||||
/saved-projects/{id}:
|
||||
get:
|
||||
tags: [SavedProjects]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProjectFull' }
|
||||
patch:
|
||||
tags: [SavedProjects]
|
||||
summary: Update top-level fields (name, audio, etc.)
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProjectUpdate' }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProject' }
|
||||
delete:
|
||||
tags: [SavedProjects]
|
||||
summary: Soft-delete (move to trash)
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'204': { description: Trashed }
|
||||
|
||||
/saved-projects/{id}/restore:
|
||||
post:
|
||||
tags: [SavedProjects]
|
||||
summary: Restore from trash
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProject' }
|
||||
|
||||
/saved-projects/{id}/duplicate:
|
||||
post:
|
||||
tags: [SavedProjects]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
new_name: { type: string }
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProject' }
|
||||
|
||||
/saved-projects/{id}/autosave:
|
||||
put:
|
||||
tags: [SavedProjects]
|
||||
summary: Autosave entire project graph (debounced from UI)
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProjectFull' }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
saved_at: { type: string, format: date-time }
|
||||
version: { type: integer }
|
||||
|
||||
# ===================== AUDIO (NEW) =====================
|
||||
/saved-projects/{id}/audio:
|
||||
get:
|
||||
tags: [Audio]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AudioSettings' }
|
||||
put:
|
||||
tags: [Audio]
|
||||
summary: Update audio mix
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AudioSettings' }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AudioSettings' }
|
||||
|
||||
/saved-projects/{id}/voiceover:
|
||||
post:
|
||||
tags: [Audio]
|
||||
summary: |
|
||||
Upload or record voiceover. Returns target file_id.
|
||||
Use file service upload endpoints for actual binary; this
|
||||
attaches an existing file to the project.
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [file_id]
|
||||
properties:
|
||||
file_id: { type: string, format: uuid, description: existing user_file_id }
|
||||
recorded_in_browser: { type: boolean, default: false }
|
||||
volume: { type: number, minimum: 0, maximum: 1, default: 1.0 }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AudioSettings' }
|
||||
delete:
|
||||
tags: [Audio]
|
||||
summary: Remove voiceover
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'204': { description: Removed }
|
||||
|
||||
/saved-projects/{id}/music:
|
||||
put:
|
||||
tags: [Audio]
|
||||
summary: Set music track (from library or upload)
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
music_track_id: { type: string, format: uuid }
|
||||
music_file_id: { type: string, format: uuid }
|
||||
volume: { type: number, minimum: 0, maximum: 1 }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AudioSettings' }
|
||||
|
||||
# ===================== SCENES =====================
|
||||
/saved-projects/{id}/scenes:
|
||||
get:
|
||||
tags: [SavedScenes]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data: { type: array, items: { $ref: '#/components/schemas/SavedSceneFull' } }
|
||||
post:
|
||||
tags: [SavedScenes]
|
||||
summary: Add a scene from project template
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [original_scene_id]
|
||||
properties:
|
||||
original_scene_id: { type: string, format: uuid }
|
||||
sort: { type: integer }
|
||||
scene_length_sec: { type: number }
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedSceneFull' }
|
||||
|
||||
/saved-scenes/{scene_id}:
|
||||
patch:
|
||||
tags: [SavedScenes]
|
||||
parameters:
|
||||
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
scene_length_sec: { type: number }
|
||||
manual_color_selection: { type: boolean }
|
||||
selected_color_preset_id: { type: integer, format: int64 }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedScene' }
|
||||
delete:
|
||||
tags: [SavedScenes]
|
||||
parameters:
|
||||
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
responses:
|
||||
'204': { description: Removed }
|
||||
|
||||
/saved-projects/{id}/scenes/reorder:
|
||||
post:
|
||||
tags: [SavedScenes]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [ordered_ids]
|
||||
properties:
|
||||
ordered_ids:
|
||||
type: array
|
||||
items: { type: integer, format: int64 }
|
||||
responses:
|
||||
'204': { description: Reordered }
|
||||
|
||||
/saved-scenes/{scene_id}/contents:
|
||||
put:
|
||||
tags: [SavedScenes]
|
||||
summary: Bulk-update contents for a scene
|
||||
parameters:
|
||||
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
contents:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSceneContent' }
|
||||
responses:
|
||||
'200': { description: Updated }
|
||||
|
||||
/saved-scenes/{scene_id}/colors:
|
||||
put:
|
||||
tags: [SavedScenes]
|
||||
summary: Bulk-update colors for a scene
|
||||
parameters:
|
||||
- { name: scene_id, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
colors:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSceneColor' }
|
||||
responses:
|
||||
'200': { description: Updated }
|
||||
|
||||
/saved-projects/{id}/shared-colors:
|
||||
put:
|
||||
tags: [SavedProjects]
|
||||
summary: Bulk update project-level shared colors
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
colors:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSharedColor' }
|
||||
responses:
|
||||
'200': { description: Updated }
|
||||
|
||||
/saved-projects/{id}/shared-layers:
|
||||
put:
|
||||
tags: [SavedProjects]
|
||||
summary: Bulk update project-level shared layers
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
layers:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSharedLayer' }
|
||||
responses:
|
||||
'200': { description: Updated }
|
||||
|
||||
# ===================== INTERNAL =====================
|
||||
/internal/saved-projects/{id}/snapshot-for-render:
|
||||
get:
|
||||
tags: [Internal]
|
||||
summary: |
|
||||
Called by Render Orchestrator to get the full JSX-ready payload.
|
||||
Returns everything needed to generate JSX (FIX/FLEXIBLE/Mockup/MV).
|
||||
security: [ServiceToken: []]
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SavedProjectSnapshot' }
|
||||
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
|
||||
ServiceToken: { type: http, scheme: bearer }
|
||||
|
||||
schemas:
|
||||
PaginationMeta:
|
||||
type: object
|
||||
properties:
|
||||
page: { type: integer }
|
||||
page_size: { type: integer }
|
||||
total: { type: integer }
|
||||
has_more: { type: boolean }
|
||||
|
||||
SavedProjectSummary:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
name: { type: string }
|
||||
image: { type: string }
|
||||
type: { type: string }
|
||||
original_project_id: { type: string, format: uuid }
|
||||
original_project_name: { type: string }
|
||||
original_container_slug: { type: string }
|
||||
choose_mode: { type: string }
|
||||
resolution: { type: string }
|
||||
project_duration_sec: { type: number }
|
||||
scene_count: { type: integer }
|
||||
last_edit_date: { type: string, format: date-time }
|
||||
created_at: { type: string, format: date-time }
|
||||
|
||||
SavedProject:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SavedProjectSummary'
|
||||
- type: object
|
||||
properties:
|
||||
frame_rate: { type: integer }
|
||||
vip_factor: { type: number }
|
||||
manual_color_picker: { type: boolean }
|
||||
selected_preset_story_id: { type: string, format: uuid, nullable: true }
|
||||
audio: { $ref: '#/components/schemas/AudioSettings' }
|
||||
|
||||
SavedProjectUpdate:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string, enum: [Draft, Active, Archived, Trash] }
|
||||
manual_color_picker: { type: boolean }
|
||||
selected_preset_story_id: { type: string, format: uuid, nullable: true }
|
||||
last_edit_step: { type: string }
|
||||
|
||||
SavedProjectFull:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SavedProject'
|
||||
- type: object
|
||||
properties:
|
||||
scenes: { type: array, items: { $ref: '#/components/schemas/SavedSceneFull' } }
|
||||
shared_colors: { type: array, items: { $ref: '#/components/schemas/SavedSharedColor' } }
|
||||
shared_color_presets: { type: array, items: { $ref: '#/components/schemas/SavedSharedColorPreset' } }
|
||||
shared_layers: { type: array, items: { $ref: '#/components/schemas/SavedSharedLayer' } }
|
||||
|
||||
AudioSettings:
|
||||
type: object
|
||||
properties:
|
||||
music_track_id: { type: string, format: uuid, nullable: true }
|
||||
music_file_id: { type: string, format: uuid, nullable: true }
|
||||
music_url: { type: string, nullable: true }
|
||||
music_duration_sec: { type: number, nullable: true }
|
||||
music_volume: { type: number, minimum: 0, maximum: 1 }
|
||||
|
||||
voiceover_file_id: { type: string, format: uuid, nullable: true }
|
||||
voiceover_url: { type: string, nullable: true }
|
||||
voiceover_duration_sec: { type: number, nullable: true }
|
||||
voiceover_volume: { type: number, minimum: 0, maximum: 1 }
|
||||
voiceover_recorded_in_browser: { type: boolean }
|
||||
|
||||
sfx_enabled: { type: boolean }
|
||||
sfx_volume: { type: number, minimum: 0, maximum: 1 }
|
||||
|
||||
SavedScene:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
original_scene_id: { type: string, format: uuid }
|
||||
key: { type: string }
|
||||
title: { type: string }
|
||||
image: { type: string }
|
||||
demo: { type: string }
|
||||
scene_type: { type: string }
|
||||
sort: { type: integer }
|
||||
scene_length_sec: { type: number }
|
||||
min_duration_sec: { type: number }
|
||||
max_duration_sec: { type: number }
|
||||
overlap_at_end_sec: { type: number }
|
||||
manual_color_selection: { type: boolean }
|
||||
|
||||
SavedSceneFull:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SavedScene'
|
||||
- type: object
|
||||
properties:
|
||||
contents: { type: array, items: { $ref: '#/components/schemas/SavedSceneContent' } }
|
||||
colors: { type: array, items: { $ref: '#/components/schemas/SavedSceneColor' } }
|
||||
color_presets: { type: array, items: { $ref: '#/components/schemas/SavedSceneColorPreset' } }
|
||||
characters: { type: array, items: { $ref: '#/components/schemas/SavedSceneCharacter' } }
|
||||
|
||||
SavedSceneContent:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
key: { type: string }
|
||||
type: { type: string }
|
||||
value: { type: string }
|
||||
value_file_id: { type: string, format: uuid, nullable: true }
|
||||
file_url_cached: { type: string, nullable: true }
|
||||
inserted_file_type: { type: string, nullable: true }
|
||||
font_face: { type: string, nullable: true }
|
||||
font_size: { type: integer, nullable: true }
|
||||
justify: { type: string }
|
||||
position_in_container: { type: integer }
|
||||
direction_layer_value: { type: integer }
|
||||
is_text_box: { type: boolean }
|
||||
ai_input_type: { type: string, nullable: true }
|
||||
selected_dp: { type: integer, nullable: true }
|
||||
repeater_item_key: { type: string, nullable: true }
|
||||
repeater_index: { type: integer, nullable: true }
|
||||
sort: { type: integer }
|
||||
|
||||
SavedSceneColor:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
element_key: { type: string }
|
||||
title: { type: string }
|
||||
icon: { type: string }
|
||||
attr_value: { type: string }
|
||||
value: { type: string }
|
||||
is_selected: { type: boolean }
|
||||
sort: { type: integer }
|
||||
|
||||
SavedSceneColorPreset:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
is_selected: { type: boolean }
|
||||
sort: { type: integer }
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
element_key: { type: string }
|
||||
value: { type: string }
|
||||
sort: { type: integer }
|
||||
|
||||
SavedSceneCharacter:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
key: { type: string, format: uuid }
|
||||
name: { type: string }
|
||||
icon: { type: string }
|
||||
controllers:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
key: { type: string }
|
||||
value: { type: string }
|
||||
sort: { type: integer }
|
||||
|
||||
SavedSharedColor:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
element_key: { type: string }
|
||||
title: { type: string }
|
||||
attr_value: { type: string }
|
||||
value: { type: string }
|
||||
is_selected: { type: boolean }
|
||||
sort: { type: integer }
|
||||
|
||||
SavedSharedColorPreset:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
name: { type: string }
|
||||
is_selected: { type: boolean }
|
||||
sort: { type: integer }
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
element_key: { type: string }
|
||||
value: { type: string }
|
||||
|
||||
SavedSharedLayer:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
key: { type: string }
|
||||
title: { type: string }
|
||||
type: { type: string }
|
||||
value: { type: string }
|
||||
value_file_id: { type: string, format: uuid, nullable: true }
|
||||
file_url_cached: { type: string, nullable: true }
|
||||
font_face: { type: string }
|
||||
font_size: { type: integer }
|
||||
justify: { type: string }
|
||||
position_in_container: { type: integer }
|
||||
direction_layer_value: { type: integer }
|
||||
is_text_box: { type: boolean }
|
||||
sort: { type: integer }
|
||||
|
||||
SavedProjectSnapshot:
|
||||
type: object
|
||||
description: |
|
||||
Complete payload for JSX generation. Same shape returned for any
|
||||
choose_mode; render service decides which JSX generator to use.
|
||||
properties:
|
||||
saved_project_id: { type: string, format: uuid }
|
||||
tenant_id: { type: string, format: uuid }
|
||||
user_id: { type: string, format: uuid }
|
||||
original_project_id: { type: string, format: uuid }
|
||||
original_project_name: { type: string }
|
||||
choose_mode: { type: string }
|
||||
resolution: { type: string }
|
||||
frame_rate: { type: integer }
|
||||
project_duration_sec: { type: number }
|
||||
vip_factor: { type: number }
|
||||
|
||||
aep:
|
||||
type: object
|
||||
properties:
|
||||
url: { type: string }
|
||||
md5: { type: string }
|
||||
size_bytes: { type: integer, format: int64 }
|
||||
render_comp: { type: string }
|
||||
original_width: { type: integer }
|
||||
original_height: { type: integer }
|
||||
|
||||
audio: { $ref: '#/components/schemas/AudioSettings' }
|
||||
|
||||
shared_colors:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSharedColor' }
|
||||
|
||||
shared_layers:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSharedLayer' }
|
||||
|
||||
scenes:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/SavedSceneFull' }
|
||||
Reference in New Issue
Block a user