Files
csbot-prototype/documentation/04-api.md
T
JiriUhlirandClaude Fable 5.1 104ae36783 Revize projektu: prava, vykon, runtime, portal a ARES
Prava a bezpecnost: spravce firmy uz nemuze nastavit priznak spravce
platformy ani clenstvi v cizi firme; pozvanky, konektory a automatizace
kontroluji sve pravo; cizi firma v query je 404; zivy stream posila
udalosti jen firmam, kterych se tykaji; akce nad ticketem maji kontrolu
prava za firmu ticketu a strop viditelnosti; tokeny se nelogujou; limit
pokusu na prihlaseni, kontakt a pozvanky; bezpecnostni hlavicky;
zachyceni chyb v async handlerech; timing-safe porovnani tokenu.

Vykon: audit neskenuje celou kolekci pri kazdem zapisu a konecne maze
firemni zaznamy; ticket se uklada jednou misto trikrat; zapisy do
Postgresu jsou serializovane podle ID; prava se pocitaji jednou na
request; widgety nacitaji tickety jednou; strankovani seznamu; worker
je pool misto kol; na webu udalost ze streamu neodmontuje stranku,
dotazy maji spolecny debounce a cache, ciselniky drzi typovany sklad.

Runtime: opakuji se jen chyby oznacene retryable; smycka nenarazi na
strop 50 kroku (novy strop 1000 akci); podminka nad datem funguje;
vystup MCP nastroje neprepisuje spoustec; sandbox skriptu firmy nejde
opustit; MCP session id se drzi mezi volanimi; incident z kroku patri
firme; jedno rozhodnuti o rezimu uloziste; snapshot neprepise soubor
po chybe cteni.

Refaktory: sdilene typy API v src/shared (web nic nekopiruje, osm
rozjetych tvaru sjednoceno); spolecny modul net/guard pro volani ven;
formularova vrstva ui/form; rozdeleni Connectors a FlowCanvas; jeden
helper pro firmu z query, validaci a CRUD udalosti; pomucky ctx.util
pro skripty konektoru; i18n verejneho webu vcetne anglictiny.

Nova funkce: zalozeni firmy z registru ARES v Nastaveni (IC nebo nazev,
dotazeni IC, DIC, sidla a pravni formy, vyber soucasnych statutarnich
zastupcu a prokury, ucty spravce firmy s nahradnim e-mailem
IC-poradi@placeholder.cz).

Dokumentace: zaznam v 99-zmeny.md a aktualizace 15 dalsich dokumentu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 10:26:07 +02:00

21 KiB

04 - API

Interaktivni dokumentace je na /apps/<app-id>/docs. Tenhle soubor popisuje to, co ze Swaggeru neni videt.

Endpointy

Verejne:

Metoda Cesta Popis
GET /health liveness, nezavisi na databazi
GET /health/ready readiness, 503 pri nedostupne databazi
GET /docs Swagger UI
GET /openapi.json OpenAPI definice
POST /api/auth/login prihlaseni, vraci JWT
POST /api/contact poptavka z webu
POST /webhook/:token prijem dat do automatizace
POST /webhook/ticket/:token prijem udalosti do ticketu
GET /webhook/ticket/:token napoveda k prijmu

Vyzaduji Authorization: Bearer <token>:

