diff --git a/README.md b/README.md index 122c7b4..a0dfd0c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,36 @@ Node.js приложение для доступа к PostgreSQL (managed k8s, i - `GET /` — состояние подключения и список таблиц (не системные схемы) - `GET /table?schema=public&name=` — просмотр содержимого таблицы с пагинацией - `POST /query` — SQL-консоль (read-only, только `SELECT`) +- `POST /api/sql` — JSON API для произвольных SQL-команд (см. ниже) - `GET /healthz` — liveness-проба для k8s +## JSON API: POST /api/sql + +Выполняет произвольный SQL против БД, на которую настроено приложение (текущие креды). +Удобен для переноса данных между БД: читать из прежней (текущие креды) → затем после +смены кредов на новую ПГ вставлять. + +**Запрос:** +```bash +curl -X POST https:///api/sql \ + -H 'Content-Type: application/json' \ + -d '{"sql":"SELECT * FROM whitelist_entries LIMIT 10"}' +``` + +**Тело:** `{ "sql": "...", "params": [...] }` — `params` подставляются в `$1..$n`. + +**Ответ:** +- `SELECT` → `{ "ok": true, "fields": [...], "rows": [...], "rowCount": n }` +- `INSERT/UPDATE/DELETE` → `{ "ok": true, "rowCount": n }` +- ошибка → HTTP 400/403, `{ "ok": false, "error": "..." }` + +**Ограничения и безопасность:** +- Выполняется **один statement** (первый до `;`) — защита от мульти-комманд (`; DROP ...`) +- `SELECT`/`WITH` — доступны всегда +- `INSERT`/`UPDATE`/`DELETE`/`DDL` — **только при `WRITE_SQL=1`** в env; + иначе возвращается HTTP 403 «Write-SQL отключён» +- Для включения записи в jsonEnv добавь `"WRITE_SQL": "1"` + ## Структура ``` diff --git a/history/2026-08-18.md b/history/2026-08-18.md index 2fe5a4d..7d9ca66 100644 --- a/history/2026-08-18.md +++ b/history/2026-08-18.md @@ -116,3 +116,28 @@ getpgdata/ **⚠️ Точная причина падения пода по-прежнему не подтверждена** (нет доступа к логам Nubes). Если под не поднимется и после этого — смотреть логи `nodejsk8s` Deployment в Nubes UI: replica не активна (CrashLoopBackOff, invalidImage, порт/health, зависший старт). + +## 7. JSON API POST /api/sql (перенос данных в новую ПГ) + +**Контекст:** нужно перенести белые списки (IP, кто создал, когда) из прежней БД в **новую** +PostgreSQL (тот же realm k8s). Доступ к новой БД — через Keycloak, автоматизировать нельзя. +Решение: код в репе `getpgdata` + передеплой с кредами новой ПГ. + +**Что добавлено (версия 1.0.3):** +- Эндпоинт **`POST /api/sql`** — JSON API для произвольных SQL-команд. + Body: `{ "sql": "...", "params": [...] }` (params → `$1..$n`). + - `SELECT`/`WITH` → `{ ok:true, fields, rows, rowCount }` (всегда доступен) + - `INSERT`/`UPDATE`/`DELETE`/DDL → `{ ok:true, rowCount }` **только при `WRITE_SQL=1`** + - иначе → HTTP 403 «Write-SQL отключён» +- **Защита:** выполняется только первый statement (split по `;`) — блокирует мульти-команды + вроде `SELECT 1; DROP TABLE ...`. +- Флаг в env: `WRITE_SQL=1` включает запись. + +**Логика переноса:** +1. Приложение подключено к прежней БД — читать данные через `/api/sql` (SELECT). +2. Сменить креды в jsonEnv на новую ПГ (+ `WRITE_SQL=1`). +3. Передеплой — теперь `/api/sql` выполняет запросы уже в новой БД; выполнить INSERT данных. + +**Проверено:** `node -c server.js` OK; локально: пустой SQL → 400; SELECT → идёт в БД; +`INSERT` без `WRITE_SQL` → 403 (до попытки подключения); `WRITE_SQL=1` + мульти-statement → +берётся только первый; процесс проверки остановлен. diff --git a/package.json b/package.json index 3f209dc..a527807 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "getpgdata", - "version": "1.0.2", + "version": "1.0.3", "description": "Node.js приложение для доступа к PostgreSQL (managed k8s, internal-only)", "main": "server.js", "scripts": { diff --git a/server.js b/server.js index ed1a518..ffd8465 100644 --- a/server.js +++ b/server.js @@ -21,6 +21,9 @@ app.set('views', path.join(__dirname, 'views')); const APP_ENV = process.env.APP_ENV || 'production'; const QUERY_MAX_ROWS = parseInt(process.env.QUERY_MAX_ROWS || '200', 10); +// Разрешить произвольный SQL (INSERT/UPDATE/DELETE/DDL) через POST /api/sql. +// По умолчанию — только SELECT (read-only). Для включения записи: WRITE_SQL=1. +const WRITE_SQL = (process.env.WRITE_SQL || '').toLowerCase() === '1'; // --------------------------------------------------------------------------- // Конфигурация подключения к PostgreSQL @@ -206,6 +209,54 @@ app.post('/query', async (req, res) => { res.render('query', { sql, columns, rows, error, elapsed, QUERY_MAX_ROWS }); }); +// --------------------------------------------------------------------------- +// JSON API для произвольных SQL-команд. +// +// POST /api/sql Body: { "sql": "...", "params": [...] } +// SELECT → { ok:true, fields:[...], rows:[...] } (всегда доступен) +// INSERT/UPDATE/DELETE/DDL → { ok:true, rowCount:n } +// (доступно только при WRITE_SQL=1) +// +// Ограничение: выполняется ОДИН statement (первый) — предотвращает +// многоступенчатые команды через API. Параметры — через $1..$n. +// --------------------------------------------------------------------------- +app.post('/api/sql', async (req, res) => { + const sql = (req.body && typeof req.body.sql === 'string' ? req.body.sql : '').trim(); + const params = Array.isArray(req.body && req.body.params) ? req.body.params : []; + + if (!sql) { + return res.status(400).json({ ok: false, error: 'Пустой SQL' }); + } + + // Берём первый statement (до ; и до разделителей) — защита от мульти-комманд + const first = sql.split(';')[0].trim(); + if (!first) { + return res.status(400).json({ ok: false, error: 'Пустой SQL' }); + } + + const isRead = /^(select|with|show|explain)\b/i.test(first); + if (!isRead) { + if (!WRITE_SQL) { + return res.status(403).json({ ok: false, error: 'Write-SQL отключён (нужен WRITE_SQL=1)' }); + } + } + + try { + const r = await pool.query(first, params); + if (r.fields && r.fields.length) { + return res.json({ + ok: true, + fields: r.fields.map((f) => f.name), + rows: r.rows.slice(0, WRITE_SQL ? 10000 : QUERY_MAX_ROWS), + rowCount: r.rowCount, + }); + } + return res.json({ ok: true, rowCount: r.rowCount == null ? 0 : r.rowCount }); + } catch (e) { + return res.status(400).json({ ok: false, error: e.message }); + } +}); + // Обработка 404 app.use((req, res) => { res.status(404).render('error', { code: 404, message: 'Страница не найдена' });