Files
csbot-prototype/src/openapi.ts
T
JiriUhlirandClaude Opus 5 78e7f99d60 Konektory do Postgresu, pristupove udaje sifrovane
Pristupove udaje konektoru se ukladaji do databaze a prezijou restart. Popis
v documentation/14-databaze.md.

Databaze je volitelna a rezimy jsou oddelene:
- postgres kdyz je DATABASE_URL i SECRETS_KEY
- memory jinak, tedy pri nasazenem mockupu a lokalnim vyvoji bez DB

Rozhodnuti je jen na jednom miste (src/data/connectorStore.ts). Nikde jinde se
nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se
zapomnelo a chovalo by se pak jinak nez zbytek.

Chybejici databaze nesmi shodit start: container, ktery nenastartuje, je pro
AppFactory nefunkcni sluzba. Misto toho se do logu napise proc a portal to ukaze
na strance Konektory. Stejne tak kdyz migrace selzou - psat do rozbiteho
schematu je horsi nez neukladat.

Databaze potrebuje oboji. Bez SECRETS_KEY by se udaje ukladaly v plaintextu
a to je horsi nez ztratit je pri restartu: tabulku vidi kazda zaloha a kazdy
dump pri ladeni.

Pridano:
- pool v src/db/pool.ts vcetne transakci a dbFor(tenantId) jako sev pro budouci
  oddelenou databazi jednoho klienta
