Export: убран filename из Content-Disposition для диалога сохранения

Версия: 0.1.13
Документация: db-query-endpoint.md
This commit is contained in:
2026-07-08 14:57:17 +04:00
parent d20bd85a7d
commit 4bdcb4c363
5 changed files with 81 additions and 5 deletions
+1
View File
@@ -6,6 +6,7 @@ __pycache__/
.DS_Store .DS_Store
.vscode/ .vscode/
.idea/ .idea/
*.har
# Креды, токены, конфиги # Креды, токены, конфиги
token*.* token*.*
+76
View File
@@ -0,0 +1,76 @@
# Endpoint SQL-запросов к БД (`/v2/db/query`)
> Создан: 2026-07-08
## Назначение
Дать DevOps-инженеру (одному человеку, заданному через `ADMIN_EMAIL`) возможность выполнять SQL-запросы к БД напрямую из браузера — без psql, без kubectl, без внешнего IP.
## Доступ
- Только один пользователь: `req.email === ADMIN_EMAIL`
- Обычный юзер сервиса (проходит SSO как все)
- Пример: `ADMIN_EMAIL=atazet@narod.ru`
## Адрес
```
GET /v2/db/query?sql=<часть запроса после SELECT>
```
## Как работает
1. Пользователь заходит в сервис через SSO (любой URL, например `/v2/app`)
2. Меняет в адресной строке путь на `/v2/db/query?sql=...`
3. Сервер:
- Проверяет сессию (resolveContext)
- Проверяет что `req.email === ADMIN_EMAIL`
- Добавляет префикс `SELECT` перед вводом пользователя
- Выполняет: `pool.query('SELECT ' + userInput)`
- Возвращает JSON
## Примеры
| Ввод в браузере | Что выполняется |
|---|---|
| `/v2/db/query?sql=* FROM _migrations` | `SELECT * FROM _migrations` |
| `/v2/db/query?sql=count(*) FROM companies` | `SELECT count(*) FROM companies` |
| `/v2/db/query?sql=* FROM whitelist_entries LIMIT 10` | `SELECT * FROM whitelist_entries LIMIT 10` |
| `/v2/db/query?sql=* FROM audit_log ORDER BY created_at DESC LIMIT 5` | `SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 5` |
## Безопасность
1. **SELECT префикс** — жёстко добавляется `SELECT` перед вводом пользователя. DROP/INSERT/UPDATE/DELETE не работают:
```
/v2/db/query?sql=DROP TABLE companies
→ SELECT DROP TABLE companies (ошибка SQL, не выполнится)
```
2. **Одиночные запросы** — драйвер `pg` не поддерживает множественные запросы в одном `query()`. Попытка `1; DROP TABLE` не сработает.
3. **Доступ по email** — только один человек, заданный в `ADMIN_EMAIL`.
4. **Rate limiter** — endpoint защищён лимитером (как mutationLimiter).
## Ответ
```json
{
"rows": [ ... ],
"rowCount": 5,
"fields": ["id", "filename", "applied_at"]
}
```
## Реализация
- Файл: `v2/src/db/query.js`
- Middleware: `resolveContext` + проверка `req.canQueryDB`
- Роутер монтируется в `v2/server.js` на `/db`
- Флаг `req.canQueryDB` выставляется в `v2/src/router/index.js` (applyContext)
## Ограничения
- Только SELECT (через префикс)
- Нет GUI — только JSON в браузере
- SQL в URL (логируется в access_log nginx/ingress — это нормально для devops-инструмента)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ipwhitelist", "name": "ipwhitelist",
"version": "0.1.12", "version": "0.1.13",
"description": "IP WhiteList microservice for cloud provider", "description": "IP WhiteList microservice for cloud provider",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -8,7 +8,7 @@
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
module.exports = { module.exports = {
version: '0.1.12', version: '0.1.13',
// ── Среда ───────────────────────────────────────────────────────────────── // ── Среда ─────────────────────────────────────────────────────────────────
// APP_ENV=test → тестовый стенд (жёлтый баннер в UI, можно включать тестовый API) // APP_ENV=test → тестовый стенд (жёлтый баннер в UI, можно включать тестовый API)
+2 -3
View File
@@ -45,11 +45,10 @@ function createExportRouter() {
return res.send(text); return res.send(text);
} }
// Скачивание (по умолчанию) // Скачивание (по умолчанию) — без filename чтобы браузер показал диалог сохранения
const text = aggregated.join('\n') + (aggregated.length ? '\n' : ''); const text = aggregated.join('\n') + (aggregated.length ? '\n' : '');
res.set('Content-Type', 'text/plain; charset=utf-8'); res.set('Content-Type', 'text/plain; charset=utf-8');
const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_. ]/g, '_'); res.set('Content-Disposition', 'attachment');
res.set('Content-Disposition', 'attachment; filename="' + fname + '"');
res.send(text); res.send(text);
} catch (e) { } catch (e) {
console.error('[v2:export] Error:', e); console.error('[v2:export] Error:', e);