Metoda Cesta
GET /api/auth/me
POST /api/auth/logout
GET /api/dashboard/access
GET /api/dashboard/widgets
GET /api/dashboard/layout
PUT /api/dashboard/layout
DELETE /api/dashboard/layout
GET /api/dashboard/summary
GET /api/dashboard/people
GET /api/dashboard/people/:id
GET /api/dashboard/intake
POST /api/dashboard/intake/regenerate
GET /api/dashboard/settings/actions/:id/scope
GET /api/dashboard/notifications
POST /api/dashboard/notifications/read
GET /api/dashboard/runs
GET /api/dashboard/tickets
GET /api/dashboard/tickets/workload
GET /api/dashboard/tickets/:id
POST /api/dashboard/tickets/:id/assign
POST /api/dashboard/tickets/:id/status
POST /api/dashboard/tickets/:id/comment
POST /api/dashboard/tickets/:id/type
POST /api/dashboard/tickets/:id/tags
POST /api/dashboard/tickets/:id/group
POST /api/dashboard/tickets/:id/claim
GET /api/dashboard/tickets/:id/actions
POST /api/dashboard/tickets/:id/actions/:actionId
GET /api/dashboard/invites
POST /api/dashboard/invites
DELETE /api/dashboard/invites/:id
GET /api/invites/:kod
POST /api/invites/:kod/accept
GET /api/dashboard/incidents
GET /api/dashboard/storage
GET /api/dashboard/services
GET /api/dashboard/connectors/services
GET /api/dashboard/connectors
POST /api/dashboard/connectors
GET /api/dashboard/connectors/:id
PATCH /api/dashboard/connectors/:id
DELETE /api/dashboard/connectors/:id
POST /api/dashboard/connectors/:id/test
GET /api/dashboard/scripts
GET /api/dashboard/scripts/:id
PUT /api/dashboard/scripts/:id
POST /api/dashboard/scripts/:id/test
POST /api/dashboard/scripts/reload
GET /api/dashboard/stream
GET /api/dashboard/automations
POST /api/dashboard/automations
GET /api/dashboard/automations/:id
PUT /api/dashboard/automations/:id
DELETE /api/dashboard/automations/:id
POST /api/dashboard/automations/:id/webhook/regenerate
POST /api/dashboard/widget-data
GET /api/dashboard/widget-data/options
GET /api/dashboard/settings/catalog
GET /api/dashboard/settings/features
GET /api/dashboard/settings/people-overview
GET /api/dashboard/settings/users-overview
PATCH /api/dashboard/settings/users/:id/password
GET /api/dashboard/settings/ares/companies
GET /api/dashboard/settings/ares/companies/:ico/persons
POST /api/dashboard/settings/ares/tenants
POST /api/admin/impersonate
POST /api/admin/impersonate/stop
GET /api/admin/impersonate/candidates
GET /api/admin/audit

Sprava zaznamu ma u kazde entity stejnou petici (seznam, detail, vytvoreni, uprava, mazani) na /api/dashboard/settings/<entita>, protoze ji dela jedna fabrika (src/routes/crud.ts):

tenants, users, roles, people, groups, ticket-types, actions, widgets, features.

Format chyb

Jednotny pro cele API:

{
  "error": "validation_error",
  "message": "Zadejte platny e-mail.",
  "issues": [{ "field": "email", "message": "Zadejte platny e-mail." }]
}
HTTP error Kdy
400 validation_error vstup neprosel schematem, issues po polich
401 unauthorized chybi nebo neplatny token
401 invalid_credentials spatny e-mail nebo heslo
403 forbidden nedostatecne pravo v dane firme
404 not_found zaznam nebo endpoint neexistuje, nebo je cizi firmy
409 ruzne operace nedava v danem stavu smysl
429 too_many_requests prekrocen limit requestu, hlavicka Retry-After
500 internal_error neodchycena chyba, detail jen mimo produkci

message je vzdy cesky a je urcena k zobrazeni uzivateli. Chybu validace sklada validationError v src/middleware/validation.ts, aby issues mely vsude stejny tvar a formular umel chybu ukazat u pole.

Limity (src/middleware/rateLimit.ts) jsou jen na verejnych endpointech, kde se da hadat: prihlaseni 20 pokusu za 15 minut, kontakt 5 za hodinu, prijeti pozvanky 5 za 15 minut. Pocita se podle adresy klienta, proto ma Express trust proxy = 1 - bez toho by vsichni za Caddy sdileli jeden limit.

Kazdy asynchronni handler je obaleny (safeRouter v src/middleware/asyncHandler.ts). Odmitnuta promise je 500 s logem, ne pad procesu.

Autentizace

Hesla se hashuji bcryptem, plaintext se nikde neuklada. Login vraci JWT podepsany JWT_SECRET s platnosti JWT_EXPIRES_IN.

Spatne heslo i neexistujici e-mail vraci stejnou odpoved, aby se neprozradilo, ktere ucty existuji. Pokus se loguje bez hesla.

Token si drzi klient v localStorage. Pro produkci je cilovy stav httpOnly cookie se Secure a SameSite plus CSRF token.

Zivy stream

GET /api/dashboard/stream je Server-Sent Events. Po pripojeni posle potvrzeni a poslednich par udalosti, pak uz jen nove. Kazdych 25 sekund jde komentarovy radek, aby spojeni neuspalo proxy.

