Soubor jako uloziste, kdyz neni databaze
Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije restart procesu i containeru, ale ne redeploy - filesystem containeru je docasny. Je to mezistupen, ne nahrada databaze, a tak je to i napsane v portalu. | Rezim | Kdy | Restart | Redeploy | | -------- | --------------------------------- | ------- | -------- | | postgres | DATABASE_URL i SECRETS_KEY | prezije | prezije | | file | neni DB, ale je DATA_DIR | prezije | ne | | memory | ani jedno, nebo nejde zapsat | ne | ne | Rozhodnuti zustava na jednom miste (src/data/connectorStore.ts). Pridano: - src/data/snapshot.ts: atomicky zapis (.tmp a prejmenovani), slucovani zapisu a dokonceni rozepsaneho zapisu pri SIGTERM. Bez atomickeho zapisu by pad uprostred nechal polovicni JSON, ktery se pri startu nenacte. Rozbity soubor se prejmenuje na .broken a jede se dal - aplikace, ktera nenastartuje, je pro AppFactory nefunkcni sluzba - src/data/connectors/local.ts: jeden kod pro pamet i soubor, lisi se jen tim, kam se zapisuje. Nahrazuje memory.ts, dve implementace by se casem rozesly - klic k sifrovani se mimo databazi vygeneruje do DATA_DIR/secrets.key s pravy 0600, takze sifrovani funguje bez nastaveni. Chrani to proti nahodnemu precteni JSONu, ne proti pristupu k disku - klic lezi vedle dat a je to tak napsane i v portalu. U databaze se negeneruje vubec: kdo ma zalohu tabulky, ma i klic ze stejneho stroje - DATA_DIR v konfiguraci, data/ v .gitignore a .dockerignore - hlaska v portalu rozlisuje tri nasledky: pamet, soubor a databaze Overeno bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou hodnoty sifrovane a plaintext v nem neni. Pote s databazi: rezim postgres funguje dal a klic vedle dat se nevygeneroval. Kontejner i data/ po overeni smazany. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
61cf29878f
commit
6e3d0640ff
@@ -105,6 +105,14 @@ export const config = {
|
||||
databasePoolMax: positiveNumber(process.env.DATABASE_POOL_MAX, 10),
|
||||
/** Spravovane databaze vyzaduji TLS. */
|
||||
databaseSsl: process.env.DATABASE_SSL === 'true',
|
||||
/**
|
||||
* Slozka pro data mimo databazi.
|
||||
*
|
||||
* Bez `DATABASE_URL` se do ni uklada JSON, ktery prezije restart procesu
|
||||
* i containeru. Redeploy ho nezachova - filesystem containeru je docasny.
|
||||
* Prazdna hodnota vypne i tohle a jede se v ciste pameti.
|
||||
*/
|
||||
dataDir: (process.env.DATA_DIR ?? path.join(process.cwd(), 'data')).trim(),
|
||||
|
||||
// ------------------------------------------------------- skripty konektoru
|
||||
|
||||
|
||||
+96
-52
@@ -1,22 +1,27 @@
|
||||
/**
|
||||
* Konektory: vyber uloziste a jedine misto, kde se rozhoduje o rezimu.
|
||||
*
|
||||
* Dve implementace, jedno rozhrani:
|
||||
* - `postgres` kdyz je `DATABASE_URL` a `SECRETS_KEY`,
|
||||
* - `memory` jinak, tedy pri mockupu a lokalnim vyvoji bez databaze.
|
||||
* Tri rezimy, jedno rozhrani:
|
||||
*
|
||||
* Rozhodnuti je **jen tady**. Nikde jinde se nezjistuje, jestli databaze je -
|
||||
* | Rezim | Kdy | Prezije |
|
||||
* | ---------- | --------------------------------------- | ----------------- |
|
||||
* | `postgres` | je `DATABASE_URL` i klic k sifrovani | vse |
|
||||
* | `file` | neni databaze, ale je datova slozka | restart, ne redeploy |
|
||||
* | `memory` | ani jedno, nebo se nepodarilo zapsat | nic |
|
||||
*
|
||||
* Rozhodnuti je **jen tady**. Nikde jinde se nezjistuje, ktery rezim jede -
|
||||
* kdyby se to rozlezlo po kodu, jedno misto by se zapomnelo a chovalo by se
|
||||
* pak jinak nez zbytek.
|
||||
*
|
||||
* Databaze potrebuje **oboji**. Bez klice by se pristupove udaje ukladaly
|
||||
* v plaintextu a to je horsi nez ztratit je pri restartu, proto se v tom
|
||||
* pripade jede dal v pameti a rekne se to nahlas.
|
||||
* Databaze potrebuje klic ze `SECRETS_KEY`. Klic vedle dat by u ni nemel smysl:
|
||||
* kdo ma zalohu tabulky, ma i klic ze stejneho stroje.
|
||||
*/
|
||||
|
||||
import { canSealSecrets } from '../db/secretBox.js';
|
||||
import path from 'node:path';
|
||||
import { config } from '../config.js';
|
||||
import { isDatabaseEnabled } from '../db/pool.js';
|
||||
import { memoryConnectors, seedMemoryConnectors } from './connectors/memory.js';
|
||||
import { canSealSecrets, initSecrets, secretsKeySource } from '../db/secretBox.js';
|
||||
import { createLocalConnectors } from './connectors/local.js';
|
||||
import { postgresConnectors } from './connectors/postgres.js';
|
||||
import type {
|
||||
Connector,
|
||||
@@ -24,6 +29,7 @@ import type {
|
||||
CreateConnectorInput,
|
||||
UpdateConnectorInput,
|
||||
} from './connectors/types.js';
|
||||
import { fileSnapshot, memorySnapshot } from './snapshot.js';
|
||||
|
||||
export type {
|
||||
Connector,
|
||||
@@ -34,79 +40,112 @@ export type {
|
||||
} from './connectors/types.js';
|
||||
export { toPublicConnector, validateConnectorValues } from './connectors/types.js';
|
||||
|
||||
export type StorageMode = 'postgres' | 'memory';
|
||||
export type StorageMode = 'postgres' | 'file' | 'memory';
|
||||
|
||||
export interface StorageStatus {
|
||||
mode: StorageMode;
|
||||
/** Proc se jede v pameti. null u databaze. */
|
||||
reason: string | null;
|
||||
/** Kratke vysvetleni pro portal. Vzdy vyplnene, i u databaze. */
|
||||
reason: string;
|
||||
/** true = restart procesu data smaze. */
|
||||
ephemeral: boolean;
|
||||
/** true = redeploy containeru data smaze, i kdyz restart ne. */
|
||||
lostOnRedeploy: boolean;
|
||||
/** Odkud je klic k sifrovani tajnych hodnot. */
|
||||
secrets: 'env' | 'file' | 'none';
|
||||
/** Kde data lezi. null u pameti. */
|
||||
location: string | null;
|
||||
}
|
||||
|
||||
let repository: ConnectorRepository = memoryConnectors;
|
||||
let repository: ConnectorRepository = createLocalConnectors({
|
||||
store: memorySnapshot(),
|
||||
canEncrypt: false,
|
||||
});
|
||||
let flushRepository: (() => Promise<void>) | null = null;
|
||||
|
||||
let status: StorageStatus = {
|
||||
mode: 'memory',
|
||||
reason: 'Databáze není nastavená.',
|
||||
reason: 'Úložiště se ještě nevybralo.',
|
||||
ephemeral: true,
|
||||
lostOnRedeploy: true,
|
||||
secrets: 'none',
|
||||
location: null,
|
||||
};
|
||||
|
||||
export interface InitOptions {
|
||||
/**
|
||||
* false = databaze je nastavena, ale neni pouzitelna (napr. selhaly migrace).
|
||||
* Pak se jede v pameti, protoze psat do rozbiteho schematu je horsi.
|
||||
* Pak se jede bez ni, protoze psat do rozbiteho schematu je horsi.
|
||||
*/
|
||||
databaseReady?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vybere uloziste. Vola se jednou pri startu, po migracich.
|
||||
* Volani je idempotentni, at jde zavolat i z testu.
|
||||
*/
|
||||
/** Vybere uloziste. Vola se jednou pri startu, po migracich. */
|
||||
export async function initConnectorStore(options: InitOptions = {}): Promise<StorageStatus> {
|
||||
const databaseReady = options.databaseReady ?? true;
|
||||
const useDatabase = isDatabaseEnabled() && databaseReady;
|
||||
|
||||
if (isDatabaseEnabled() && !databaseReady) {
|
||||
repository = memoryConnectors;
|
||||
status = {
|
||||
mode: 'memory',
|
||||
reason: 'Databáze je nastavená, ale schéma není použitelné. Zkontrolujte log migrací.',
|
||||
ephemeral: true,
|
||||
};
|
||||
console.error(`[storage] ${status.reason}`);
|
||||
await seedMemoryConnectors();
|
||||
return status;
|
||||
}
|
||||
// Klic vedle dat je pripustny jen mimo databazi. U ni ma prijit ze secretu.
|
||||
initSecrets({ allowKeyFile: !useDatabase, dir: config.dataDir });
|
||||
const secrets = secretsKeySource();
|
||||
|
||||
if (isDatabaseEnabled() && canSealSecrets()) {
|
||||
if (useDatabase && canSealSecrets()) {
|
||||
repository = postgresConnectors;
|
||||
status = { mode: 'postgres', reason: null, ephemeral: false };
|
||||
console.info('[storage] konektory: Postgres, přístupové údaje šifrované');
|
||||
flushRepository = null;
|
||||
status = {
|
||||
mode: 'postgres',
|
||||
reason: 'Data jsou v Postgresu, přístupové údaje šifrované.',
|
||||
ephemeral: false,
|
||||
lostOnRedeploy: false,
|
||||
secrets,
|
||||
location: 'Postgres',
|
||||
};
|
||||
console.info(`[storage] konektory: Postgres, klic ze ${secrets === 'env' ? 'secretu' : 'souboru'}`);
|
||||
return status;
|
||||
}
|
||||
|
||||
repository = memoryConnectors;
|
||||
// ---------------------------------------------------------- mimo databazi
|
||||
|
||||
if (isDatabaseEnabled() && !canSealSecrets()) {
|
||||
status = {
|
||||
mode: 'memory',
|
||||
reason:
|
||||
'Databáze je nastavená, ale chybí SECRETS_KEY. Přístupové údaje by se ukládaly ' +
|
||||
'v plaintextu, takže konektory zůstávají v paměti.',
|
||||
ephemeral: true,
|
||||
};
|
||||
console.error(`[storage] ${status.reason}`);
|
||||
} else {
|
||||
status = {
|
||||
mode: 'memory',
|
||||
reason: 'Databáze není nastavená (DATABASE_URL). Restart procesu data smaže.',
|
||||
ephemeral: true,
|
||||
};
|
||||
console.warn(`[storage] ${status.reason}`);
|
||||
const reasons: string[] = [];
|
||||
if (isDatabaseEnabled() && !databaseReady) {
|
||||
reasons.push('Databáze je nastavená, ale schéma není použitelné, zkontrolujte log migrací.');
|
||||
} else if (useDatabase && !canSealSecrets()) {
|
||||
reasons.push('Databáze je nastavená, ale chybí SECRETS_KEY. Údaje by se ukládaly čitelně.');
|
||||
} else if (!isDatabaseEnabled()) {
|
||||
reasons.push('Databáze není nastavená (DATABASE_URL).');
|
||||
}
|
||||
|
||||
// Ukazkovy konektor jen v pametovem rezimu. V databazi by se delal pri kazdem startu.
|
||||
await seedMemoryConnectors();
|
||||
const useFile = config.dataDir !== '';
|
||||
const file = path.join(config.dataDir, 'connectors.json');
|
||||
|
||||
const local = createLocalConnectors({
|
||||
store: useFile ? fileSnapshot(file) : memorySnapshot(),
|
||||
canEncrypt: canSealSecrets(),
|
||||
});
|
||||
await local.init();
|
||||
|
||||
repository = local;
|
||||
flushRepository = () => local.flush();
|
||||
|
||||
if (useFile) {
|
||||
reasons.push('Data se ukládají do JSON souboru, takže přežijí restart, ale ne redeploy.');
|
||||
if (!canSealSecrets()) {
|
||||
reasons.push('Přístupové údaje se neukládají, protože není čím je zašifrovat.');
|
||||
}
|
||||
} else {
|
||||
reasons.push('Data jsou jen v paměti procesu, restart je smaže.');
|
||||
}
|
||||
|
||||
status = {
|
||||
mode: useFile ? 'file' : 'memory',
|
||||
reason: reasons.join(' '),
|
||||
// Soubor prezije restart procesu, pamet ne.
|
||||
ephemeral: !useFile,
|
||||
lostOnRedeploy: true,
|
||||
secrets,
|
||||
location: useFile ? file : null,
|
||||
};
|
||||
|
||||
console.warn(`[storage] konektory: ${status.mode} - ${status.reason}`);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -114,6 +153,11 @@ export function storageStatus(): StorageStatus {
|
||||
return status;
|
||||
}
|
||||
|
||||
/** Dokonci rozepsany zapis. Vola se pri ukonceni procesu. */
|
||||
export async function flushConnectorStore(): Promise<void> {
|
||||
if (flushRepository) await flushRepository();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- pruchod do zvoleneho uloziste
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* Konektory v pameti procesu.
|
||||
*
|
||||
* Rezim pro mockup a pro lokalni vyvoj bez databaze. **Restart procesu data
|
||||
* smaze**, vcetne pristupovych udaju. Pri vyvoji s `tsx watch` to znamena
|
||||
* po kazde uprave kodu, takze je to potreba mit na pameti.
|
||||
*
|
||||
* Podle stejneho rozhrani jede i `postgres.ts`. Rozdil se resi jen pri vyberu
|
||||
* uloziste v `../connectorStore.ts`.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
missingFields,
|
||||
nowIso,
|
||||
type Connector,
|
||||
type ConnectorRepository,
|
||||
type CreateConnectorInput,
|
||||
type UpdateConnectorInput,
|
||||
} from './types.js';
|
||||
import { findService } from '../services.js';
|
||||
|
||||
const rows: Connector[] = [];
|
||||
|
||||
function byTenant(tenantIds: string[]): Connector[] {
|
||||
return rows.filter((row) => tenantIds.includes(row.tenantId));
|
||||
}
|
||||
|
||||
function clearDefaults(tenantId: string, serviceId: string): void {
|
||||
for (const row of rows) {
|
||||
if (row.tenantId === tenantId && row.serviceId === serviceId) row.isDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const memoryConnectors: ConnectorRepository = {
|
||||
kind: 'memory',
|
||||
|
||||
async list(tenantIds, options = {}) {
|
||||
return byTenant(tenantIds)
|
||||
.filter((row) => !options.serviceId || row.serviceId === options.serviceId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'cs'))
|
||||
.map((row) => ({ ...row, values: { ...row.values } }));
|
||||
},
|
||||
|
||||
async get(id, tenantIds) {
|
||||
const row = rows.find((item) => item.id === id);
|
||||
// Cizi konektor se chova jako neexistujici, ne jako chyba prava.
|
||||
if (!row || !tenantIds.includes(row.tenantId)) return undefined;
|
||||
return { ...row, values: { ...row.values } };
|
||||
},
|
||||
|
||||
async defaultFor(tenantId, serviceId) {
|
||||
const forService = rows.filter(
|
||||
(row) => row.tenantId === tenantId && row.serviceId === serviceId && row.enabled,
|
||||
);
|
||||
const chosen = forService.find((row) => row.isDefault) ?? forService[0];
|
||||
return chosen ? { ...chosen, values: { ...chosen.values } } : undefined;
|
||||
},
|
||||
|
||||
async countsByService(tenantIds) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const row of byTenant(tenantIds)) {
|
||||
counts.set(row.serviceId, (counts.get(row.serviceId) ?? 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
|
||||
async create(input: CreateConnectorInput) {
|
||||
const timestamp = nowIso();
|
||||
const existing = rows.filter(
|
||||
(row) => row.tenantId === input.tenantId && row.serviceId === input.serviceId,
|
||||
);
|
||||
|
||||
const connector: Connector = {
|
||||
id: `con_${randomUUID().slice(0, 8)}`,
|
||||
tenantId: input.tenantId,
|
||||
serviceId: input.serviceId,
|
||||
name: input.name,
|
||||
baseUrl: input.baseUrl ?? null,
|
||||
values: { ...(input.values ?? {}) },
|
||||
enabled: true,
|
||||
status: 'untested',
|
||||
lastCheckAt: null,
|
||||
lastError: null,
|
||||
// Prvni konektor na sluzbu je vychozi, jinak by krok bez vyberu nemel co vzit.
|
||||
isDefault: input.isDefault ?? existing.length === 0,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
|
||||
if (connector.isDefault) clearDefaults(input.tenantId, input.serviceId);
|
||||
rows.push(connector);
|
||||
return { ...connector, values: { ...connector.values } };
|
||||
},
|
||||
|
||||
async update(id, patch: UpdateConnectorInput, tenantIds) {
|
||||
const row = rows.find((item) => item.id === id);
|
||||
if (!row || !tenantIds.includes(row.tenantId)) return undefined;
|
||||
|
||||
if (patch.name !== undefined) row.name = patch.name;
|
||||
if (patch.baseUrl !== undefined) row.baseUrl = patch.baseUrl;
|
||||
if (patch.enabled !== undefined) row.enabled = patch.enabled;
|
||||
|
||||
if (patch.values) {
|
||||
for (const [key, value] of Object.entries(patch.values)) {
|
||||
if (value === '') delete row.values[key];
|
||||
else row.values[key] = value;
|
||||
}
|
||||
// Zmena udaju znamena, ze predchozi overeni uz nic nerika.
|
||||
row.status = 'untested';
|
||||
row.lastError = null;
|
||||
}
|
||||
|
||||
if (patch.isDefault === true) {
|
||||
clearDefaults(row.tenantId, row.serviceId);
|
||||
row.isDefault = true;
|
||||
}
|
||||
|
||||
row.updatedAt = nowIso();
|
||||
return { ...row, values: { ...row.values } };
|
||||
},
|
||||
|
||||
async remove(id, tenantIds) {
|
||||
const row = rows.find((item) => item.id === id);
|
||||
if (!row || !tenantIds.includes(row.tenantId)) return false;
|
||||
|
||||
rows.splice(rows.indexOf(row), 1);
|
||||
|
||||
// Kdyz zmizel vychozi, prevezme to prvni zbyly - jinak by kroky bez vyberu
|
||||
// prestaly fungovat, aniz by se cokoliv jineho zmenilo.
|
||||
if (row.isDefault) {
|
||||
const next = rows.find(
|
||||
(item) => item.tenantId === row.tenantId && item.serviceId === row.serviceId,
|
||||
);
|
||||
if (next) next.isDefault = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async setStatus(id, status, error, tenantIds) {
|
||||
const row = rows.find((item) => item.id === id);
|
||||
if (!row || !tenantIds.includes(row.tenantId)) return undefined;
|
||||
|
||||
row.status = status;
|
||||
row.lastError = error;
|
||||
row.lastCheckAt = nowIso();
|
||||
row.updatedAt = row.lastCheckAt;
|
||||
return { ...row, values: { ...row.values } };
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Ukazkovy konektor bez vyplnenych udaju.
|
||||
*
|
||||
* Zamerne bez nich: ukazuje presne ten stav, ve kterem konektor vznikne, tedy
|
||||
* "sluzba je napojena, ale chybi pristupove udaje". Zaklada se jen v pametovem
|
||||
* rezimu - v databazi by se pri kazdem startu delal znovu.
|
||||
*/
|
||||
export async function seedMemoryConnectors(): Promise<void> {
|
||||
if (rows.length > 0) return;
|
||||
if (!findService('idoklad')) return;
|
||||
|
||||
await memoryConnectors.create({
|
||||
tenantId: 'tnt_automia',
|
||||
serviceId: 'idoklad',
|
||||
name: 'iDoklad Automia',
|
||||
});
|
||||
}
|
||||
|
||||
/** Jen pro diagnostiku: kolik povinnych poli chybi napric vsemi konektory. */
|
||||
export function memoryMissingCount(): number {
|
||||
return rows.reduce((total, row) => {
|
||||
const service = findService(row.serviceId);
|
||||
return total + (service ? missingFields(service, row.values).length : 0);
|
||||
}, 0);
|
||||
}
|
||||
+72
-8
@@ -12,6 +12,8 @@
|
||||
*/
|
||||
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { config } from '../config.js';
|
||||
|
||||
/** Obalka, ktera se uklada do JSONB. */
|
||||
@@ -30,21 +32,83 @@ const ALGORITHM = 'aes-256-gcm';
|
||||
const KEY_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Klic z konfigurace, srovnany na 32 bajtu.
|
||||
* Odkud je klic.
|
||||
*
|
||||
* SHA-256 z hodnoty promenne, aby fungoval jakkoliv dlouhy retezec. Neni to
|
||||
* derivace hesla (na to by patril scrypt), ale `SECRETS_KEY` ma byt nahodny
|
||||
* klic, ne heslo - a to je v dokumentaci napsane.
|
||||
* `env` ze `SECRETS_KEY`. Klic je oddeleny od dat, takze zaloha souboru
|
||||
* nebo tabulky sama nestaci. Tohle je jediny rezim vhodny do provozu.
|
||||
* `file` vygenerovany klic v datove slozce vedle dat. Chrani proti nahodnemu
|
||||
* precteni JSONu, **ne** proti nekomu, kdo ma pristup k disku - klic
|
||||
* lezi hned vedle. Je to rezim pro mockup.
|
||||
* `none` neni cim sifrovat, tajne hodnoty se neukladaji.
|
||||
*/
|
||||
export type SecretsKeySource = 'env' | 'file' | 'none';
|
||||
|
||||
let material: string | null = null;
|
||||
let source: SecretsKeySource = 'none';
|
||||
|
||||
/**
|
||||
* Nastavi klic. Vola se jednou pri startu, pred vyberem uloziste.
|
||||
*
|
||||
* `allowKeyFile` je zamerne parametr, ne automatika: v databazovem rezimu se
|
||||
* klic vedle dat generovat nesmi, tam ma prijit ze secretu.
|
||||
*/
|
||||
export function initSecrets(options: { allowKeyFile: boolean; dir: string }): SecretsKeySource {
|
||||
if (config.secretsKey !== '') {
|
||||
material = config.secretsKey;
|
||||
source = 'env';
|
||||
return source;
|
||||
}
|
||||
|
||||
if (!options.allowKeyFile) {
|
||||
material = null;
|
||||
source = 'none';
|
||||
return source;
|
||||
}
|
||||
|
||||
const file = path.join(options.dir, 'secrets.key');
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
material = fs.readFileSync(file, 'utf8').trim();
|
||||
} else {
|
||||
fs.mkdirSync(options.dir, { recursive: true });
|
||||
material = randomBytes(32).toString('base64url');
|
||||
// 0600: cist smi jen vlastnik procesu. Na Windows se prava ignoruji.
|
||||
fs.writeFileSync(file, material, { encoding: 'utf8', mode: 0o600 });
|
||||
console.info(`[secrets] vygenerovan klic v ${file}`);
|
||||
}
|
||||
source = 'file';
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'[secrets] klic v souboru nelze pouzit, tajne hodnoty se nebudou ukladat:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
material = null;
|
||||
source = 'none';
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
export function secretsKeySource(): SecretsKeySource {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Klic srovnany na 32 bajtu.
|
||||
*
|
||||
* SHA-256 z hodnoty, aby fungoval jakkoliv dlouhy retezec. Neni to derivace
|
||||
* hesla (na to by patril scrypt), ale klic ma byt nahodny retezec, ne heslo -
|
||||
* a to je v dokumentaci napsane.
|
||||
*/
|
||||
function key(): Buffer {
|
||||
if (config.secretsKey === '') {
|
||||
throw new Error('SECRETS_KEY neni nastavena, pristupove udaje nelze sifrovat.');
|
||||
if (material === null) {
|
||||
throw new Error('Klic pro sifrovani neni nastaveny, pristupove udaje nelze sifrovat.');
|
||||
}
|
||||
return createHash('sha256').update(config.secretsKey).digest();
|
||||
return createHash('sha256').update(material).digest();
|
||||
}
|
||||
|
||||
export function canSealSecrets(): boolean {
|
||||
return config.secretsKey !== '';
|
||||
return material !== null;
|
||||
}
|
||||
|
||||
export function seal(value: string): SealedValue {
|
||||
|
||||
+8
-3
@@ -16,7 +16,7 @@ import { contactRouter } from './routes/contact.js';
|
||||
import { dashboardRouter } from './routes/dashboard.js';
|
||||
import { simulateRouter } from './routes/simulate.js';
|
||||
import { webhookRouter } from './routes/webhook.js';
|
||||
import { initConnectorStore, storageStatus } from './data/connectorStore.js';
|
||||
import { flushConnectorStore, initConnectorStore, storageStatus } from './data/connectorStore.js';
|
||||
import { runMigrations } from './db/migrate.js';
|
||||
import { closeDatabase, databaseHealth, isDatabaseEnabled } from './db/pool.js';
|
||||
import { ensureLoaded, scriptsDir } from './scripts/registry.js';
|
||||
@@ -227,7 +227,7 @@ const server = app.listen(config.port, '0.0.0.0', () => {
|
||||
console.info(`[start] health: ${config.rootPath}/health, docs: ${config.rootPath}/docs`);
|
||||
console.info(`[start] skripty konektoru: ${scriptsDir()}`);
|
||||
const storage = storageStatus();
|
||||
console.info(`[start] uloziste konektoru: ${storage.mode}${storage.reason ? ` (${storage.reason})` : ''}`);
|
||||
console.info(`[start] uloziste konektoru: ${storage.mode}${storage.location ? ` (${storage.location})` : ''}`);
|
||||
});
|
||||
|
||||
// Pool se pri ukonceni zavre, at se spojeni neopousti otevrena.
|
||||
@@ -235,7 +235,12 @@ for (const signal of ['SIGTERM', 'SIGINT'] as const) {
|
||||
process.on(signal, () => {
|
||||
console.info(`[stop] ${signal}, ukoncuji`);
|
||||
server.close(() => {
|
||||
void closeDatabase().finally(() => process.exit(0));
|
||||
// Rozepsany zapis do souboru se musi dokoncit, jinak se posledni zmena
|
||||
// ztrati - debounce je kratky, ale nenulovy.
|
||||
void flushConnectorStore()
|
||||
.catch((err: unknown) => console.error('[stop] zapis dat selhal:', err))
|
||||
.then(() => closeDatabase())
|
||||
.finally(() => process.exit(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user