- migrace ze src/db/migrations/*.sql pod pg_advisory_lock, jinak je pri rolling
  deployi pusti vsechny instance naraz. Jeden soubor je jedna transakce
- sifrovani AES-256-GCM s nahodnym IV a verzi klice. Nerozsifrovatelna hodnota
  nepada, chova se jako nevyplnena a zaloguje se - jeden rozbity konektor nesmi
  shodit seznam ostatnich
- /health/ready s pingem do DB. /health na databazi zamerne nezavisi, kratky
  vypadek by jinak vedl k restartovani containeru
- GET /api/dashboard/storage a hlaska v portalu o tom, ze data jsou jen v pameti
- jediny vychozi konektor na firmu a sluzbu hlida castecny unikatni index, ne jen
  kod. Dva soubezne zapisy by jinak udelaly dva vychozi

Zmeneno: cteni i zapis konektoru je asynchronni, vcetne validace stromu.

Overeno proti Postgresu 16 v kontejneru: migrace, sifrovani v tabulce, preziti
restartu, rozsifrovani spravnym klicem, degradace pri spatnem klici, PATCH bez
tajneho pole, prepnuti a smazani vychoziho konektoru, pametovy rezim bez
DATABASE_URL. Kontejner po overeni smazan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:08:25 +02:00

1626 lines
58 KiB
TypeScript

import { config } from './config.js';
/**
* OpenAPI popis API.
*
* `servers` MUSI obsahovat prefix reverse proxy, jinak Swagger "Try it out"
* vola endpointy na root domene a dostane 404 (viz AGENTS.md).
* Prefix se bere z ROOT_PATH, nikdy se nehardcoduje.
*/
export function buildOpenApiDocument() {
const server = config.rootPath === '' ? '/' : config.rootPath;
return {
openapi: '3.0.3',
info: {
title: 'Automia - portal a API',
version: '1.0.0',
description:
'Webova prezentace a klientsky portal. Automatizace, voiceboti, integrace, ' +
'tickety a incidenty. Aplikace bezi za reverse proxy AppFactory.',
},
servers: [{ url: server, description: 'Verejna adresa vcetne prefixu proxy' }],
tags: [
{ name: 'Provoz', description: 'Health a zakladni informace' },
{ name: 'Autentizace', description: 'Prihlaseni do portalu' },
{ name: 'Dashboard', description: 'Data klientskeho portalu' },
{ name: 'Tickety', description: 'Pozadavky, jejich resitele a log prubehu' },
{ name: 'Automatizace', description: 'Sprava automatizaci a stromu akci' },
{ name: 'Sluzby', description: 'Katalog toho, co umime napojit' },
{ name: 'Konektory', description: 'Napojeni firmy na sluzbu vcetne pristupovych udaju' },
{ name: 'Skripty', description: 'Vykonna cast sluzby: manifest, kod a zkusebni beh' },
{ name: 'Simulace', description: 'Vyvolani provoznich udalosti pro nahled' },
{ name: 'Webhook', description: 'Verejny prijem dat do automatizace' },
{ name: 'Kontakt', description: 'Poptavkovy formular z webu' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Token z POST /api/auth/login. Vlozte samotny token bez slova Bearer.',
},
},
schemas: {
Error: {
type: 'object',
properties: {
error: { type: 'string', example: 'validation_error' },
message: { type: 'string', example: 'Zadejte platny e-mail.' },
},
},
User: {
type: 'object',
description:
'Uzivatel muze patrit do vic firem. Role je vzdy az uvnitr firmy, ' +
'pristup napric firmami je zvlast jako platformAdmin.',
properties: {
id: { type: 'string', example: 'usr_1' },
email: { type: 'string', example: 'admin@automia.cz' },
name: { type: 'string', example: 'Jiri Uhlir' },
platformAdmin: {
type: 'boolean',
description: 'Vidi napric vsemi firmami a muze mezi nimi prepinat.',
},
memberships: {
type: 'array',
items: {
type: 'object',
properties: {
tenantId: { type: 'string', example: 'tnt_automia' },
role: { type: 'string', enum: ['admin', 'agent'] },
},
},
},
},
},
Access: {
type: 'object',
description: 'Co uzivatel smi. Klient podle toho kresli prepinac pohledu.',
properties: {
scopes: {
type: 'array',
items: { type: 'string', enum: ['all', 'tenant', 'mine'] },
},
tenants: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string', example: 'tnt_automia' },
name: { type: 'string', example: 'Automia' },
},
},
},
defaultTenantId: { type: 'string', nullable: true },
canAssignOthers: {
type: 'boolean',
description: 'Smi prehazovat tickety mezi lidmi, ne jen brat na sebe.',
},
personId: { type: 'string', nullable: true },
},
},
Connector: {
type: 'object',
description:
'Napojeni firmy na jednu sluzbu. Hodnoty pristupovych udaju tady zamerne ' +
'nejsou a nikdy nebudou - secrets se z beznych endpointu nevraci.',
properties: {
id: { type: 'string', example: 'con_1a2b3c4d' },
tenantId: { type: 'string', example: 'tnt_automia' },
serviceId: { type: 'string', example: 'idoklad' },
name: { type: 'string', example: 'iDoklad Automia' },
baseUrl: { type: 'string', nullable: true },
enabled: { type: 'boolean' },
status: { type: 'string', enum: ['untested', 'ok', 'error'] },
lastCheckAt: { type: 'string', format: 'date-time', nullable: true },
lastError: { type: 'string', nullable: true },
isDefault: {
type: 'boolean',
description: 'Krok stromu bez vybraneho konektoru pouzije tenhle.',
},
filled: {
type: 'array',
items: { type: 'string' },
description: 'ID poli, ktera jsou vyplnena. Hodnoty se nevraci.',
},
missing: {
type: 'array',
items: { type: 'string' },
description: 'ID povinnych poli, ktera chybi.',
},
config: {
type: 'object',
additionalProperties: { type: 'string' },
description: 'Necitliva nastaveni. Tajna pole tu nejsou vubec.',
},
ready: { type: 'boolean' },
},
},
ScriptField: {
type: 'object',
description:
'Parametr skriptu. Stejny tvar pro vstup i vystup - kontrola je pak ' +
'jedna funkce, ne dve skoro stejne.',
required: ['id', 'label', 'type', 'required'],
properties: {
id: {
type: 'string',
example: 'invoiceId',
description: 'Pouziva se v sablonach jako {{invoiceId}}.',
},
label: { type: 'string', example: 'ID faktury v iDokladu' },
type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] },
required: { type: 'boolean' },
hint: { type: 'string' },
options: {
type: 'array',
description: 'Vyber z hodnot. Jina hodnota neprojde kontrolou.',
items: {
type: 'object',
properties: { value: { type: 'string' }, label: { type: 'string' } },
},
},
pattern: { type: 'string', description: 'Jen u typu string.' },
multiline: { type: 'boolean', description: 'Jen u typu string.' },
default: { description: 'Dosadi se, kdyz hodnota chybi a parametr neni povinny.' },
},
},
ScriptManifest: {
type: 'object',
description: 'Co skript umi. Podle nej s nim umi pracovat strom automatizace.',
properties: {
id: {
type: 'string',
example: 'idoklad.get-issued-invoice',
description: 'Tvar sluzba.operace. Nazev souboru musi byt <id>.js.',
},
serviceId: { type: 'string', example: 'idoklad' },
serviceName: { type: 'string', example: 'iDoklad' },
operationId: { type: 'string', example: 'get-issued-invoice' },
name: { type: 'string', example: 'Získat vydanou fakturu' },
description: { type: 'string' },
inputs: { type: 'array', items: { $ref: '#/components/schemas/ScriptField' } },
outputs: { type: 'array', items: { $ref: '#/components/schemas/ScriptField' } },
timeoutMs: { type: 'integer', example: 15000 },
},
},
ScriptProblem: {
type: 'object',
description:
'Rozbity skript. Nesmi shodit ostatni ani tise zmizet, proto se vraci sem.',
properties: {
file: { type: 'string', example: 'idoklad.get-issued-invoice.js' },
scriptId: { type: 'string', nullable: true },
message: { type: 'string' },
issues: {
type: 'array',
items: {
type: 'object',
properties: { field: { type: 'string' }, message: { type: 'string' } },
},
},
},
},
ConnectionStatus: {
type: 'object',
description:
'Stav napojeni konektoru. Hodnoty pristupovych udaju se nevraci nikdy, ' +
'jen jmena promennych, ktere chybi.',
properties: {
connectorId: { type: 'string', example: 'idoklad' },
baseUrl: { type: 'string', example: 'https://services.csbot.cz/apps/idoklad' },
ready: { type: 'boolean' },
missing: {
type: 'array',
items: { type: 'string' },
example: ['IDOKLAD_CLIENT_SECRET'],
},
headers: { type: 'array', items: { type: 'string' }, example: ['X-ClientId'] },
},
},
ScriptRunResult: {
type: 'object',
description:
'Vysledek behu skriptu. `retryable` rika, jestli ma smysl zkusit to znovu - ' +
'timeout ano, spatny vstup ne.',
properties: {
ok: { type: 'boolean' },
scriptId: { type: 'string' },
outputs: {
type: 'object',
additionalProperties: true,
description: 'Prazdne, kdyz beh selhal.',
},
logs: {
type: 'array',
items: {
type: 'object',
properties: {
at: { type: 'string', format: 'date-time' },
message: { type: 'string' },
detail: { type: 'string' },
},
},
},
durationMs: { type: 'integer' },
httpCalls: { type: 'integer' },
error: {
type: 'object',
nullable: true,
properties: {
kind: {
type: 'string',
enum: [
'not_found',
'config',
'validation',
'output',
'retryable',
'terminal',
'timeout',
'internal',
],
},
message: { type: 'string' },
retryable: { type: 'boolean' },
status: { type: 'integer' },
detail: { type: 'string' },
issues: {
type: 'array',
items: {
type: 'object',
properties: { field: { type: 'string' }, message: { type: 'string' } },
},
},
},
},
},
},
LoginRequest: {
type: 'object',
required: ['email', 'password'],
properties: {
email: { type: 'string', format: 'email', example: 'admin@automia.cz' },
password: { type: 'string', format: 'password', example: 'demo1234' },
},
},
LoginResponse: {
type: 'object',
properties: {
token: { type: 'string' },
user: { $ref: '#/components/schemas/User' },
},
},
Person: {
type: 'object',
description: 'Resitel ticketu. Nemusi mit ucet v portalu, spojka je e-mail.',
properties: {
id: { type: 'string', example: 'ppl_vomacka' },
name: { type: 'string', example: 'Karel Vomacka' },
email: { type: 'string', format: 'email' },
role: { type: 'string', example: 'Servicedesk' },
capacity: {
type: 'integer',
description: 'Kolik nevyrizenych ticketu je pro nej jeste zdrava zatez.',
},
},
},
TicketCustomer: {
type: 'object',
properties: {
id: {
type: 'string',
nullable: true,
description: 'ID firmy v CRM. null = zakaznika se nepodarilo dohledat.',
example: 'crm_1042',
},
company: { type: 'string', example: 'Firma s.r.o.' },
contact: { type: 'string', example: 'Petra Klientova' },
reply: {
type: 'string',
description: 'Adresa nebo cislo, odkud pozadavek prisel a kam se odpovida.',
},
},
},
Ticket: {
type: 'object',
properties: {
id: { type: 'string', example: 'TK-4821' },
subject: { type: 'string' },
body: {
type: 'string',
description:
'Cely text pozadavku. Prazdny retezec = krok "Zalozit ticket" obsah nenaplnil.',
},
sourceRef: {
type: 'string',
nullable: true,
description: 'Odkaz na zdrojovou zpravu u poskytovatele.',
example: 'wamid.HBgLNDIwNzc0OTAyMzMx',
},
channel: {
type: 'string',
enum: ['whatsapp', 'facebook', 'instagram', 'email', 'voice', 'form', 'portal'],
description: 'Odkud pozadavek prisel.',
},
customer: { $ref: '#/components/schemas/TicketCustomer' },
status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] },
assignee: {
type: 'object',
nullable: true,
description: 'Kdo ma ticket u sebe. null = ceka ve fronte.',
properties: {
id: { type: 'string', example: 'ppl_vomacka' },
name: { type: 'string', example: 'Karel Vomacka' },
},
},
automationId: {
type: 'string',
nullable: true,
description: 'Automatizace, ktera ticket zalozila. null = zalozeno rucne.',
},
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
},
TicketTraceEntry: {
type: 'object',
description:
'Jeden radek logu ticketu. Strom se sklada pres parentId - vetev podminky ' +
'visi na zaznamu te podminky.',
properties: {
id: { type: 'string', example: 'tr_12' },
parentId: {
type: 'string',
nullable: true,
description: 'null = zaznam v hlavni sekvenci.',
},
kind: { type: 'string', enum: ['trigger', 'action', 'condition', 'note'] },
connectorId: { type: 'string', nullable: true, example: 'raynet' },
operationId: { type: 'string', nullable: true, example: 'upsert-contact' },
label: { type: 'string' },
status: { type: 'string', enum: ['ok', 'error', 'skipped', 'info'] },
response: {
type: 'string',
nullable: true,
description: 'Co sluzba vratila. Kvuli tomuhle log existuje.',
},
durationMs: { type: 'integer', nullable: true },
at: { type: 'string', format: 'date-time' },
},
},
TicketDetail: {
allOf: [
{ $ref: '#/components/schemas/Ticket' },
{
type: 'object',
properties: {
trace: {
type: 'array',
items: { $ref: '#/components/schemas/TicketTraceEntry' },
},
},
},
],
},
Workload: {
type: 'object',
description: 'Prehled nad firmou - kdo ma kolik ticketu u sebe.',
properties: {
rows: {
type: 'array',
items: {
type: 'object',
properties: {
person: { $ref: '#/components/schemas/Person' },
open: { type: 'integer', description: 'Nevyresene tickety.' },
total: { type: 'integer' },
critical: { type: 'integer' },
oldestOpenAt: { type: 'string', format: 'date-time', nullable: true },
overloaded: { type: 'boolean' },
},
},
},
unassigned: { type: 'integer', description: 'Nevyresene tickety bez resitele.' },
openTotal: { type: 'integer' },
},
},
Incident: {
type: 'object',
properties: {
id: { type: 'string', example: 'INC-231' },
title: { type: 'string' },
service: { type: 'string' },
severity: { type: 'string', enum: ['sev1', 'sev2', 'sev3'] },
status: {
type: 'string',
enum: ['investigating', 'identified', 'monitoring', 'resolved'],
},
startedAt: { type: 'string', format: 'date-time' },
resolvedAt: { type: 'string', format: 'date-time', nullable: true },
},
},
TriggerField: {
type: 'object',
required: ['id', 'name', 'type', 'required'],
properties: {
id: { type: 'string', example: 'f_42' },
name: {
type: 'string',
example: 'score',
description: 'Klic v prichozich datech, pismena, cislice a podtrzitko.',
},
type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] },
required: { type: 'boolean' },
},
},
FlowStep: {
type: 'object',
description:
'Krok stromu. Bud akce nad konektorem, nebo podminka se dvema vetvemi.',
properties: {
id: { type: 'string' },
kind: { type: 'string', enum: ['action', 'condition'] },
connectorId: { type: 'string', example: 'email' },
operationId: { type: 'string', example: 'send' },
inputs: {
type: 'object',
additionalProperties: { type: 'string' },
description:
'Nastaveni akce. Klic je ID pole z katalogu, hodnota je sablona - ' +
'{{nazev}} se nahradi parametrem spoustece. Neznamy klic vraci 400.',
example: { subject: 'Reklamace od {{profileName}}', body: '{{text}}' },
},
fieldId: { type: 'string', example: 'f_42' },
operator: {
type: 'string',
enum: [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'contains',
'startsWith',
'isEmpty',
'isNotEmpty',
'isTrue',
'isFalse',
],
},
value: { type: 'string', example: '15' },
yes: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } },
no: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } },
},
},
AutomationFlow: {
type: 'object',
properties: {
trigger: {
type: 'object',
nullable: true,
properties: {
connectorId: { type: 'string', example: 'webhook' },
operationId: { type: 'string', example: 'received' },
fields: {
type: 'array',
items: { $ref: '#/components/schemas/TriggerField' },
},
webhookToken: {
type: 'string',
readOnly: true,
description: 'Generuje vyhradne server, hodnota od klienta se ignoruje.',
},
},
},
steps: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } },
},
},
Automation: {
type: 'object',
properties: {
id: { type: 'string', example: 'AUT-01' },
name: { type: 'string' },
kind: { type: 'string', enum: ['workflow', 'voicebot', 'integrace', 'report'] },
enabled: { type: 'boolean' },
runsToday: { type: 'integer' },
successRate: { type: 'number' },
avgDurationMs: { type: 'integer' },
lastRunAt: { type: 'string', format: 'date-time' },
stepCount: { type: 'integer' },
configured: { type: 'boolean' },
issues: {
type: 'array',
items: { type: 'string' },
description: 'Co chybi k zapnuti. Prazdne pole znamena hotovo.',
},
},
},
AutomationDetail: {
allOf: [
{ $ref: '#/components/schemas/Automation' },
{
type: 'object',
properties: {
flow: { $ref: '#/components/schemas/AutomationFlow' },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
},
],
},
},
},
paths: {
'/health': {
get: {
tags: ['Provoz'],
summary: 'Health check',
description: 'Vraci 200, pokud je aplikace schopna prijimat provoz.',
responses: {
'200': {
description: 'Aplikace bezi',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'ok' },
uptimeSec: { type: 'integer', example: 42 },
},
},
},
},
},
},
},
},
'/api/auth/login': {
post: {
tags: ['Autentizace'],
summary: 'Prihlaseni',
requestBody: {
required: true,
content: {
'application/json': { schema: { $ref: '#/components/schemas/LoginRequest' } },
},
},
responses: {
'200': {
description: 'Token a udaje uzivatele',
content: {
'application/json': { schema: { $ref: '#/components/schemas/LoginResponse' } },
},
},
'401': {
description: 'Nespravny e-mail nebo heslo',
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
},
},
},
},
'/api/auth/me': {
get: {
tags: ['Autentizace'],
summary: 'Prihlaseny uzivatel',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Udaje uzivatele',
content: {
'application/json': {
schema: {
type: 'object',
properties: { user: { $ref: '#/components/schemas/User' } },
},
},
},
},
'401': { description: 'Chybi nebo neplatny token' },
},
},
},
'/api/auth/logout': {
post: {
tags: ['Autentizace'],
summary: 'Odhlaseni',
security: [{ bearerAuth: [] }],
responses: { '204': { description: 'Odhlaseno' } },
},
},
'/api/dashboard/summary': {
get: {
tags: ['Dashboard'],
summary: 'Souhrn pro prehled',
security: [{ bearerAuth: [] }],
responses: { '200': { description: 'Souhrnne metriky a casova rada' } },
},
},
'/api/dashboard/widgets': {
get: {
tags: ['Dashboard'],
summary: 'Katalog widgetu prehledu',
description: 'Co jde polozit na dashboard vcetne povolenych sirek.',
security: [{ bearerAuth: [] }],
responses: { '200': { description: 'Widgety' } },
},
},
'/api/dashboard/layout': {
get: {
tags: ['Dashboard'],
summary: 'Rozlozeni dashboardu',
description:
'Uklada se pro dvojici uzivatel a firma. `custom: false` znamena, ' +
'ze uzivatel kouka na vychozi rozlozeni.',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'tenantId',
in: 'query',
schema: { type: 'string' },
description: 'Firma. Bez ni se pouzije prvni, do ktere uzivatel patri.',
},
],
responses: {
'200': { description: 'Rozlozeni' },
'403': { description: 'Ucet nepatri do zadne firmy' },
'404': { description: 'Firma neexistuje, nebo do ni uzivatel nepatri' },
},
},
put: {
tags: ['Dashboard'],
summary: 'Ulozit rozlozeni',
description: 'Overuje se proti katalogu. Neznamy widget nebo sirka vraci 400.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['items'],
properties: {
items: {
type: 'array',
items: {
type: 'object',
required: ['id', 'widgetId', 'size'],
properties: {
id: { type: 'string', example: 'w1' },
widgetId: { type: 'string', example: 'stat.openTickets' },
size: { type: 'string', enum: ['third', 'half', 'full'] },
},
},
},
},
},
},
},
},
responses: {
'200': { description: 'Ulozeno' },
'400': { description: 'Neplatne rozlozeni' },
},
},
delete: {
tags: ['Dashboard'],
summary: 'Vratit na vychozi rozlozeni',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }],
responses: { '200': { description: 'Vychozi rozlozeni' } },
},
},
'/api/dashboard/access': {
get: {
tags: ['Dashboard'],
summary: 'Co uzivatel smi videt',
description:
'Povolene pohledy, firmy k prepinani a prava. Klient si to nesmi dovozovat sam.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Opravneni',
content: {
'application/json': { schema: { $ref: '#/components/schemas/Access' } },
},
},
},
},
},
'/api/dashboard/people': {
get: {
tags: ['Tickety'],
summary: 'Seznam resitelu',
description:
'Lide, na ktere jde ticket priradit. `meId` je resitel odpovidajici ' +
'prihlasenemu uzivateli, nebo null, pokud zadny neni.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Resitele',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: { type: 'array', items: { $ref: '#/components/schemas/Person' } },
meId: { type: 'string', nullable: true },
},
},
},
},
},
},
},
},
'/api/dashboard/tickets': {
get: {
tags: ['Tickety'],
summary: 'Seznam ticketu',
description: 'Neznama hodnota filtru se ignoruje a zaloguje, seznam se nezuzi.',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'scope',
in: 'query',
schema: { type: 'string', enum: ['all', 'tenant', 'mine'] },
description:
'`all` napric firmami (jen platformni admin), `tenant` cela firma, ' +
'`mine` jen moje tickety. Nepovoleny pohled vraci 403, nikdy se tise nezuzi.',
},
{
name: 'tenantId',
in: 'query',
schema: { type: 'string' },
description: 'Firma u pohledu `tenant` a `mine`. Bez clenstvi vraci 404.',
example: 'tnt_automia',
},
{
name: 'assignee',
in: 'query',
schema: { type: 'string' },
description:
'ID resitele nebo `unassigned` pro frontu. U pohledu `mine` se ignoruje.',
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] },
},
{
name: 'channel',
in: 'query',
schema: { type: 'string', enum: ['whatsapp', 'email', 'voice', 'form', 'portal'] },
},
],
responses: {
'200': {
description: 'Tickety',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: { type: 'array', items: { $ref: '#/components/schemas/Ticket' } },
meId: { type: 'string', nullable: true },
},
},
},
},
},
},
},
},
'/api/dashboard/tickets/workload': {
get: {
tags: ['Tickety'],
summary: 'Kdo co ma u sebe',
description: 'Prehled zateze pres cely tym vcetne poctu ticketu ve fronte.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Vytizeni resitelu',
content: {
'application/json': { schema: { $ref: '#/components/schemas/Workload' } },
},
},
},
},
},
'/api/dashboard/tickets/{id}': {
get: {
tags: ['Tickety'],
summary: 'Detail ticketu vcetne logu',
description: 'Log obsahuje i to, co ktera volana sluzba vratila.',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'TK-4821',
},
],
responses: {
'200': {
description: 'Detail',
content: {
'application/json': { schema: { $ref: '#/components/schemas/TicketDetail' } },
},
},
'404': { description: 'Neexistuje' },
},
},
},
'/api/dashboard/tickets/{id}/assign': {
post: {
tags: ['Tickety'],
summary: 'Priradit resitele',
description: 'Poslete null pro vraceni ticketu do fronty.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['assigneeId'],
properties: {
assigneeId: { type: 'string', nullable: true, example: 'ppl_vomacka' },
},
},
},
},
},
responses: {
'200': {
description: 'Prirazeno',
content: {
'application/json': { schema: { $ref: '#/components/schemas/Ticket' } },
},
},
'400': { description: 'Chybi assigneeId' },
'404': { description: 'Ticket nebo resitel neexistuje' },
},
},
},
'/api/dashboard/tickets/{id}/status': {
post: {
tags: ['Tickety'],
summary: 'Zmenit stav ticketu',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['status'],
properties: {
status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] },
},
},
},
},
},
responses: {
'200': {
description: 'Zmeneno',
content: {
'application/json': { schema: { $ref: '#/components/schemas/Ticket' } },
},
},
'400': { description: 'Neplatny stav' },
'404': { description: 'Neexistuje' },
},
},
},
'/api/dashboard/tickets/{id}/comment': {
post: {
tags: ['Tickety'],
summary: 'Pridat komentar',
description: 'Komentar je dalsi radek logu, aby bylo vse na jedne casove ose.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['text'],
properties: { text: { type: 'string', minLength: 2 } },
},
},
},
},
responses: {
'200': {
description: 'Zapsano',
content: {
'application/json': { schema: { $ref: '#/components/schemas/Ticket' } },
},
},
'400': { description: 'Prazdny komentar' },
'404': { description: 'Neexistuje' },
},
},
},
'/api/dashboard/incidents': {
get: {
tags: ['Dashboard'],
summary: 'Seznam incidentu',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Incidenty',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: { type: 'array', items: { $ref: '#/components/schemas/Incident' } },
},
},
},
},
},
},
},
},
'/api/dashboard/stream': {
get: {
tags: ['Dashboard'],
summary: 'Zivy stream zmen (SSE)',
description:
'Server-Sent Events. Drzi otevrene spojeni a posila udalosti, jakmile nastanou. ' +
'Swagger UI streamovanou odpoved nezobrazi rozumne, testujte prohlizecem nebo curl.',
security: [{ bearerAuth: [] }],
responses: { '200': { description: 'Proud udalosti text/event-stream' } },
},
},
'/health/ready': {
get: {
tags: ['Provoz'],
summary: 'Readiness vcetne databaze',
description:
'Vraci 503, kdyz je databaze nastavena a nedostupna. `/health` na databazi ' +
'zamerne nezavisi - kratky vypadek DB by jinak vedl k restartovani containeru.',
responses: {
'200': { description: 'Aplikace je pripravena' },
'503': { description: 'Databaze je nastavena, ale nedostupna' },
},
},
},
'/api/dashboard/storage': {
get: {
tags: ['Dashboard'],
summary: 'Kam se uklada',
description:
'mode postgres nebo memory. `ephemeral: true` znamena, ze restart procesu ' +
'data smaze. Portal to musi umet rict nahlas.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Rezim uloziste',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
mode: { type: 'string', enum: ['postgres', 'memory'] },
reason: { type: 'string', nullable: true },
ephemeral: { type: 'boolean' },
},
},
},
},
},
},
},
},
'/api/dashboard/services': {
get: {
tags: ['Sluzby'],
summary: 'Katalog sluzeb pro builder',
description:
'Vraci jen sluzby, ktere uzivatel vidi. Neviditelna sluzba v odpovedi neni ' +
'vubec, ne se stavem "nemate pravo". Operace, ktere obsluhuje skript, nesou ' +
'implementation: script a maji skutecne inputs a outputFields.',
security: [{ bearerAuth: [] }],
responses: { '200': { description: 'Sluzby, kategorie a operatory podminek' } },
},
},
'/api/dashboard/connectors/services': {
get: {
tags: ['Sluzby'],
summary: 'Katalog sluzeb ocima firmy',
description:
'Jako /services, navic connectorCount, tedy kolik konektoru na sluzbu firma ma. ' +
'Podle toho se rozlisi napojeno od muzete si napojit. Neni to vlastnost sluzby, ' +
'ale te firmy.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }],
responses: { '200': { description: 'Sluzby vcetne pouziti ve firme' } },
},
},
'/api/dashboard/connectors': {
get: {
tags: ['Konektory'],
summary: 'Konektory firmy',
description:
'Hodnoty pristupovych udaju se NIKDY nevraci, jen filled (co je vyplnene) ' +
'a missing (ktera povinna pole chybi).',
security: [{ bearerAuth: [] }],
parameters: [
{ name: 'tenantId', in: 'query', schema: { type: 'string' } },
{ name: 'serviceId', in: 'query', schema: { type: 'string' } },
],
responses: {
'200': {
description: 'Konektory firmy',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: {
type: 'array',
items: { $ref: '#/components/schemas/Connector' },
},
tenantId: { type: 'string' },
},
},
},
},
},
},
},
post: {
tags: ['Konektory'],
summary: 'Zalozit konektor',
description:
'Pristupove udaje se posilaji ve values s klici podle Service.credentials. ' +
'Nevyplnene povinne pole neni chyba, konektor se ulozi a jen nepujde pouzit.',
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['serviceId', 'name'],
properties: {
serviceId: { type: 'string', example: 'idoklad' },
name: { type: 'string', example: 'iDoklad Celo' },
baseUrl: { type: 'string', nullable: true },
values: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
},
},
},
},
responses: {
'201': {
description: 'Zalozeno',
content: {
'application/json': { schema: { $ref: '#/components/schemas/Connector' } },
},
},
'400': { description: 'Obecna sluzba konektor nepotrebuje, nebo nezname pole' },
'404': { description: 'Sluzba neexistuje nebo ji uzivatel nevidi' },
},
},
},
'/api/dashboard/connectors/{id}': {
get: {
tags: ['Konektory'],
summary: 'Detail konektoru',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
responses: { '200': { description: 'Konektor' }, '404': { description: 'Neexistuje' } },
},
patch: {
tags: ['Konektory'],
summary: 'Upravit konektor',
description:
'Ve values staci poslat jen to, co se meni. PRAZDNY RETEZEC hodnotu smaze, ' +
'chybejici klic ji nechava - diky tomu jde ulozit formular, ktery tajne hodnoty ' +
'neposila. Zmena udaju vzdy zrusi predchozi overeni.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: { type: 'string' },
baseUrl: { type: 'string', nullable: true },
values: { type: 'object', additionalProperties: { type: 'string' } },
enabled: { type: 'boolean' },
isDefault: { type: 'boolean', enum: [true] },
},
},
},
},
},
responses: { '200': { description: 'Upraveno' }, '404': { description: 'Neexistuje' } },
},
delete: {
tags: ['Konektory'],
summary: 'Smazat konektor',
description:
'Kdyz zmizel vychozi konektor, prevezme to prvni zbyly - jinak by kroky bez ' +
'vybraneho konektoru prestaly fungovat.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' } },
},
},
'/api/dashboard/connectors/{id}/test': {
post: {
tags: ['Konektory'],
summary: 'Overit napojeni',
description:
'Zavola verifyPath sluzby, coz je zamerne cteci volani vyzadujici autorizaci. ' +
'Kdyz ho sluzba nema, overi se jen /health a odpoved to v checked rekne - aby ' +
'si nikdo nemyslel, ze jsou overene i pristupove udaje. Neuspesne overeni neni ' +
'chyba API, vraci se 200 s ok: false.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
responses: {
'200': {
description: 'Vysledek overeni',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
ok: { type: 'boolean' },
checked: { type: 'string' },
status: { type: 'integer' },
message: { type: 'string' },
},
},
},
},
},
'404': { description: 'Konektor neexistuje' },
},
},
},
'/api/dashboard/scripts': {
get: {
tags: ['Skripty'],
summary: 'Seznam skriptu konektoru',
description:
'Manifesty vsech nactenych skriptu, rozbite skripty v `problems` ' +
'a stav napojeni v `connections`. Pristupove udaje se nikdy nevraci, ' +
'jen jmena chybejicich environment variables.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Skripty, problemy a stav napojeni',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: {
type: 'array',
items: { $ref: '#/components/schemas/ScriptManifest' },
},
problems: {
type: 'array',
items: { $ref: '#/components/schemas/ScriptProblem' },
},
connections: {
type: 'array',
items: { $ref: '#/components/schemas/ConnectionStatus' },
},
directory: { type: 'string', example: '/app/scripts' },
},
},
},
},
},
},
},
},
'/api/dashboard/scripts/reload': {
post: {
tags: ['Skripty'],
summary: 'Znovu nacist skripty ze slozky',
description:
'Skripty se nacitaji samy podle casu zmeny souboru. Tenhle endpoint ' +
'to jen vynuti hned, bez cekani.',
security: [{ bearerAuth: [] }],
responses: {
'200': { description: 'Skripty po nacteni' },
'403': { description: 'Jen spravce platformy' },
},
},
},
'/api/dashboard/scripts/{id}': {
get: {
tags: ['Skripty'],
summary: 'Manifest a kod skriptu',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'idoklad.get-issued-invoice',
},
],
responses: {
'200': {
description: 'Kod se vraci vzdy. `manifest` je null, kdyz je skript rozbity.',
},
'400': { description: 'Neplatne ID skriptu' },
'404': { description: 'Skript neexistuje' },
},
},
put: {
tags: ['Skripty'],
summary: 'Ulozit kod skriptu',
description:
'Nejdriv se kod nacte a overi, az pak prepise soubor. Rozbita uprava ' +
'se neulozi a puvodni skript dal funguje.',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'idoklad.get-issued-invoice',
},
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['code'],
properties: {
code: {
type: 'string',
description: 'Cely obsah souboru vcetne exportu manifest a run.',
},
},
},
},
},
},
responses: {
'200': { description: 'Ulozeno, vraci se overeny manifest' },
'400': {
description: 'Kod nebo manifest neprosel, v `issues` je co opravit',
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
},
'403': { description: 'Jen spravce platformy' },
},
},
},
'/api/dashboard/scripts/{id}/test': {
post: {
tags: ['Skripty'],
summary: 'Zkusebni spusteni skriptu',
description:
'POZOR: vola opravdovou sluzbu. Vystavena faktura opravdu vznikne. ' +
'Chyba skriptu neni chyba API, vraci se 200 s popisem v `error`.',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'idoklad.get-issued-invoice',
},
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
inputs: {
type: 'object',
additionalProperties: true,
example: { invoiceId: 12345 },
},
},
},
},
},
},
responses: {
'200': {
description: 'Vysledek behu',
content: {
'application/json': { schema: { $ref: '#/components/schemas/ScriptRunResult' } },
},
},
'400': { description: 'Neplatne ID nebo vstupy' },
'403': { description: 'Jen spravce platformy' },
},
},
},
'/api/dashboard/automations': {
get: {
tags: ['Automatizace'],
summary: 'Seznam automatizaci',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Automatizace',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: { type: 'array', items: { $ref: '#/components/schemas/Automation' } },
},
},
},
},
},
},
},
post: {
tags: ['Automatizace'],
summary: 'Zalozit automatizaci',
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['name'],
properties: { name: { type: 'string', minLength: 3 } },
},
},
},
},
responses: {
'201': {
description: 'Vytvoreno',
content: {
'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } },
},
},
'400': { description: 'Neplatny nazev' },
},
},
},
'/api/dashboard/automations/{id}': {
parameters: [
{ name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'AUT-01' },
],
get: {
tags: ['Automatizace'],
summary: 'Detail vcetne stromu akci',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Detail',
content: {
'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } },
},
},
'404': { description: 'Neexistuje' },
},
},
put: {
tags: ['Automatizace'],
summary: 'Ulozit automatizaci',
description:
'Validuje strom proti katalogu konektoru. Nedokoncenou automatizaci server ' +
'nezapne ani pri enabled=true, duvody vraci v poli issues.',
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 3 },
enabled: { type: 'boolean' },
flow: { $ref: '#/components/schemas/AutomationFlow' },
},
},
},
},
},
responses: {
'200': {
description: 'Ulozeno',
content: {
'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } },
},
},
'400': { description: 'Neplatny strom' },
'404': { description: 'Neexistuje' },
},
},
delete: {
tags: ['Automatizace'],
summary: 'Smazat automatizaci',
security: [{ bearerAuth: [] }],
responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' } },
},
},
'/api/dashboard/automations/{id}/webhook/regenerate': {
post: {
tags: ['Automatizace'],
summary: 'Nova adresa webhooku',
description: 'Stara adresa okamzite prestane fungovat.',
security: [{ bearerAuth: [] }],
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
responses: {
'200': {
description: 'Novy token',
content: {
'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } },
},
},
'404': { description: 'Neexistuje nebo spoustecem neni webhook' },
},
},
},
'/api/simulate': {
post: {
tags: ['Simulace'],
summary: 'Vyvolat provozni udalost',
description:
'Meni skutecna data, aby bylo videt, jak dashboard reaguje zive. ' +
'Nevyplnena pole server doplni ukazkovou hodnotou.',
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['action'],
properties: {
action: {
type: 'string',
enum: [
'ticket.created',
'ticket.resolved',
'incident.started',
'incident.resolved',
'automation.run',
],
},
subject: { type: 'string' },
body: { type: 'string', description: 'Cely text pozadavku.' },
channel: {
type: 'string',
enum: ['whatsapp', 'facebook', 'instagram', 'email', 'voice', 'form', 'portal'],
description: 'Odkud pozadavek prisel. Podle toho se poskladá i log ticketu.',
},
contact: { type: 'string' },
knownCustomer: {
type: 'boolean',
description:
'false = CRM firmu nedohleda, ticket zustane bez zakaznika i bez resitele.',
},
priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] },
ticketId: { type: 'string' },
title: { type: 'string' },
service: { type: 'string' },
severity: { type: 'string', enum: ['sev1', 'sev2', 'sev3'] },
incidentId: { type: 'string' },
automationId: { type: 'string' },
ok: { type: 'boolean' },
},
},
},
},
},
responses: {
'200': { description: 'Udalost provedena' },
'201': { description: 'Zaznam vytvoren' },
'409': { description: 'Neni co provest' },
},
},
},
'/webhook/{token}': {
post: {
tags: ['Webhook'],
summary: 'Prijem dat do automatizace',
description:
'Verejny endpoint bez prihlaseni. Autorizuje neuhodnutelny token v adrese. ' +
'Telo se overuje proti parametrum deklarovanym u spoustece.',
parameters: [
{
name: 'token',
in: 'path',
required: true,
schema: { type: 'string' },
description: '32znakovy token vygenerovany serverem.',
},
],
requestBody: {
required: true,
content: {
'application/json': {
schema: { type: 'object', additionalProperties: true },
example: { customer: 'Nordis', score: 18, comment: 'vse ok' },
},
},
},
responses: {
'202': { description: 'Prijato' },
'400': { description: 'Chybi parametr nebo nesedi typ' },
'404': { description: 'Neznamy token' },
'409': { description: 'Automatizace je pozastavena' },
},
},
},
'/api/contact': {
post: {
tags: ['Kontakt'],
summary: 'Odeslat poptavku z webu',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'email', 'topic', 'message'],
properties: {
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' },
company: { type: 'string' },
phone: { type: 'string' },
topic: {
type: 'string',
enum: [
'automatizace',
'voicebot',
'integrace',
'dashboard',
'podpora',
'jine',
],
},
message: { type: 'string', minLength: 10 },
},
},
},
},
},
responses: {
'202': { description: 'Prijato' },
'400': { description: 'Neplatny vstup' },
},
},
},
},
};
}