# 04 - API Interaktivni dokumentace je na `/apps//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 `: | 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/incidents/:id` | | PATCH | `/api/dashboard/incidents/:id/status` | | 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/`, protoze ji dela jedna fabrika (`src/routes/crud.ts`): `tenants`, `users`, `roles`, `groups`, `ticket-types`, `actions`, `widgets`, `features`. `people` ma stejne cesty a stejne pravo (`people.manage`), ale vlastni handlery v `settings.ts`: zaznam, ktery se meni, je ucet bez firmy a odpoved je pohled za jednu firmu, coz fabrika neumi. Popis je nize v sekci Lide. ## Format chyb Jednotny pro cele API: ```json { "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, : 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](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. ```bash curl -X POST https://services.csbot.cz/apps//webhook/ \ -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](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](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. `assigneeId` je ID uctu; kdo ve firme ticketu neni clenem, 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 ve firme nema clenstvi (neni resitel), dostane 400. ## Lide (resitele) Resitel je clenstvi uctu ve firme, ne vlastni zaznam; ID resitele je ID uctu. Duvody v [06-tickety.md](06-tickety.md). `Person` je pohled: `id` a `name`, `email`, `enabled` z uctu, `tenantId`, `role` (popisek), `capacity`, `externalIds` a `roleIds` z clenstvi. Tentyz clovek ve dvou firmach prijde dvakrat se stejnym `id`. `GET /api/dashboard/people` vraci cleny zvolene firmy s povolenym uctem a jejich skupiny (`items`, `groups`, `meId`). Sprava je na `/api/dashboard/settings/people` pod pravem `people.manage`: | Volani | Telo | Co se stane | | ----------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET people` | | vcetne vypnutych uctu | | `POST people` | `{ name, email, password?, roleIds?, role?, capacity?, externalIds?, enabled? }` | zalozi ucet (heslo nahodne, kdyz chybi; role `role_agent`, kdyz chybi) s clenstvim ve firme, nebo prida clenstvi uctu, ktery s tim e-mailem uz je | | `PATCH people/:id` | tataz pole, vsechna nepovinna | `name`, `email` (unikatni) a `enabled` meni ucet, ostatni clenstvi v teto firme | | `DELETE people/:id` | | odebere jen clenstvi; ucet bez clenstvi, ktery neni spravce platformy, se vypne | Spravce firmy na spravce platformy nesaha, stejne jako u `/users`. Udalosti jsou `person.created`, `person.updated`, `person.deleted` s payloadem `{ id, person }`, u smazani `{ id }`. `enabled` je za clenstvi: vypne cloveka jen v teto firme, ucet a ostatni clenstvi zustavaji. `/users` (spravce platformy) bere u clenstvi vedle `roleIds` i `seesAllTenant`, `role`, `capacity` a `externalIds` a pri uprave je zachova, takze zmena role v Nastaveni nesmaze kapacitu nastavenou v Lidech. ## 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. Pozvanka nese jen `email` a `roleIds`; stary priznak `asPerson` se prijme a ignoruje, protoze resitelem je kazdy clen firmy. ## Akce na ticketu Popis modelu je v [09-navrh-rozsireni.md](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](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 (`role_admin`, popisek clenstvi z funkci v rejstriku, kapacita 8), vraci firmu a seznam uctu. Ucet je zaroven resitel | 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](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: ```json { "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](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 | | lide (`/settings/people`) | `people.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](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](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](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.