90 lines
3.8 KiB
JavaScript
90 lines
3.8 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// 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 };
|