Poptavka z webu je ticket, prilohy, udaje provozovatele, kolacovy graf

Provozovatel portalu: firma s priznakem portalOperator (jen jedna, zapnuti
odebere ostatnim) a novymi poli contactEmail, contactPhone, website vedle
ico, dic, adresy a pravni formy. Verejny GET /api/public/brand vraci jeji
udaje a web je bere pres useBrand() na kontaktu, v paticce, O nas,
prihlaseni i v titulku; brand.ts je jen zaloha.

Poptavka z webu zaklada u provozovatele ticket kanalu form: predmet
"Poptavka: tema", telo JSON s poli formulare, tag Poptavka plus tema,
zakaznik z formulare, poznamka v logu. Bez provozovatele se jen zaloguje.

Prilohy ticketu: formular az 3 soubory po 5 MB, ticket az 10; nahrani,
seznam, stazeni a smazani (pravo ticket.comment, strop viditelnosti,
poznamky v logu, audit). Soubor jde v JSON jako Base64 a lezi v beznem
ulozisti, bez nove zavislosti; strop tela jen na techto cestach.

Vlastni widget s kreslenim Graf umi i pocet ticketu se seskupenim jako
kolac (PieChart.tsx, ciste SVG, osm barev z tokenu, zbytek jako ostatni).

