Konektory do Postgresu, pristupove udaje sifrovane

Pristupove udaje konektoru se ukladaji do databaze a prezijou restart. Popis
v documentation/14-databaze.md.

Databaze je volitelna a rezimy jsou oddelene:
- postgres kdyz je DATABASE_URL i SECRETS_KEY
- memory jinak, tedy pri nasazenem mockupu a lokalnim vyvoji bez DB

Rozhodnuti je jen na jednom miste (src/data/connectorStore.ts). Nikde jinde se
nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se
zapomnelo a chovalo by se pak jinak nez zbytek.

Chybejici databaze nesmi shodit start: container, ktery nenastartuje, je pro
AppFactory nefunkcni sluzba. Misto toho se do logu napise proc a portal to ukaze
na strance Konektory. Stejne tak kdyz migrace selzou - psat do rozbiteho
schematu je horsi nez neukladat.

Databaze potrebuje oboji. Bez SECRETS_KEY by se udaje ukladaly v plaintextu
a to je horsi nez ztratit je pri restartu: tabulku vidi kazda zaloha a kazdy
dump pri ladeni.

Pridano:
- pool v src/db/pool.ts vcetne transakci a dbFor(tenantId) jako sev pro budouci
  oddelenou databazi jednoho klienta
- migrace ze src/db/migrations/*.sql pod pg_advisory_lock, jinak je pri rolling
  deployi pusti vsechny instance naraz. Jeden soubor je jedna transakce
- sifrovani AES-256-GCM s nahodnym IV a verzi klice. Nerozsifrovatelna hodnota
  nepada, chova se jako nevyplnena a zaloguje se - jeden rozbity konektor nesmi
  shodit seznam ostatnich
- /health/ready s pingem do DB. /health na databazi zamerne nezavisi, kratky
  vypadek by jinak vedl k restartovani containeru
- GET /api/dashboard/storage a hlaska v portalu o tom, ze data jsou jen v pameti
- jediny vychozi konektor na firmu a sluzbu hlida castecny unikatni index, ne jen
  kod. Dva soubezne zapisy by jinak udelaly dva vychozi

Zmeneno: cteni i zapis konektoru je asynchronni, vcetne validace stromu.

Overeno proti Postgresu 16 v kontejneru: migrace, sifrovani v tabulce, preziti
restartu, rozsifrovani spravnym klicem, degradace pri spatnem klici, PATCH bez
tajneho pole, prepnuti a smazani vychoziho konektoru, pametovy rezim bez
DATABASE_URL. Kontejner po overeni smazan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-12 15:08:25 +02:00
co-authored by Claude Opus 5
parent 3279dd7dac
commit 78e7f99d60
22 changed files with 1761 additions and 298 deletions
+20 -1
View File
@@ -1,4 +1,5 @@
import {
AlertTriangle,
CheckCircle2,
Eye,
EyeOff,
@@ -26,6 +27,7 @@ import type {
Service,
ServiceCredentialField,
ServiceOverview,
StorageStatus,
} from '@/types/dashboard';
/**
@@ -46,6 +48,7 @@ export default function Connectors() {
const [services, setServices] = useState<ServiceOverview | null>(null);
const [connectors, setConnectors] = useState<Connector[] | null>(null);
const [storage, setStorage] = useState<StorageStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -60,10 +63,12 @@ export default function Connectors() {
Promise.all([
apiFetch<ServiceOverview>('/api/dashboard/connectors/services'),
apiFetch<{ items: Connector[] }>('/api/dashboard/connectors'),
apiFetch<StorageStatus>('/api/dashboard/storage'),
])
.then(([overview, list]) => {
.then(([overview, list, storageStatus]) => {
setServices(overview);
setConnectors(list.items);
setStorage(storageStatus);
})
.catch((err: unknown) => {
setError(err instanceof Error ? err.message : 'Konektory se nepodařilo načíst.');
@@ -111,6 +116,20 @@ export default function Connectors() {
)}
</header>
{/*
Kam se uklada. Bez teto hlasky se clovek divi, kam se podely jeho
konektory po restartu, a hleda chybu v aplikaci.
*/}
{storage?.ephemeral && (
<p className="flex items-start gap-2 rounded-card border border-warn-400/40 bg-warn-500/8 p-3 text-sm text-warn-400">
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<span>
Přístupové údaje se ukládají jen do paměti serveru a restart je smaže.
<span className="mt-0.5 block text-white/50">{storage.reason}</span>
</span>
</p>
)}
<DataState loading={loading} error={error} onRetry={load}>
{(connectors?.length ?? 0) === 0 ? (
<p className="py-10 text-center text-sm text-white/45">
+9
View File
@@ -292,6 +292,15 @@ export interface Connector {
ready: boolean;
}
/** Kam se uklada a jestli to prezije restart. */
export interface StorageStatus {
mode: 'postgres' | 'memory';
/** Proc se jede v pameti. null u databaze. */
reason: string | null;
/** true = restart procesu data smaze. */
ephemeral: boolean;
}
export interface ConnectorTestResult {
ok: boolean;
/** Co se vlastne overilo: pristupove udaje, nebo jen dostupnost sluzby. */