Revize projektu: prava, vykon, runtime, portal a ARES

Prava a bezpecnost: spravce firmy uz nemuze nastavit priznak spravce
platformy ani clenstvi v cizi firme; pozvanky, konektory a automatizace
kontroluji sve pravo; cizi firma v query je 404; zivy stream posila
udalosti jen firmam, kterych se tykaji; akce nad ticketem maji kontrolu
prava za firmu ticketu a strop viditelnosti; tokeny se nelogujou; limit
pokusu na prihlaseni, kontakt a pozvanky; bezpecnostni hlavicky;
zachyceni chyb v async handlerech; timing-safe porovnani tokenu.

Vykon: audit neskenuje celou kolekci pri kazdem zapisu a konecne maze
firemni zaznamy; ticket se uklada jednou misto trikrat; zapisy do
Postgresu jsou serializovane podle ID; prava se pocitaji jednou na
request; widgety nacitaji tickety jednou; strankovani seznamu; worker
je pool misto kol; na webu udalost ze streamu neodmontuje stranku,
dotazy maji spolecny debounce a cache, ciselniky drzi typovany sklad.

Runtime: opakuji se jen chyby oznacene retryable; smycka nenarazi na
strop 50 kroku (novy strop 1000 akci); podminka nad datem funguje;
vystup MCP nastroje neprepisuje spoustec; sandbox skriptu firmy nejde
opustit; MCP session id se drzi mezi volanimi; incident z kroku patri
firme; jedno rozhodnuti o rezimu uloziste; snapshot neprepise soubor
po chybe cteni.

Refaktory: sdilene typy API v src/shared (web nic nekopiruje, osm
rozjetych tvaru sjednoceno); spolecny modul net/guard pro volani ven;
formularova vrstva ui/form; rozdeleni Connectors a FlowCanvas; jeden
helper pro firmu z query, validaci a CRUD udalosti; pomucky ctx.util
pro skripty konektoru; i18n verejneho webu vcetne anglictiny.

Nova funkce: zalozeni firmy z registru ARES v Nastaveni (IC nebo nazev,
dotazeni IC, DIC, sidla a pravni formy, vyber soucasnych statutarnich
zastupcu a prokury, ucty spravce firmy s nahradnim e-mailem
IC-poradi@placeholder.cz).

