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:
co-authored by
Claude Opus 5
parent
3279dd7dac
commit
78e7f99d60
@@ -36,7 +36,8 @@ React aplikaci ze slozky `dist/public`.
|
||||
| Sprava clenstvi z portalu | chybi | memberships jdou zmenit jen v kodu |
|
||||
| Bugs a wishes | chybi | vyvojarska agenda, samostatna evidence vedle ticketu |
|
||||
| Beh automatizaci | chybi | ulozeny strom se nevykonava, neni runtime |
|
||||
| Databaze | chybi | data jsou v pameti, restart je vrati na vychozi stav |
|
||||
| Databaze pro konektory | hotovo | Postgres, udaje sifrovane. Bez DATABASE_URL jede pamet |
|
||||
| Databaze pro zbytek | chybi | automatizace, rozlozeni a tickety jsou v pameti |
|
||||
| Odesilani e-mailu z formulare | chybi | poptavka se zatim jen loguje |
|
||||
|
||||
## Znama omezeni
|
||||
|
||||
@@ -9,7 +9,8 @@ Verejne:
|
||||
|
||||
| Metoda | Cesta | Popis |
|
||||
| ------ | ------------------- | --------------------------------------- |
|
||||
| GET | `/health` | health check |
|
||||
| GET | `/health` | liveness, nezavisi na databazi |
|
||||
| GET | `/health/ready` | readiness, 503 pri nedostupne databazi |
|
||||
| GET | `/docs` | Swagger UI |
|
||||
| GET | `/openapi.json` | OpenAPI definice |
|
||||
| POST | `/api/auth/login` | prihlaseni, vraci JWT |
|
||||
@@ -36,6 +37,7 @@ Vyzaduji `Authorization: Bearer <token>`:
|
||||
| POST | `/api/dashboard/tickets/:id/status` |
|
||||
| POST | `/api/dashboard/tickets/:id/comment` |
|
||||
| GET | `/api/dashboard/incidents` |
|
||||
| GET | `/api/dashboard/storage` |
|
||||
| GET | `/api/dashboard/services` |
|
||||
| GET | `/api/dashboard/connectors/services` |
|
||||
| GET | `/api/dashboard/connectors` |
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# 14 - Databaze
|
||||
|
||||
Naprogramovano a overeno proti Postgresu 16. Zatim se do databaze ukladaji
|
||||
**konektory**, tedy pristupove udaje k sluzbam. Zbytek je v pameti procesu,
|
||||
poradi dalsich kroku je na konci.
|
||||
|
||||
## Databaze je volitelna, ale rezimy jsou oddelene
|
||||
|
||||
Aplikace jede ve dvou rezimech a rozdil se resi **na jednom miste**,
|
||||
v `src/data/connectorStore.ts`. Nikde jinde se nezjistuje, jestli databaze je -
|
||||
kdyby se to rozlezlo po kodu, jedno misto by se zapomnelo.
|
||||
|
||||
| Rezim | Kdy | Prezije restart |
|
||||
| ---------- | --------------------------------------- | --------------- |
|
||||
| `postgres` | je `DATABASE_URL` **i** `SECRETS_KEY` | ano |
|
||||
| `memory` | jinak | ne |
|
||||
|
||||
Chybejici databaze **nesmi shodit start**. Container, ktery nenastartuje, je pro
|
||||
AppFactory nefunkcni sluzba (AGENTS.md). Misto toho se do logu napise, proc se
|
||||
jede v pameti, a portal to ukaze na strance Konektory.
|
||||
|
||||
Databaze potrebuje **oboji**. Bez klice by se pristupove udaje ukladaly
|
||||
v plaintextu, a to je horsi nez ztratit je pri restartu - tabulku vidi kazda
|
||||
zaloha a kazdy dump pri ladeni.
|
||||
|
||||
Stejne tak: kdyz jsou migrace nastavene, ale selzou, jede se dal v pameti.
|
||||
Psat do rozbiteho schematu je horsi nez neukladat.
|
||||
|
||||
## Promenne
|
||||
|
||||
| Promenna | K cemu |
|
||||
| ------------------- | ---------------------------------------------------------- |
|
||||
| `DATABASE_URL` | `postgres://uzivatel:heslo@host:5432/csbot` |
|
||||
| `SECRETS_KEY` | klic pro sifrovani pristupovych udaju, **secret** |
|
||||
| `DATABASE_POOL_MAX` | kolik spojeni si vezme jedna instance, vychozi 10 |
|
||||
| `DATABASE_SSL` | `true` u spravovanych databazi, ktere vyzaduji TLS |
|
||||
|
||||
`SECRETS_KEY` ma byt nahodny retezec, ne heslo:
|
||||
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
||||
```
|
||||
|
||||
**Klic se nesmi ztratit ani zmenit bez prevodu dat.** Bez nej se ulozene udaje
|
||||
nerozsifruji. Nic se nerozbije, jen se konektory chovaji jako nevyplnene
|
||||
a v logu je napsane proc - udaje se pak zadaji znovu.
|
||||
|
||||
## Lokalni spusteni
|
||||
|
||||
```bash
|
||||
docker run -d --name csbot-pg -p 5433:5432 \
|
||||
-e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=csbot postgres:16-alpine
|
||||
|
||||
export DATABASE_URL="postgres://postgres:devpass@127.0.0.1:5433/csbot"
|
||||
export SECRETS_KEY="$(node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))")"
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Migrace se pousti samy pri startu. Kontrola, ze to jede z databaze:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3000/health/ready
|
||||
```
|
||||
|
||||
Bez promennych `npm run dev` funguje dal, jen v pameti.
|
||||
|
||||
## Migrace
|
||||
|
||||
Soubory `src/db/migrations/*.sql`, v abecednim poradi, kazdy jednou.
|
||||
Co uz proslo, je v tabulce `schema_migrations`.
|
||||
|
||||
Dve veci, na kterych to stoji:
|
||||
|
||||
- **Poradovy zamek.** Pri rolling deployi startuje vic instanci naraz a bez
|
||||
`pg_advisory_lock` by migrace pustily vsechny.
|
||||
- **Jeden soubor je jedna transakce.** Pri chybe se z nej neuplatni nic, takze
|
||||
nevznikne rozdelane schema, o kterem nikdo nevi.
|
||||
|
||||
**Migrace se nikdy neupravuji zpetne.** Uz projely u nekoho jineho, takze zmena
|
||||
souboru znamena dve rozdilna schemata se stejnym cislem. Oprava je vzdy novy
|
||||
soubor.
|
||||
|
||||
## Sifrovani pristupovych udaju
|
||||
|
||||
`src/db/secretBox.ts`, AES-256-GCM.
|
||||
|
||||
```json
|
||||
{ "v": 1, "iv": "...", "tag": "...", "data": "..." }
|
||||
```
|
||||
|
||||
- **GCM**, ne CBC: sifruje a zaroven overuje, ze s daty nikdo nehybal.
|
||||
- **Nahodne IV** pro kazdou hodnotu, aby dve stejne hodnoty nedaly stejnou sifru.
|
||||
- **`v` je verze klice.** Vymena klice pak znamena precist starym a zapsat novym,
|
||||
ne zahodit vsechna napojeni.
|
||||
- **Nerozsifrovatelna hodnota nepada.** Jeden rozbity konektor nesmi shodit
|
||||
seznam vsech ostatnich, takze se chova jako nevyplneny a zaloguje se to.
|
||||
|
||||
Sifruji se vsechna pole, i necitliva. Je to jednodussi nez rozhodovat u kazdeho
|
||||
zvlast a nic to nestoji.
|
||||
|
||||
## Schema
|
||||
|
||||
`connectors` (migrace `001_connectors.sql`):
|
||||
|
||||
| Sloupec | Poznamka |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| `tenant_id` | povinne, index zacina jim |
|
||||
| `service_id` | odkaz do katalogu v kodu, ne do tabulky |
|
||||
| `secrets` | JSONB se sifrovanymi hodnotami, nikdy plaintext |
|
||||
| `is_default` | jediny vychozi na firmu a sluzbu, hlida index |
|
||||
|
||||
**Sluzby v databazi nejsou.** Jsou to definice, ktere delame my, a repo je u nich
|
||||
zdroj pravdy kvuli code review a historii v gitu. Rucne upraveny radek v produkci
|
||||
nikdo za tri mesice nedohleda. Duvody jsou v [12-sluzby-a-konektory.md](12-sluzby-a-konektory.md).
|
||||
|
||||
Vychozi konektor hlida **castecny unikatni index**, ne jen kod:
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX connectors_one_default_idx
|
||||
ON connectors (tenant_id, service_id) WHERE is_default;
|
||||
```
|
||||
|
||||
Bez nej by dva soubezne zapisy udelaly dva vychozi a krok bez vybraneho
|
||||
konektoru by si vybiral podle nahody.
|
||||
|
||||
## Dvere k oddelene databazi
|
||||
|
||||
`dbFor(tenantId)` dnes vraci vzdy tentyz pool. Je to zamerny sev: jednou prijde
|
||||
klient, ktery bude chtit vlastni databazi nebo bude delat tricet procent provozu,
|
||||
a presun ma byt konfigurace, ne prepisovani dotazu.
|
||||
|
||||
Podminka je **nikdy nespojovat dotazem dva klienty**, coz uz vynucuje povinny
|
||||
argument `tenantIds` v ulozistich. Podrobnosti
|
||||
v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md).
|
||||
|
||||
## Health
|
||||
|
||||
| Endpoint | Zavisi na DB | K cemu |
|
||||
| --------------- | ------------ | ----------------------------------------- |
|
||||
| `/health` | ne | liveness, AppFactory podle nej restartuje |
|
||||
| `/health/ready` | ano | readiness, vraci 503 pri nedostupne DB |
|
||||
|
||||
`/health` **nesmi** na databazi zavisel. Kratky vypadek DB by jinak vedl
|
||||
k restartovani containeru, coz nic nespravi. Vysledek pingu se par sekund
|
||||
cachuje, aby monitoring nedelal dotaz pri kazdem pingu.
|
||||
|
||||
## Pool a jedno pravidlo
|
||||
|
||||
`DATABASE_POOL_MAX` je vychozi 10 a je to zamerne malo. **Worker nesmi drzet
|
||||
spojeni po dobu volani ciziho API** - volani do iDokladu trva 300 ms a pri
|
||||
stovce soubeznych kroku by to bylo sto obsazenych spojeni. Se spravnym poradim
|
||||
(odeber ulohu, uvolni spojeni, volej, zapis) staci par.
|
||||
|
||||
Az bude instanci vic, prijde PgBouncer v transakcnim rezimu. **Pozor: v nem
|
||||
nefunguje `LISTEN/NOTIFY`**, na kterem ma stat sbernice udalosti pro SSE.
|
||||
Ta pak potrebuje prime spojeni mimo PgBouncer.
|
||||
|
||||
## Co bylo overeno
|
||||
|
||||
Proti Postgresu 16 v kontejneru:
|
||||
|
||||
| Co | Vysledek |
|
||||
| ----------------------------------------------------- | -------- |
|
||||
| Migrace projedou a zapisou se do `schema_migrations` | ano |
|
||||
| Udaje jsou v tabulce sifrovane, plaintext nikde | ano |
|
||||
| Konektor prezije restart procesu | ano |
|
||||
| Se spravnym klicem se udaje rozsifruji | ano |
|
||||
| Se spatnym klicem se chovaji jako nevyplnene a loguje se | ano |
|
||||
| `PATCH` bez tajneho pole tajne pole nesmaze | ano |
|
||||
| Prepnuti vychoziho konektoru | ano |
|
||||
| Smazani vychoziho preda priznak zbylemu | ano |
|
||||
| Bez `DATABASE_URL` jede pametovy rezim a rekne to | ano |
|
||||
|
||||
## Co chybi
|
||||
|
||||
| Chybi | Poznamka |
|
||||
| ---------------------------- | ----------------------------------------------------- |
|
||||
| Automatizace v databazi | dalsi na rade, je to to, co si clovek nastavi |
|
||||
| Rozlozeni dashboardu | male a samostatne, hned po automatizacich |
|
||||
| Tickety a incidenty | naposled, dnes je generuje simulace |
|
||||
| Uzivatele, firmy, resitele | v prototypu je to spis konfigurace nez data |
|
||||
| Sbernice udalosti pres LISTEN/NOTIFY | dnes `EventEmitter` v pameti jedne instance |
|
||||
| Vymena klice (rotace) | `v` je pripravene, prevod dat napsany neni |
|
||||
| Retence a partitionovani | az u tabulek behu, viz dokument 10 |
|
||||
@@ -2,6 +2,45 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-08-12 - databaze pro konektory
|
||||
|
||||
Konektory se ukladaji do Postgresu, pristupove udaje sifrovane.
|
||||
Popis v [14-databaze.md](14-databaze.md).
|
||||
|
||||
### Pridano
|
||||
|
||||
- `pg` jako zavislost, pool v `src/db/pool.ts` vcetne transakci a `dbFor(tenantId)`
|
||||
jako sev pro budouci oddelenou databazi jednoho klienta.
|
||||
- Migrace ze souboru `src/db/migrations/*.sql`, pousti se pri startu pod
|
||||
`pg_advisory_lock` - pri rolling deployi je jinak pusti vsechny instance naraz.
|
||||
Jeden soubor je jedna transakce, takze pri chybe nevznikne rozdelane schema.
|
||||
- Sifrovani pristupovych udaju (`src/db/secretBox.ts`), 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.
|
||||
- Dve implementace uloziste konektoru za jednim rozhranim (`memory`, `postgres`).
|
||||
Rozhodnuti je jen na jednom miste, v `src/data/connectorStore.ts`.
|
||||
- `/health/ready` s pingem do databaze. `/health` na databazi zamerne nezavisi:
|
||||
kratky vypadek DB by jinak vedl k restartovani containeru.
|
||||
- `GET /api/dashboard/storage` a hlaska na strance Konektory o tom, ze data jsou
|
||||
jen v pameti. Bez toho se clovek divi, kam se podely jeho konektory.
|
||||
- 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.
|
||||
- Databaze je volitelna. Bez `DATABASE_URL` nebo `SECRETS_KEY` se jede v pameti
|
||||
a rekne se to v logu i v portalu. Container, ktery nenastartuje, je pro
|
||||
AppFactory nefunkcni sluzba.
|
||||
- Kdyz jsou migrace nastavene a selzou, jede se dal v pameti. Psat do rozbiteho
|
||||
schematu je horsi nez neukladat.
|
||||
|
||||
### 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, a pametovy rezim bez `DATABASE_URL`.
|
||||
|
||||
## 2026-08-12 - transformace dat a oprava konektoru
|
||||
|
||||
Transformace dat popsana v [13-transformace-dat.md](13-transformace-dat.md).
|
||||
|
||||
Reference in New Issue
Block a user