Typy udalosti: ticket.created, ticket.updated, ticket.assigned, ticket.resolved, incident.started, incident.updated, incident.resolved, automation.created, automation.updated, automation.deleted, automation.run, webhook.received.

K tomu udalosti entit tenant, user, role, person, group, ticketType, action, widget, connector, feature s priponou .created, .updated, .deleted. Payload je { id, <druh>: zaznam }, u smazani jen { id }. Publikuje je crudRouter (volba event), routy konektoru a PUT features. Klient z nich opravuje sklad ciselniku bez dotazu.

Kazda udalost nese tenantId (null = cela platforma). Stream posila jen udalosti firem, do kterych uzivatel patri, a to i v historii po pripojeni. Udalost s payload.userId jde jen tomu cloveku. Spravce platformy vidi vse. Driv videl kazdy prihlaseny udalosti vsech firem - nazev ticketu cizi firmy v bubline je unik dat, i kdyz se na ticket nedostane.

ticket.updated, ticket.assigned a ticket.resolved nesou v payload.ticket cely ticket, aby klient opravil seznam na miste.

Klient se pripojuje pres fetch s hlavickou Authorization, ne pres EventSource. Duvod je v 03-architektura-a-mapa-kodu.md.

Webhook

Verejny endpoint bez prihlaseni. Autorizuje neuhodnutelny token v adrese, 32 znaku z randomBytes(24) v base64url.

Token generuje vyhradne server. Hodnota webhookToken poslana klientem se ignoruje, jinak by si sel nastavit predvidatelnou adresu.

Situace Odpoved
vse v poradku 202
neznamy token 404
automatizace je pozastavena 409
chybi povinny parametr, spatny typ 400

Parametry navic se neodmitaji, jen loguji. Odesilatele bezne posilaji i vlastni data a odmitat je by rozbijelo integrace.

curl -X POST https://services.csbot.cz/apps/<app-id>/webhook/<token> \
  -H "Content-Type: application/json" \
  -d '{"customer":"Nordis","score":18}'

Token se porovnava v konstantnim case (timingSafeEqualString v src/lib/secure.ts), stejne jako token prijmu a kod pozvanky. V logu requestu je z tokenu videt jen prvnich sest znaku.

Firmy a pohledy

Prava popisuje 07-firmy-a-prava.md, tady jen API.

Endpointy dashboardu berou scope (all, tenant, mine) a tenantId. GET /api/dashboard/access rekne, co uzivatel smi, aby to klient nedovozoval.

Pozadavek na pohled nebo firmu bez opravneni vraci 403 nebo 404, nikdy tise zuzeny vysledek. Uzivatel nesmi koukat na cizi cisla v domneni, ze jsou spravna.

Tickety

Popis modelu je v 06-tickety.md, tady jen to, co se tyka API.

GET /api/dashboard/tickets bere navic filtry assignee, status, channel. U assignee je zvlastni hodnota unassigned pro frontu bez resitele. U pohledu mine se assignee ignoruje, pohled je silnejsi. Neznama hodnota filtru se zaloguje a ignoruje - je lepsi ukazat vic ticketu nez prazdny seznam bez vysvetleni.

Odpoved nese vedle items jeste meId. Klient podle nej pozna, ktere tickety jsou jeho, a jestli ma vubec smysl nabizet filtr "moje".

Strankovani. /tickets a /runs berou limit a offset, limit nejvys 500. Celkovy pocet je v hlavicce X-Total-Count, u ticketu i v tele jako total. Bez limit se vraci vse jako driv (u behu poslednich 50), aby se nerozbily stavajici odkazy. Klient cte hlavicku pres apiFetchWithMeta.

Filtrovani dela server, ne klient. Seznam a prehled vytizeni tak nikdy neukazuji jina cisla. Vyhledavaci pole v portalu je jina vec - to jen dohledava v uz nactenem seznamu.

GET /api/dashboard/tickets/:id vraci navic trace, tedy log prubehu vcetne toho, co ktera volana sluzba vratila.

POST /api/dashboard/tickets/:id/assign s telem {"assigneeId": null} vrati ticket do fronty. Neznamy resitel vraci 404, ne tiche odpojeni.

