Files
iistwin/migrations/0088_external_db_connections.sql
Ильяс Султанов 59bd802bf7 Песочница отчётов для ИИ: recharts в custom pages + внешние БД с read-only шлюзом
- JsComponentRenderer: ctx.libs.charts (recharts) — графики в кастомных страницах
  и JS-вкладках без redeploy; справка get_js_coding_reference обновлена
- Внешние БД (миграция 0088): вкладка «Настройки → Внешние БД» — реестр подключений
  (пароль AES-256-GCM, env EXTERNAL_DB_SECRET, по API не отдаётся), тест подключения,
  allowlist таблиц на подключение
- Read-only шлюз POST /api/external-db/:id/query (finance.manage): только SELECT,
  без мульти-выражений, обёртка с LIMIT 5000, statement_timeout 10с, проверка таблиц
  по allowlist, аудит в external_db_query_log (кто/SQL/строки/мс)
- Назначение: ИИ-агенты строят отчёты и виджеты поверх витрин (напр. НДС 1С) без
  новых серверных эндпоинтов и пересборки
2026-09-29 22:50:40 +03:00

52 lines
2.5 KiB
SQL

-- Внешние подключения к БД (read-only шлюз для отчётов/виджетов, в т.ч. ИИ-агентами).
-- Пароль хранится в AES-256-GCM (server/routes/external-db.routes.ts), по API не отдаётся.
-- allowed_tables — jsonb-массив имён таблиц (lowercase), доступных через шлюз;
-- пустой массив = шлюз закрыт (админ обязан явно указать таблицы).
CREATE TABLE IF NOT EXISTS external_db_connections (
id SERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 5432,
database TEXT NOT NULL,
username TEXT NOT NULL,
password_encrypted TEXT NOT NULL,
ssl BOOLEAN NOT NULL DEFAULT false,
allowed_tables JSONB NOT NULL DEFAULT '[]',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS external_db_connections_org_idx ON external_db_connections (organization_id);
-- Журнал запросов шлюза: кто, какой SQL, сколько строк, сколько мс
CREATE TABLE IF NOT EXISTS external_db_query_log (
id BIGSERIAL PRIMARY KEY,
organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
connection_id INTEGER NOT NULL REFERENCES external_db_connections(id) ON DELETE CASCADE,
user_id INTEGER,
bot_id INTEGER,
sql_text TEXT NOT NULL,
row_count INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER NOT NULL DEFAULT 0,
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS external_db_query_log_org_idx ON external_db_query_log (organization_id, created_at DESC);
-- Tenant isolation
ALTER TABLE external_db_connections ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_iso ON external_db_connections;
CREATE POLICY tenant_iso ON external_db_connections USING (
current_setting('app.is_superadmin', true) = 'true'
OR organization_id = NULLIF(current_setting('app.current_org_id', true), '')::int
);
ALTER TABLE external_db_query_log ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_iso ON external_db_query_log;
CREATE POLICY tenant_iso ON external_db_query_log USING (
current_setting('app.is_superadmin', true) = 'true'
OR organization_id = NULLIF(current_setting('app.current_org_id', true), '')::int
);