// ═══════════════════════════════════════════════════════════════════════════════ // V2 — инициализация схемы БД // // ЭТО: CREATE TABLE IF NOT EXISTS — безопасный повторный запуск. // ЗАЧЕМ: // 1. Приложение само создаёт таблицы при старте. // 2. Безопасно для production: IF NOT EXISTS. // // ТАБЛИЦЫ: // companies — client_id (W-номер), name, custom_limit // whitelist_entries — CIDR, комментарий, кто создал/изменил/удалил // audit_log — все действия: CREATE/UPDATE/DELETE/RESTORE, с impersonated_by // ═══════════════════════════════════════════════════════════════════════════════ async function ensureSchema(pool) { await pool.query(` -- Компании: W-номер уникален, upsert через ON CONFLICT CREATE TABLE IF NOT EXISTS companies ( id SERIAL PRIMARY KEY, client_id VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(255), custom_limit INTEGER DEFAULT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ ); -- Записи: CIDR + метаданные, soft delete CREATE TABLE IF NOT EXISTS whitelist_entries ( id SERIAL PRIMARY KEY, company_id INTEGER NOT NULL, value_cidr VARCHAR(64) NOT NULL, comment TEXT, created_by VARCHAR(255), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by VARCHAR(255), updated_at TIMESTAMPTZ, deleted_by VARCHAR(255), deleted_at TIMESTAMPTZ ); -- Защита от дублей: уникальный CIDR в компании среди активных записей CREATE UNIQUE INDEX IF NOT EXISTS idx_whitelist_entries_active_cidr ON whitelist_entries(company_id, value_cidr) WHERE deleted_at IS NULL; -- Аудит: каждое действие с возможностью отследить имперсонацию CREATE TABLE IF NOT EXISTS audit_log ( id SERIAL PRIMARY KEY, user_email VARCHAR(255) NOT NULL, company_id INTEGER NOT NULL, action VARCHAR(32) NOT NULL CHECK (action IN ('CREATE','UPDATE','DELETE','RESTORE')), old_value TEXT, new_value TEXT, entry_id INTEGER, impersonated_by VARCHAR(255) DEFAULT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_audit_log_company_time ON audit_log(company_id, created_at DESC); `); // Миграция: impersonated_by (если колонки ещё нет) await pool.query(` ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS impersonated_by VARCHAR(255) DEFAULT NULL `).catch(() => {}); // Миграция: добавить RESTORE в CHECK (если ещё нет) await pool.query(` DO $$ DECLARE cn text; BEGIN SELECT con.conname INTO cn FROM pg_constraint con JOIN pg_class rel ON rel.oid = con.conrelid WHERE rel.relname = 'audit_log' AND con.contype = 'c'; IF cn IS NOT NULL THEN EXECUTE 'ALTER TABLE audit_log DROP CONSTRAINT ' || cn; END IF; EXECUTE 'ALTER TABLE audit_log ADD CONSTRAINT audit_log_action_check CHECK (action IN (''CREATE'',''UPDATE'',''DELETE'',''RESTORE''))'; END; $$; `).catch(() => {}); console.log('[db] Schema ensured'); } module.exports = { ensureSchema };