fix: три бага — CSRF в админке, пагинация аудита, скрыта кнопка выхода

1. Изменение лимитов (Баг 1): добавлен CSRF-токен в форму /v2/admin/limit
   - v2/src/admin/index.js: createAdminRouter принимает {generateCsrfToken},
     передаёт csrfToken в шаблон admin.ejs
   - views/v2/admin.ejs: скрытое поле <input name=_csrf> в форме лимита

2. Журнал операций (Баг 2): пагинация + фильтр по дате вместо хардкод LIMIT 500
   - v2/src/db/queries.js: getAudit({companyId,days,page,limit}) —
     COUNT(*) OVER() для пагинации, WHERE created_at > NOW() - INTERVAL
   - v2/src/admin/index.js: чтение days/page из query string, limit=100
   - views/v2/admin-audit.ejs: фильтр 30/90/365 дней, prev/next пагинация

3. Кнопка Выйти (Баг 3): скрыта, т.к. /v2/logout не разлогинивает из Keycloak
   - views/v2/user.ejs: кнопка закомментирована с пояснением

v0.1.8 → v0.1.9
This commit is contained in:
2026-07-01 18:26:06 +04:00
parent 2a37697afb
commit 8dc7940318
7 changed files with 91 additions and 16 deletions
+15 -5
View File
@@ -13,7 +13,7 @@ const express = require('express');
const q = require('../db/queries');
const config = require('../config');
function createAdminRouter() {
function createAdminRouter({ generateCsrfToken } = {}) {
const router = express.Router();
router.use((req, res, next) => {
@@ -30,19 +30,29 @@ function createAdminRouter() {
router.get('/', async (req, res) => {
try {
const companies = await q.getAllCompanies();
res.render('v2/admin', { companies, user: req.adminUser, version: config.version });
res.render('v2/admin', {
companies, user: req.adminUser, version: config.version,
csrfToken: generateCsrfToken ? generateCsrfToken(req, res) : '',
});
} catch (e) {
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
}
});
// ── GET /audit ──────────────────────────────────────────────────────
// ── GET /audit — журнал операций с пагинацией и фильтром по дате ───
router.get('/audit', async (req, res) => {
try {
const companyId = req.query.companyId ? parseInt(req.query.companyId) : null;
const audit = await q.getAudit(companyId);
const days = Math.min(Math.max(parseInt(req.query.days) || 365, 1), 365);
const page = Math.max(parseInt(req.query.page) || 1, 1);
const perPage = 100;
const result = await q.getAudit({ companyId, days, page, limit: perPage });
const company = companyId ? await q.getCompanyById(companyId) : null;
res.render('v2/admin-audit', { audit, company, version: config.version });
res.render('v2/admin-audit', {
audit: result.rows, total: result.total,
page, pages: Math.ceil(result.total / perPage) || 1,
days, company, version: config.version,
});
} catch (e) {
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre>');
}
+1 -1
View File
@@ -8,7 +8,7 @@
// ═══════════════════════════════════════════════════════════════════════════════
module.exports = {
version: '0.1.8',
version: '0.1.9',
// ── IAM ──────────────────────────────────────────────────────────────────
iamUrl: process.env.V2_IAM_URL || 'https://auth-api.ngcloud.ru/api/v1/auth/user',
+38 -7
View File
@@ -265,16 +265,47 @@ async function logAudit(userEmail, companyId, action, oldValue, newValue, entryI
);
}
async function getAudit(companyId = null) {
let sql = `
SELECT a.*, c.name AS company_name, c.client_id
/**
* Журнал операций с фильтром по дате и пагинацией.
* @param {Object} opts
* @param {number|null} opts.companyId — фильтр по компании (null = все)
* @param {number} opts.days — глубина в днях (по умолчанию 365)
* @param {number} opts.page — страница (с 1)
* @param {number} opts.limit — записей на странице
* @returns {{ rows: Array, total: number }}
*/
async function getAudit({ companyId = null, days = 365, page = 1, limit = 100 } = {}) {
const offset = (page - 1) * limit;
const params = [];
let where = '';
if (companyId) {
where += ' WHERE a.company_id = $' + (params.length + 1);
params.push(companyId);
}
const dayParam = '$' + (params.length + 1);
where += (where ? ' AND' : ' WHERE') + ' a.created_at > NOW() - (' + dayParam + ' || \' days\')::INTERVAL';
params.push(String(days));
const limitIdx = params.length + 1;
const offsetIdx = params.length + 2;
const sql = `
SELECT a.*, c.name AS company_name, c.client_id,
COUNT(*) OVER() AS total_count
FROM audit_log a
JOIN companies c ON c.id = a.company_id
${where}
ORDER BY a.created_at DESC
LIMIT $${limitIdx} OFFSET $${offsetIdx}
`;
const params = [];
if (companyId) { sql += ' WHERE a.company_id = $1'; params.push(companyId); }
sql += ' ORDER BY a.created_at DESC LIMIT 500';
return (await pool.query(sql, params)).rows;
params.push(limit, offset);
const result = await pool.query(sql, params);
return {
rows: result.rows,
total: result.rows.length > 0 ? parseInt(result.rows[0].total_count, 10) : 0,
};
}
module.exports = {