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>
This commit is contained in:
JiriUhlir
2026-09-09 10:26:07 +02:00
co-authored by Claude Fable 5.1
parent 0c405ea55a
commit 104ae36783
215 changed files with 13226 additions and 8320 deletions
+93 -11
View File
@@ -85,6 +85,9 @@ Vyzaduji `Authorization: Bearer <token>`:
| 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` |
@@ -102,20 +105,35 @@ fabrika (`src/routes/crud.ts`):
Jednotny pro cele API:
```json
{ "error": "validation_error", "message": "Zadejte platny e-mail." }
{
"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 |
| 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` | nedostatecna role |
| 404 | `not_found` | zaznam nebo endpoint neexistuje |
| 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.
`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
@@ -139,6 +157,21 @@ Typy udalosti: `ticket.created`, `ticket.updated`, `ticket.assigned`,
`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](03-architektura-a-mapa-kodu.md).
@@ -166,8 +199,9 @@ curl -X POST https://services.csbot.cz/apps/<app-id>/webhook/<token> \
-d '{"customer":"Nordis","score":18}'
```
Prototyp pozadavek prijme, zvaliduje a zapocita do metrik, ale strom akci
nevykona - runtime neexistuje.
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
@@ -192,6 +226,11 @@ 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.
@@ -246,7 +285,26 @@ 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.
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, 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
@@ -284,9 +342,33 @@ 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) a `platformAdmin`. Klient podle
toho kresli, ale **nic si nedovozuje** - kdo co smi, rozhoduje server u kazdeho
requestu znovu.
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](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.