OpenAPI rozdelene na mensi soubory (102 cest, 28 schemat overeno shodnych),
28 novych testu (135 celkem), dokumentace aktualizovana.
This commit is contained in:
JiriUhlir
2026-09-09 19:35:00 +02:00
parent 22dda2d139
commit c25e826766
72 changed files with 3978 additions and 1118 deletions
+76 -1
View File
@@ -14,7 +14,8 @@ Verejne:
| GET | `/docs` | Swagger UI |
| GET | `/openapi.json` | OpenAPI definice |
| POST | `/api/auth/login` | prihlaseni, vraci JWT |
| POST | `/api/contact` | poptavka z webu |
| POST | `/api/contact` | poptavka z webu, vznikne ticket provozovatele |
| GET | `/api/public/brand` | udaje provozovatele portalu pro web |
| POST | `/webhook/:token` | prijem dat do automatizace |
| POST | `/webhook/ticket/:token` | prijem udalosti do ticketu |
| GET | `/webhook/ticket/:token` | napoveda k prijmu |
@@ -51,6 +52,10 @@ Vyzaduji `Authorization: Bearer <token>`:
| POST | `/api/dashboard/tickets/:id/claim` |
| GET | `/api/dashboard/tickets/:id/actions` |
| POST | `/api/dashboard/tickets/:id/actions/:actionId` |
| GET | `/api/dashboard/tickets/:id/attachments` |
| POST | `/api/dashboard/tickets/:id/attachments` |
| GET | `/api/dashboard/tickets/:id/attachments/:attachmentId/content` |
| DELETE | `/api/dashboard/tickets/:id/attachments/:attachmentId` |
| GET | `/api/dashboard/invites` |
| POST | `/api/dashboard/invites` |
| DELETE | `/api/dashboard/invites/:id` |
@@ -142,6 +147,75 @@ pozvanky 5 za 15 minut. Pocita se podle adresy klienta, proto ma Express
Kazdy asynchronni handler je obaleny (`safeRouter` v `src/middleware/asyncHandler.ts`).
Odmitnuta promise je 500 s logem, ne pad procesu.
## Strop tela requestu
Globalni `express.json` v `src/app.ts` ma 256 kB. To staci na formulare
a stromy automatizaci, ne na soubory. Dve cesty prijimaji soubory v base64
a maji **vlastni** `express.json` s vetsim stropem; globalni parser je
preskakuje (`hasOwnBodyLimit` v `src/routes/bodyLimit.ts`), jinak by telo
odmitl driv, nez se k nemu router dostane.
| Cesta | Strop |
| ---------------------------------------- | ------------------------------------------- |
| `POST /api/contact` | `jsonLimitFor(3, 5 MB)`, tj. 3 soubory |
| `POST /api/dashboard/tickets/:id/attachments` | `jsonLimitFor(10, 5 MB)`, tj. 10 souboru |
`jsonLimitFor(count, maxBytes)` pocita `count * maxBytes * 4/3` (base64) plus
64 kB rezervy na zbytek JSONu. Vypocet je na jednom miste, aby formular
a prilohy pocitaly stejne. U kontaktu bezi limit pokusu **pred** parserem
tela: kdo uz pokusy vycerpal, nema server nutit cist megabajty.
## Poptavka z webu
`POST /api/contact` je verejny, 5 poptavek za hodinu z jedne adresy. Telo:
`name`, `email`, `topic` (`automatizace`, `voicebot`, `integrace`,
`dashboard`, `podpora`, `jine`), `message`, nepovinne `company`, `phone`
a `attachments` (nejvys 3, kazda `{ name, mime?, content }` s obsahem
v base64).
Poptavka vznikne jako **ticket firmy, ktera je provozovatelem portalu**
(`Tenant.portalOperator`, viz [07-firmy-a-prava.md](07-firmy-a-prava.md)):
kanal `form`, predmet `Poptávka: <tema>`, telo je JSON formulare, tagy
`Poptávka` a tema, `externalSource` `web-form`. Kdyz zadna firma
provozovatelem neni, poptavka se jen zaloguje (warn). Odpoved je v obou
pripadech 202 - zvenku nema byt poznat, jak je portal nastaveny. Podrobnosti
v [06-tickety.md](06-tickety.md).
## Udaje provozovatele
`GET /api/public/brand` je verejny, bez limitu pokusu (cte z kopie firem
v pameti, je levnejsi nez health) a s `Cache-Control: public, max-age=60`.
Vraci `name`, `legalName`, `ico`, `dic`, `address`, `legalForm`, `email`,
`phone`, `website` provozovatele portalu; bez provozovatele same `null`
a porad 200, aby web umel rict "neni nastaveno" misto padu. Web ho cte
pres `useBrand`, viz [22-znacka-a-design.md](22-znacka-a-design.md).
## Prilohy ticketu
Model je v [06-tickety.md](06-tickety.md). Prilohy nejsou pole ticketu, maji
vlastni cesty pod `/api/dashboard/tickets/:id/attachments`:
| Volani | Co se stane |
| ---------------------------------------- | ---------------------------------------------------------------------------- |
| `GET attachments` | seznam bez obsahu (`id`, `name`, `mime`, `size`, `uploadedBy`, `createdAt`) |
| `POST attachments` | telo `{ files: [{ name, mime?, content }] }`, obsah base64; vraci 201 a `items` |
| `GET attachments/:attachmentId/content` | binarni obsah s `Content-Type` a `Content-Disposition` (nazev v RFC 5987) |
| `DELETE attachments/:attachmentId` | 204 |
Limity: 5 MB na soubor po dekodovani, 10 priloh na ticket, nazev bez cesty
a ridicich znaku, nejvys 200 znaku. Kontrola bezi nad **celou davkou** pred
prvnim zapisem: kdyz neprojde treti soubor, neulozi se ani prvni dva.
Ticket se hleda stejne jako u detailu (`visibleTicketOrDeny`): cizi nebo nad
strop viditelnosti je 404. Zapis a mazani chce `ticket.comment` za firmu
ticketu - priloha je jen dalsi zprava k ticketu. Kazda zmena zapise radek do
logu ticketu, posle `ticket.updated` a jde do auditu jako
`ticket.attachment.add` / `ticket.attachment.remove`.
Stazeni chce hlavicku `Authorization`, obycejny odkaz `<a href>` ji neposle.
Portal proto stahuje pres fetch (`apiBlob` v `web/src/lib/api.ts`) a docasny
odkaz na blob.
## Autentizace
Hesla se hashuji bcryptem, plaintext se nikde neuklada. Login vraci JWT
@@ -401,6 +475,7 @@ Pravo se vzdy pta **za firmu zaznamu**, ne za prepnutou firmu. Cizi firma je
| 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 |
| prilohy ticketu (POST, DELETE) | `ticket.comment` 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 |