Firmy a prava: tenance napric portalem

Portal nemel zadnou tenanci. Kterykoliv prihlaseny uzivatel videl vsechny
tickety vsech firem i cely seznam resitelu, requireRole se nikde nevolal.

Tenant je hranice viditelnosti, tenantId na ticketu, resiteli i automatizaci.
Uzivatel muze patrit do vic firem, v kazde s jinou roli. Pristup napric firmami
je zvlast jako platformAdmin.

Tri pohledy na tickety: all, tenant, mine. Admin mezi nimi prepina vcetne
vyberu firmy. O pravech rozhoduje jedine data/access.ts, klient si nic
nedovozuje a bere je z GET /api/dashboard/access.

Filtr na firmu je v ulozistich povinny argument, takze zapomenuty filtr
neznamena vse, ale nezkompiluje se. Cizi firma vraci 403 nebo 404, nikdy
tise zuzeny vysledek.

Prirazeni jen v ramci firmy. Prehazovat praci mezi lidmi smi jen admin,
agent si smi vzit ticket na sebe.

Zmena prihlasovani: ucet klient@firma.cz zanikl, demo ucty jsou nove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-03 12:59:30 +02:00
parent dd021b5f69
commit 2a3d75c85e
25 changed files with 997 additions and 163 deletions
+2
View File
@@ -28,6 +28,8 @@ React aplikaci ze slozky `dist/public`.
| Obsah ticketu a sablony | hotovo | `{{parametr}}` ze spoustece do poli akce |
| Vystupy kroku a predvalidace | hotovo | podminka se umi zeptat, co vratil predchozi krok |
| Kanaly WhatsApp, FB, Instagram | hotovo | vcetne vzorovych automatizaci na prijem |
| Firmy a prava | hotovo | tri pohledy, uzivatel muze byt ve vic firmach |
| Sprava clenstvi z portalu | chybi | memberships jdou zmenit jen v kodu |
| Bugs a wishes | chybi | vyvojarska agenda, samostatna evidence vedle ticketu |
| Beh automatizaci | chybi | ulozeny strom se nevykonava, neni runtime |
| Databaze | chybi | data jsou v pameti, restart je vrati na vychozi stav |
@@ -41,6 +41,8 @@ image jen `dist`, takze staci jedna slozka.
| `src/routes/contact.ts` | poptavkovy formular z webu |
| `src/data/ticketStore.ts` | tickety, jejich resitele, log prubehu, prehled vytizeni |
| `src/data/people.ts` | resitele ticketu - oddeleni od uzivatelu portalu |
| `src/data/tenants.ts` | firmy, ktere portal pouzivaji |
| `src/data/access.ts` | kdo co vidi - jedno misto pro cely portal |
| `src/data/incidentStore.ts` | incidenty vcetne zmen a udalosti |
| `src/data/automationStore.ts` | automatizace, strom akci, tokeny webhooku |
| `src/data/connectors.ts` | katalog konektoru, jejich spousteču a akci |
@@ -87,6 +89,14 @@ Kod zustava anglicky.
**Data v pameti.** Vedome zjednoduseni prototypu. Uloziste jsou oddelena od rout,
takze napojeni na databazi znamena prepsat soubory v `src/data/`, ne endpointy.
**Filtr na firmu je povinny argument.** `listTickets`, `listPeople`
i `listAutomations` vyzaduji `tenantIds`. Zapomenuty filtr tak neznamena "vse",
ale nezkompiluje se. Podrobnosti v [07-firmy-a-prava.md](07-firmy-a-prava.md).
**Prava se nikdy nedovozuji na klientovi.** Server vraci `GET /api/dashboard/access`
s tim, co uzivatel smi. Kdyby si to klient pocital sam, pocitalo by se to na dvou
mistech a jednou se to rozejde.
**Resitel neni uzivatel.** Uzivatel se prihlasuje do portalu, resitel ma u sebe
tickety. Technik muze mit tickety a ucet nikdy nemit. Spojka je e-mail,
podrobnosti v [06-tickety.md](06-tickety.md).
+15 -4
View File
@@ -22,6 +22,7 @@ Vyzaduji `Authorization: Bearer <token>`:
| ------ | --------------------------------------------------- |
| GET | `/api/auth/me` |
| POST | `/api/auth/logout` |
| GET | `/api/dashboard/access` |
| GET | `/api/dashboard/summary` |
| GET | `/api/dashboard/people` |
| GET | `/api/dashboard/tickets` |
@@ -113,14 +114,24 @@ curl -X POST https://services.csbot.cz/apps/<app-id>/webhook/<token> \
Prototyp pozadavek prijme, zvaliduje a zapocita do metrik, ale strom akci
nevykona - runtime neexistuje.
## 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 filtry v query: `assignee`, `status`, `channel`.
U `assignee` jsou dve zvlastni hodnoty: `me` znamena resitele odpovidajiciho
prihlasenemu uzivateli, `unassigned` frontu bez resitele. Neznama hodnota filtru
se zaloguje a ignoruje - je lepsi ukazat vic ticketu nez prazdny seznam
`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
+18
View File
@@ -96,6 +96,24 @@ autorita, kopie na klientovi existuje jen proto, aby UI nenabidlo nesmysl.
Dokud spoustec nema zadny parametr, nejde pridat podminka - nebylo by podle ceho
se rozhodovat. Dialog to vysvetli.
## Sirka karet pri zanoreni
Vetve ANO a NE jsou vedle sebe jen tehdy, kdyz je na to v dane karte misto.
Rozhoduje **sirka karty, ne sirka okna** - pouzivaji se container queries
(`@container` a `@2xl:grid-cols-2` v `FlowCanvas.tsx`).
Duvod: kazde zanoreni pulí dostupnou sirku. S beznym `lg:grid-cols-2` vypadal
strom na sirokem monitoru dobre v prvni urovni a ve treti uz mel karty siroke
par desitek pixelu, takze se popisy lamaly po jednom slove. Container query se
od urcite hloubky sama prepne na vetve pod sebou.
Ze stejneho duvodu se v uzke karte skryva popis akce (`hidden @xs:block`),
zmensuje ikona a ovladaci tlacitka se skladaji do sloupce. Nazev kroku
a nastaveni poli zustavaji vzdy videt - to je to podstatne.
**Pri uprave stromu nepouzivat `sm:` / `lg:` na veci uvnitr karet.** Reaguji
na okno a v zanoreni lzou.
## Co je v kterem kroku videt
Krok vidi parametry spoustece **plus vystupy vsech kroku pred nim**. Akce muze
+124
View File
@@ -0,0 +1,124 @@
# 07 - Firmy a prava
## Dve ruzne "firmy"
Snadno se pletou, proto hned na zacatku:
- **Tenant** je firma, ktera portal pouziva. Ma v nem svuj tym, svoje tickety
a svoje automatizace. Je to hranice viditelnosti.
- **`ticket.customer`** je zakaznik toho tenanta, tedy kdo pozadavek poslal.
S pravy nema nic spolecneho.
Tenanti jsou v `src/data/tenants.ts`.
## Uzivatel muze byt ve vic firmach
Proto `memberships`, ne jedno `tenantId`. Typicky externista, ktery dela
servicedesk dvema klientum a v kazdem ma jinou roli.
```ts
interface User {
id: string;
email: string;
name: string;
platformAdmin: boolean; // vidi napric vsemi firmami
memberships: Membership[]; // { tenantId, role }
}
type TenantRole = 'admin' | 'agent';
```
**Role je vzdy az uvnitr firmy.** Pristup napric firmami je zvlast jako
`platformAdmin` - to je nase pravo, ne zakaznicke. Kdyby byla role jen jedna
globalni, nesla by tahle situace vubec zapsat.
## Tri pohledy na tickety
| Pohled | Co ukazuje | Kdo smi |
| -------- | ------------------------- | ------------------------------ |
| `all` | napric vsemi firmami | jen `platformAdmin` |
| `tenant` | cela jedna firma | kdokoliv, kdo do ni patri |
| `mine` | jen tickety prihlaseneho | kdo ma navazaneho resitele |
Posilaji se jako query: `?scope=tenant&tenantId=tnt_automia`.
U `tenant` a `mine` je potreba vedet **kterou** firmu, protoze uzivatel jich
muze mit vic. Bez `tenantId` se pouzije prvni.
## Server je autorita
Vsechno rozhoduje `src/data/access.ts`, jedno misto pro cely portal. Kdyby se to
rozlezlo po routach, driv nebo pozdeji vznikne endpoint, ktery filtr zapomene.
`GET /api/dashboard/access` vraci, co uzivatel smi:
```json
{
"scopes": ["all", "tenant", "mine"],
"tenants": [{ "id": "tnt_automia", "name": "Automia" }],
"defaultTenantId": "tnt_automia",
"canAssignOthers": true,
"personId": "ppl_uhlir"
}
```
Klient podle toho kresli prepinac. **Nesmi si to dovozovat sam** - jinak by se
prava pocitala na dvou mistech a jednou se rozejdou.
## Nikdy tise nezuzujeme
Pozadavek na pohled nebo firmu, na kterou uzivatel nema pravo, vraci **chybu**,
ne potichu zuzeny vysledek:
| Situace | Odpoved |
| -------------------------------- | ------- |
| pohled bez opravneni | 403 |
| firma, do ktere nepatri | 404 |
| ucet bez firmy | 403 |
| prirazeni ostatnim bez prava | 403 |
Duvod: kdyby se pozadavek na cizi firmu jen prepnul na vlastni, uzivatel by
koukal na cizi cisla v domneni, ze jsou spravna. To je horsi nez chyba.
## Filtr v ulozisti je povinny
`listTickets`, `listPeople` a `listAutomations` maji `tenantIds` jako **povinny**
argument, ne volitelny. Zapomenuty filtr tak neznamena "vse", ale nezkompiluje se.
Zapis (`assignTicket`, `updateTicketStatus`, `addComment`, `updateAutomation`,
`deleteAutomation`) bere `tenantIds` taky. Cizi zaznam se chova jako neexistujici,
tedy 404, ne 403 - z odpovedi nemá jit poznat, ze takove ID vubec existuje.
Vyjimka je verejny webhook. Ten se autorizuje tokenem v adrese, ne prihlasenim,
takze si automatizaci najde pres vsechny firmy.
## Prirazeni jen v ramci firmy
`assignTicket` odmitne resitele z jine firmy. Jinak by ticket zmizel z prehledu
sve firmy a objevil se nekomu, kdo do ni nepatri.
Prehazovat praci mezi lidmi smi jen `admin`. `agent` si smi vzit ticket na sebe,
ale nemuze ho poslat kolegovi - to hlida `canAssignOthers`.
## Demo ucty
Heslo je u vsech `demo1234`.
| E-mail | Kdo je |
| ------------------------- | ----------------------------------------------- |
| `admin@automia.cz` | spravce platformy, vidi vsechny tri firmy |
| `karel.vomacka@automia.cz`| agent v Automii, admin u Nordisu - dve firmy |
| `martin.kriz@automia.cz` | bezny resitel jedne firmy |
Druhy ucet je ten zajimavy: ukazuje prepinac firem i to, ze prava se lisi
podle toho, ktera firma je prave zvolena.
## Co chybi
| Chybi | Poznamka |
| ------------------------ | ---------------------------------------------------- |
| Sprava clenstvi z portalu| memberships jdou zmenit jen v kodu |
| Pozvanky uzivatelu | zadny onboarding |
| Tenant u incidentu | incidenty jsou zatim spolecne, nefiltruji se |
| Tenant u konektoru | katalog je spolecny, napojeni se zatim neeviduje |
| Audit pristupu | odepreni se jen loguje, nikde se neuklada |
+23
View File
@@ -91,6 +91,29 @@ co vratil. Bez toho byla predvalidace k nicemu.
Prijmove zamerne neprirazuji, smerovani si ticket prevezme a podminkou
`assigned neni splneno` neprepise rucni rozhodnuti.
### Firmy a prava
Treti a nejvazneji dira: portal nemel zadnou tenanci. Kterykoliv prihlaseny
uzivatel videl vsechny tickety vsech firem a cely seznam resitelu, `requireRole`
se nikde nevolal. Popis v [07-firmy-a-prava.md](07-firmy-a-prava.md).
- Tenant jako hranice viditelnosti. `tenantId` na ticketu, resiteli
i automatizaci.
- Uzivatel muze patrit do **vic firem**, v kazde s jinou roli (`memberships`).
Pristup napric firmami je zvlast jako `platformAdmin`.
- Tri pohledy na tickety: `all`, `tenant`, `mine`. Admin mezi nimi prepina,
vcetne vyberu firmy.
- `src/data/access.ts` jako jedine misto, kde se rozhoduje o pravech.
`GET /api/dashboard/access` rekne klientovi, co smi kreslit.
- Filtr na firmu je v ulozistich **povinny argument**. Zapomenuty filtr
neznamena "vse", ale nezkompiluje se.
- Nikdy tise nezuzujeme. Cizi firma vraci 403 nebo 404.
- Prirazeni jen v ramci firmy. Prehazovat praci mezi lidmi smi jen admin,
agent si smi vzit ticket na sebe.
- `requireRole` nahrazen `requirePlatformAdmin`. Prava uvnitr firmy resi
`access.ts`, protoze zavisi na tom, ktera firma pozadavek zajima.
- Demo ucty pokryvaji vsechny tri situace vcetne cloveka ve dvou firmach.
### Zapsano jako otevrene rozhodnuti
Vsechny automatizace se stejnym spoustecem se spusti. Doporucene rozdeleni na to