Cely navrh rozsireni: role, firmy, typy ticketu, akce, widgety, audit
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e3d0640ff
commit
e7cf499a0b
+22
-7
@@ -6,12 +6,19 @@
|
||||
* `platformAdmin` - to je nase pravo, ne zakaznicke.
|
||||
*/
|
||||
|
||||
/** Role uvnitr jedne firmy. */
|
||||
export type TenantRole = 'admin' | 'agent';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
role: TenantRole;
|
||||
roleIds: string[];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@@ -37,6 +44,14 @@ export interface PublicUser {
|
||||
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 {
|
||||
@@ -44,7 +59,7 @@ export function toPublicUser(user: User): PublicUser {
|
||||
return rest;
|
||||
}
|
||||
|
||||
/** Role uzivatele v dane firme, nebo undefined kdyz do ni nepatri. */
|
||||
export function roleIn(user: User, tenantId: string): TenantRole | undefined {
|
||||
return user.memberships.find((m) => m.tenantId === tenantId)?.role;
|
||||
/** 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 ?? [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user