Implementace vsech bodu z documentation/09-navrh-rozsireni.md. Vsechno lezi v obecnem ulozisti, ktere umi Postgres i JSON soubor. Zaklad, aby se nepsalo osmkrat totez: - src/data/store/: jedno rozhrani EntityStore, dve implementace (local se souborem nebo pameti, postgres nad tabulkou records). Vyber je na jednom miste v store/index.ts - src/data/store/cached.ts: synchronni kopie v pameti pro entity ctene pri kazdem requestu (uzivatel v autorizaci, firmy pri vypoctu prav). Bez toho by se autorizacni middleware musel predelat na async - src/routes/crud.ts: fabrika na CRUD routy. Kazda entita by jinak znamenala stejnych sto radku a sedmkrat by se opravila spatne - src/data/bootstrap.ts: jedno misto, kde je seznam entit a jejich vychozi sady - migrace 002_records.sql: jedna tabulka s JSONB. Tvary se jeste hybou a nikdo se nad nimi nedotazuje po polich. Az se to usadi, entita se povysi na vlastni tabulku, presne jako uz maji konektory Prava a role (bod 5): - role jsou zaznamy, pravo je retezec, katalog prav je zdroj pravdy. Union 'admin' | 'agent' na ucetni a skladnika nestacil a pridavat hodnoty je slepa ulicka, kazdy klient chce jine - Membership.roleIds misto role. Systemove role admin a agent zustavaji, takze se zadny ucet nemusel predelavat - accessFor vraci prava i zalozky. Klient si nic nedovozuje Zalozky a limity za firmu (bod 6): - TenantFeatures: moduly, limity, zpristupnene sluzby. Dve vrstvy s jinym vlastnikem, ktere se nesmi michat: co ma firma zaplacene nastavujeme my, kdo z jejich lidi to smi nastavuje jejich admin. Efektivni viditelnost je prunik, takze vypnuty modul neexistuje ani pro admina te firmy - navigace ze serveru, ne konstanta na klientovi Firmy, uzivatele, resitele a skupiny: CRUD vcetne clenstvi a hesel. Heslo se z API nikdy nevraci, ani jako hash. Skupiny resitelu kvuli tomu, ze prehazovat praci na jmeno nestaci - clovek chce rict "tohle je pro ucetni". Typy ticketu a akce (body 1, 2, 3): - typ ticketu s vlastnimi polemi, plus tagy. Akce se vazou na typ nebo tag, ale za tagem nestoji zadna pole, takze akce na tagu umi jen vestavena pole - Ticket dostal typeId, fields, tags a assigneeGroupId - definice akce s telem jako operace, strom nebo skript. Pravo vznika spolu s akci jako action:<id>, admin pak zaskrtava akce, ne prava - CTA na ticketu filtruje server podle typu, tagu, podminek a prav. Kdyby to pocital klient, pocitalo by se to na dvou mistech - vestavene akce (typ, tagy, skupina) jdou pres tutez fabriku, takze maji svoje pravo a projdou auditem - spusteni zapise do logu ticketu hned, jeste nez se neco stane Vlastni widgety (bod 4): - rozdeleni na render a source, groupBy, filtr je tentyz, ktery umi seznam ticketu. Uzivatel nepise dotazy - jeden batch endpoint na cely prehled. Widget, ktery selze, vraci chybu na sve pozici a nezhasne prehled Audit a impersonace (bod 6c): - audit zapisuje i odepreni, jinak by pokusy o cizi firmu nikde nezustaly - impersonace: jen spravce platformy, nikdy na jineho spravce platformy, 30 minut bez obnoveni, vychozi jen cteni. Zapis pod rezimem cteni vraci 403 na urovni middleware, ne az v handleru Overeno bez databaze: vsech devet ulozist se nacte, role se zaloz1 a prezije restart, agent dostane 403 na spravu roli a uzsi navigaci, neznama prava se odmitnou, heslo se nevraci, typ a tagy ticketu se ulozi, CTA se objevi, widget data pocitaji vcetne seskupeni, impersonace odmitne zapis i prepnuti na admina, audit obsahuje actedBy. Degradace pri nedostupne databazi taky overena: migrace selzou, jede se do souboru a rekne se proc. Neovereno: migrace 002_records.sql proti zive databazi. Kontejner uz nebyl k dispozici, generickou vrstvu drzi tentyz pool a migrator jako konektory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
66 lines
1.8 KiB
TypeScript
66 lines
1.8 KiB
TypeScript
/**
|
|
* Uzivatel muze patrit do vic firem najednou (napr. externista, ktery dela
|
|
* servicedesk dvema klientum). Proto `memberships`, ne jedno `tenantId`.
|
|
*
|
|
* Role je vzdy az uvnitr firmy. Pristup napric firmami je zvlast, jako
|
|
* `platformAdmin` - to je nase pravo, ne zakaznicke.
|
|
*/
|
|
|
|
/**
|
|
* Clenstvi ve firme.
|
|
*
|
|
* `roleIds` je seznam, ne jedna role. Duvod: `'admin' | 'agent'` na ucetni,
|
|
* skladnika a vedouciho nestacilo a pridavat dalsi hodnoty do unionu je slepa
|
|
* ulicka, kazdy klient chce jine. Role jsou proto zaznamy, viz `data/permissions.ts`.
|
|
*
|
|
* Odkaz smi byt ID role, nebo klic systemove role (`admin`, `agent`), aby slo
|
|
* zapsat oboji a stara data dal fungovala.
|
|
*/
|
|
export interface Membership {
|
|
tenantId: string;
|
|
roleIds: string[];
|
|
}
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
/** bcrypt hash - nikdy neposilat na klienta */
|
|
passwordHash: string;
|
|
name: string;
|
|
/** Vidi napric vsemi firmami a muze mezi nimi prepinat. */
|
|
platformAdmin: boolean;
|
|
memberships: Membership[];
|
|
}
|
|
|
|
/** Verze uzivatele bezpecna pro odeslani na klienta. */
|
|
export interface PublicUser {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
platformAdmin: boolean;
|
|
memberships: Membership[];
|
|
}
|
|
|
|
export interface JwtPayload {
|
|
sub: string;
|
|
email: string;
|
|
/**
|
|
* Jen u tokenu impersonace: kdo se za uzivatele vydava.
|
|
* Bez toho by v auditu zustalo jen "uzivatel to udelal sam".
|
|
*/
|
|
act?: string;
|
|
actEmail?: string;
|
|
/** Jen u impersonace: smi i zapisovat. */
|
|
writes?: boolean;
|
|
}
|
|
|
|
export function toPublicUser(user: User): PublicUser {
|
|
const { passwordHash: _passwordHash, ...rest } = user;
|
|
return rest;
|
|
}
|
|
|
|
/** Role uzivatele v dane firme. Prazdne pole = do firmy nepatri. */
|
|
export function rolesIn(user: User, tenantId: string): string[] {
|
|
return user.memberships.find((m) => m.tenantId === tenantId)?.roleIds ?? [];
|
|
}
|