Dokumentace: zaznam v 99-zmeny.md a aktualizace 15 dalsich dokumentu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-09 10:26:07 +02:00
co-authored by Claude Fable 5.1
parent 0c405ea55a
commit 104ae36783
215 changed files with 13226 additions and 8320 deletions
+4 -2
View File
@@ -60,8 +60,10 @@ export const manifest = {
* @param {{
* http: { get: Function, post: Function, patch: Function, put: Function, del: Function,
* postForm: Function },
* util: { unwrap: Function, pick: Function, first: Function, text: Function,
* num: Function, bool: Function, date: Function, round: Function, need: Function },
* util: { unwrap: Function, pick: Function, first: Function, list: Function,
* text: Function, num: Function, bool: Function, date: Function, day: Function,
* round: Function, need: Function, get: Function, addresses: Function,
* quote: Function, applyRules: Function, fillJson: Function },
* log: Function, config: Record<string, string>, idempotencyKey: string,
* fail: Function, retry: Function,
* }} ctx
+3 -17
View File
@@ -60,26 +60,12 @@ function digits(value) {
return String(value ?? '').replace(/[^0-9]/g, '');
}
/**
* Seznam z odpovedi banky.
*
* COBS obaluje kolekci pod vlastni nazev (`accounts`, `transactions`), jiny
* standard pouziva `content` nebo `items`. Hadat jeden tvar by znamenalo, ze
* pri zmene obalky krok tise vrati nula zaznamu misto chyby.
*/
function collection(ctx, body, ...names) {
if (Array.isArray(body)) return body;
const found = ctx.util.pick(body, ...names, 'data', 'content', 'items');
if (Array.isArray(found)) return found;
const unwrapped = ctx.util.unwrap(body);
return Array.isArray(unwrapped) ? unwrapped : null;
}
export async function run(inputs, ctx) {
const { pick, text, need } = ctx.util;
const { body: accountsBody } = await ctx.http.get('/accounts', { query: { size: 50 } });
const accounts = collection(ctx, accountsBody, 'accounts');
// COBS obaluje kolekci pod vlastni nazev, `list` zkusi i obvykle obalky.
const accounts = ctx.util.list(accountsBody, 'accounts');
if (accounts === null) ctx.fail('Odpověď banky nemá seznam účtů.', accountsBody);
if (accounts.length === 0) {
ctx.fail('Souhlas nedosáhne na žádný účet. Zkontrolujte platnost souhlasu v ČSOB.');
@@ -117,7 +103,7 @@ export async function run(inputs, ctx) {
},
});
const transactions = collection(ctx, body, 'transactions');
const transactions = ctx.util.list(body, 'transactions');
if (transactions === null) ctx.fail('Odpověď banky nemá seznam pohybů.', body);
return {
+2 -9
View File
@@ -56,14 +56,6 @@ export const manifest = {
timeoutMs: 30000,
};
/** Adresy z jednoho radku. Clovek pise carky, Gmail chce jeden retezec. */
function addresses(value) {
return String(value ?? '')
.split(/[,;]/)
.map((item) => item.trim())
.filter((item) => item !== '');
}
/** Base64 z textu. Pres bajty, aby diakritika prezila. */
function base64(value) {
const bytes = new TextEncoder().encode(value);
@@ -86,8 +78,9 @@ function header(value) {
}
export async function run(inputs, ctx) {
const { pick, text, need } = ctx.util;
const { pick, text, need, addresses } = ctx.util;
// Clovek pise carky, Gmail chce jeden retezec.
const to = addresses(inputs.to);
if (to.length === 0) ctx.fail('Nezůstal žádný příjemce, zkontrolujte pole Příjemci.');
const cc = addresses(inputs.cc);
+5 -9
View File
@@ -88,11 +88,6 @@ export const manifest = {
],
};
/** Datum ve tvaru, ktery iDoklad ceka. */
function isoDay(value) {
return new Date(value).toISOString().slice(0, 10);
}
function addDays(value, days) {
const date = new Date(value);
date.setUTCDate(date.getUTCDate() + days);
@@ -100,7 +95,8 @@ function addDays(value, days) {
}
export async function run(inputs, ctx) {
const { unwrap, pick, text, num, date, need } = ctx.util;
// `day` dava datum ve tvaru, ktery iDoklad ceka (2026-08-20).
const { unwrap, pick, text, num, date, need, day } = ctx.util;
if (inputs.unitPrice < 0) ctx.fail('Cena za jednotku nemůže být záporná.');
const amount = inputs.amount ?? 1;
@@ -120,9 +116,9 @@ export async function run(inputs, ctx) {
...defaults,
partnerId: inputs.partnerId,
description: inputs.description,
dateOfIssue: isoDay(issuedAt),
dateOfTaxing: isoDay(issuedAt),
dateOfMaturity: isoDay(maturityAt),
dateOfIssue: day(issuedAt),
dateOfTaxing: day(issuedAt),
dateOfMaturity: day(maturityAt),
items: [
{
name: inputs.itemName,
+2 -6
View File
@@ -52,12 +52,8 @@ export const manifest = {
],
};
function isoDay(value) {
return new Date(value).toISOString().slice(0, 10);
}
export async function run(inputs, ctx) {
const { unwrap, pick, num, date, need } = ctx.util;
const { unwrap, pick, num, date, need, day } = ctx.util;
if (inputs.amount !== null && inputs.amount <= 0) {
ctx.fail('Uhrazená částka musí být větší než nula.');
@@ -81,7 +77,7 @@ export async function run(inputs, ctx) {
...defaults,
invoiceId: inputs.invoiceId,
paymentAmount: amount,
dateOfPayment: isoDay(paidAt),
dateOfPayment: day(paidAt),
sendPaymentConfirmation: inputs.sendConfirmation ?? false,
};
+3 -9
View File
@@ -66,16 +66,10 @@ export const manifest = {
timeoutMs: 30000,
};
/** Seznam adres z jednoho radku. Graph chce pole objektu, clovek pise carky. */
function addresses(value) {
return String(value ?? '')
.split(/[,;]/)
.map((item) => item.trim())
.filter((item) => item !== '')
.map((address) => ({ address }));
}
export async function run(inputs, ctx) {
// Graph chce pole objektu, clovek pise carky.
const addresses = (value) => ctx.util.addresses(value).map((address) => ({ address }));
const to = addresses(inputs.to);
if (to.length === 0) ctx.fail('Nezůstal žádný příjemce, zkontrolujte pole Příjemci.');
+3 -8
View File
@@ -53,11 +53,6 @@ export const manifest = {
timeoutMs: 30000,
};
/** Retezec do OData filtru. Apostrof se zdvojuje, jinak dotaz spadne. */
function quote(value) {
return `'${String(value).replace(/'/g, "''")}'`;
}
const notFound = {
found: false,
cardCode: null,
@@ -76,9 +71,9 @@ export async function run(inputs, ctx) {
// Poradi je zamer: kod je klic, ICO je jednoznacne, nazev se pise pokazde jinak.
const attempts = [];
if (inputs.cardCode) attempts.push([`CardCode eq ${quote(inputs.cardCode)}`, 'kód']);
if (inputs.taxId) attempts.push([`FederalTaxID eq ${quote(inputs.taxId)}`, 'IČO']);
if (inputs.name) attempts.push([`contains(CardName,${quote(inputs.name)})`, 'název']);
if (inputs.cardCode) attempts.push([`CardCode eq ${ctx.util.quote(inputs.cardCode)}`, 'kód']);
if (inputs.taxId) attempts.push([`FederalTaxID eq ${ctx.util.quote(inputs.taxId)}`, 'IČO']);
if (inputs.name) attempts.push([`contains(CardName,${ctx.util.quote(inputs.name)})`, 'název']);
for (const [filter, label] of attempts) {
const { body } = await ctx.http.get('/api/business-partners', {
+2 -5
View File
@@ -51,13 +51,10 @@ export const manifest = {
timeoutMs: 30000,
};
function quote(value) {
return `'${String(value).replace(/'/g, "''")}'`;
}
export async function run(inputs, ctx) {
const { pick, num, text, round } = ctx.util;
const { pick, num, text, round, quote } = ctx.util;
// Apostrof v OData filtru se zdvojuje, to dela `quote`.
const conditions = [];
if (inputs.cardCode) conditions.push(`CardCode eq ${quote(inputs.cardCode)}`);
if (inputs.dateFrom) conditions.push(`DocDate ge ${quote(inputs.dateFrom.slice(0, 10))}`);
+1 -5
View File
@@ -66,11 +66,7 @@ function statRows(ctx, body) {
});
}
const stats = pick(body, 'stats');
if (Array.isArray(stats)) return stats;
const data = pick(body, 'data');
return Array.isArray(data) ? data : [];
return ctx.util.list(body, 'stats') ?? [];
}
export async function run(inputs, ctx) {