getpgdata: JSON API POST /api/sql для произвольных SQL-команд (WRITE_SQL=1), версия 1.0.3
This commit is contained in:
@@ -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: 'Страница не найдена' });
|
||||
|
||||
Reference in New Issue
Block a user