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
@@ -4,11 +4,20 @@ import { config } from '../config.js';
|
||||
import { findUserById } from '../data/users.js';
|
||||
import type { JwtPayload, User } from '../types.js';
|
||||
|
||||
/** Kdyz je request pod impersonaci, tady je, kdo za tim opravdu je. */
|
||||
export interface Impersonation {
|
||||
actorId: string;
|
||||
actorEmail: string;
|
||||
/** false = jen cteni. Vychozi rezim, zapis se musi vyslovne zapnout. */
|
||||
allowWrites: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User;
|
||||
impersonation?: Impersonation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +39,26 @@ export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
return res.status(401).json({ error: 'unauthorized', message: 'Uživatel neexistuje.' });
|
||||
}
|
||||
req.user = user;
|
||||
|
||||
if (payload.act) {
|
||||
req.impersonation = {
|
||||
actorId: payload.act,
|
||||
actorEmail: payload.actEmail ?? payload.act,
|
||||
allowWrites: payload.writes === true,
|
||||
};
|
||||
|
||||
// Jen cteni znamena jen cteni. Bez teto vetve by rezim byl jen napis.
|
||||
if (!req.impersonation.allowWrites && req.method !== 'GET') {
|
||||
console.warn(
|
||||
`[auth] ${req.impersonation.actorEmail} zkusil zapis za ${user.email} bez povoleni`,
|
||||
);
|
||||
return res.status(403).json({
|
||||
error: 'forbidden',
|
||||
message: 'Přepnutí na jiný účet je jen pro čtení. Zapisovat takhle nelze.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
console.warn('[auth] neplatny token:', err instanceof Error ? err.message : err);
|
||||
|
||||
Reference in New Issue
Block a user