POST /api/dashboard/tickets/:id/claim je prevzeti prace, ne prehozeni: volajici si bere ticket sam a telo je prazdne. Smi to u ticketu bez resitele a u ticketu ve skupine, ve ktere je. Kdyz uz ticket nekdo resi, vraci 409, resp. 403 u cizi skupiny - vzit nekomu rozdelanou praci je jine rozhodnuti a chce to pravo ticket.assign.others. Kdo neni vedeny jako resitel, dostane 400.

Pozvanky do firmy

/api/invites/:kod je verejne, protoze kdo prijde za odkazem, jeste ucet mit nemusi. Autorizuje kod v adrese, proto je nahodny a dlouhy - stejne jako u webhooku. Odpoved je zamerne uzka: nazev firmy, jestli pozvanka plati, a kdyz uz adresu zname, tak ji, at ji clovek nemusi psat. Nic o tom, kdo ve firme je.

POST /api/invites/:kod/accept s telem {"name", "email", "password"}:

Situace Co se stane
ucet neexistuje zalozi se a pripoji k firme
ucet existuje, heslo sedi jen se pripoji k firme
ucet existuje, heslo nesedi 401
pozvanka je na jinou adresu 403
pozvanka uz byla pouzita nebo vyprsela 409 s konkretnim duvodem

Overeni hesla u existujiciho uctu neni formalita: bez nej by kdokoliv s odkazem pripojil cizi adresu ke sve firme a videl by jeji data.

Sprava pozvanek (/api/dashboard/invites) chce pravo user.manage. Seznam vraci u kazde pozvanky celou adresu vcetne prefixu proxy, aby slo rovnou kopirovat - relativni cesta se do zpravy vlepit neda.

Akce na ticketu

Popis modelu je v 09-navrh-rozsireni.md.

GET /api/dashboard/tickets/:id/actions vraci jen akce, ktere v teto situaci opravdu jdou spustit: sedi typ nebo tag, projdou podminky a volajici na ne ma pravo. Klient si nefiltruje nic - jinak by se to pocitalo na dvou mistech a jednou se to rozejde.

POST /api/dashboard/tickets/:id/actions/:actionId vraci 200 i kdyz akce selhala. Selhani akce neni chyba API. V odpovedi je ok, summary, detail s celym chybovym hlasenim a durationMs. Cely prubeh se zapise do logu ticketu.

Vestavene akce (type, tags, group, assign, status, comment) jsou zvlast: meni ticket sam, ne cizi sluzbu, a kazda ma vlastni pravo. Vsechny vcetne claim jdou pres builtinAction v src/routes/ticketActions.ts, kde se pravo pta za firmu ticketu a ticket se nejdriv najde pres strop viditelnosti (visibleTicketOrDeny). Driv mely assign, status a comment vlastni handlery a kazdy se ptal jinak.

Firma z registru ARES

Jen spravce platformy (/api/dashboard/settings/ares). Popis rozhodnuti je v 07-firmy-a-prava.md.

Endpoint Co vraci
GET ares/companies?query= same cislice (1 az 8) hledaji IC presne, jinak nazev, nejvys 10. U firmy, ktera uz v portalu je, existingTenantId
GET ares/companies/{ico}/persons soucasni statutari a prokura z verejneho rejstriku, u kazdeho navrzeny e-mail IC-poradi@placeholder.cz
POST ares/tenants zalozi firmu a ucty vybranych osob, vraci firmu a seznam uctu

Chyba registru je ares_error s kodem podle toho, co ARES vratil - neni to chyba naseho API a nema se opakovat automaticky. Adresa registru je ARES_BASE_URL.

Prijem udalosti do ticketu

Popis modelu je v 18-ticketovaci-system.md.

POST /webhook/ticket/:token je verejny, autorizuje token firmy v adrese. Vraci 201 kdyz ticket vznikl, 200 kdyz se udalost navesila na existujici:

{ "ok": true, "created": false, "ticketId": "TK-4822", "externalId": "3", "eventId": "tev_2" }

externalId je unikatni v ramci firmy. Token urcuje firmu, takze dve firmy mohou obe poslat objednavku cislo 3 a nedojde ke smichani. Cislo i retezec jsou tentyz klic.

Neznamy typeId se zahodi a zaloguje, ticket vznikne bez typu. Odmitnout celou udalost kvuli jednomu poli by znamenalo ztratu dat.

Fronta behu

Popis je v 20-fronta-a-runtime.md.

POST /webhook/:token vraci 202, ne 200: data jsme prevzali a strom se vykona na pozadi. Vysledek se hleda v GET /api/dashboard/runs nebo v logu ticketu. Cekat na cizi sluzbu v requestu nejde - za jeji rychlost nerucime a odesilateli by vyprsel timeout.

GET /webhook/:token vraci kontrakt: co se v tele ceka, na jakych cestach a ukazku. Bez toho by musel ten, kdo webhook zapojuje, hadat.

GET /api/dashboard/runs ma u kazdeho behu cele chybove hlaseni, pocet pokusu a kdy se to zkusi znovu.

Prava a navigace

GET /api/dashboard/access vraci permissions (efektivni prava po slouceni roli), nav (zalozky, ktere ma volajici videt), platformAdmin a roleNames (nazvy roli v prepnute firme, pro popisek u uctu). Klient podle toho kresli, ale nic si nedovozuje - kdo co smi, rozhoduje server u kazdeho requestu znovu.

Kdo co smi, po routach

Pravo se vzdy pta za firmu zaznamu, ne za prepnutou firmu. Cizi firma je 404, chybejici pravo 403.

Co Pravo
firmy CRUD, ARES spravce platformy
uzivatele CRUD spravce platformy, nebo user.manage jen v ramci sve firmy
pozvanky user.manage, role jen z te firmy
konektory create, update, delete, test connector.manage
automatizace create, update, delete, regenerate automation.edit
/services, /connectors/services clenstvi ve firme
assign, status, comment, claim na ticketu prava vestavene akce za firmu ticketu plus strop viditelnosti
/api/admin/impersonate* impersonate
/api/admin/audit audit.view
/storage, /scripts s cestami na serveru cesty jen spravci platformy, ostatni dostanou odpoved bez nich

Spravce firmy s user.manage nenastavi platformAdmin, neprida clenstvi v jine firme, nesahne na spravce platformy a nesmaze cloveka, ktery je i v jine firme - ten ucet neni jen jeho. Podrobnosti v 07-firmy-a-prava.md.

GET /api/dashboard/settings/catalog vraci katalog prav a modulu, aby formular role nemel seznam prav napsany v kodu klienta.

Prepnuti na jiny ucet

POST /api/admin/impersonate vraci novy token s narokem act (kdo se za koho vydava) a writes. Bez writes middleware odmitne cokoliv jineho nez GET s 403. Kazde prepnuti i ukonceni je v auditu vcetne toho, kdo to byl doopravdy.

Svuj puvodni token si klient odklada do sessionStorage, server o nem nic nevi.

Sluzby a konektory

Popis modelu je v 12-sluzby-a-konektory.md, tady jen API.

Sluzba je to, co umime. Konektor je napojeni jedne firmy vcetne jejich pristupovych udaju.

Hodnoty pristupovych udaju se nikdy nevraci, jen filled a missing. V PATCH staci poslat jen to, co se meni: prazdny retezec hodnotu smaze, chybejici klic ji nechava.

Sluzba, kterou uzivatel nevidi, se nevraci vubec, ne se stavem 403.

POST /connectors/:id/test vraci 200 i pri neuspechu. checked rika, co se vlastne overilo - u sluzby bez verifyPath jen dostupnost, ne udaje.

Skripty konektoru

Popis modelu je v 11-skripty-konektoru.md, tady jen API.

Cteni smi kazdy prihlaseny, protoze builder potrebuje vedet, co skript umi. Uprava, zkusebni spusteni a vynucene nacteni smi jen spravce platformy - uprava skriptu meni chovani vseho, co ho pouziva.

GET /api/dashboard/scripts vraci vedle manifestu i problems s rozbitymi skripty a connections se stavem napojeni. Hodnoty pristupovych udaju se nevraci nikdy, jen jmena chybejicich environment variables.

PUT /api/dashboard/scripts/:id kod nejdriv nacte a overi a az pak prepise soubor. Rozbita uprava vraci 400 s issues a puvodni skript dal funguje.

POST /api/dashboard/scripts/:id/test vola opravdovou sluzbu. Chyba skriptu neni chyba API, vraci se 200 a popis v error vcetne toho, jestli ma smysl zkusit to znovu.

Pri pridani endpointu

Soucasne aktualizovat src/openapi.ts a tenhle soubor. Swagger musi odpovidat skutecnemu chovani aplikace, jinak je horsi nez zadny.