refactor: шаг 6 — GET / и POST /add /edit /delete вынесены в src/routes/entries.js

This commit is contained in:
2026-05-30 13:40:50 +03:00
parent 55d26c1ece
commit 87a841fa56
2 changed files with 227 additions and 142 deletions
+9 -142
View File
@@ -66,148 +66,15 @@ async function start() {
// ── Все маршруты ниже требуют валидного JWT (проверяет auth.middleware) ───────
app.use(auth.middleware);
// ── Главная страница: список белых адресов ────────────────────────────────────
app.get('/', async (req, res) => {
const { clientId, companyName, isAdmin } = req.user;
try {
if (isAdmin) {
// Администратор видит все компании с фильтром по выбранной.
// ?company=<id> — числовой PK из таблицы companies.
const companies = await q.getAllCompanies();
const selectedId = req.query.company
? parseInt(req.query.company, 10)
: (companies[0] ? companies[0].id : null);
const selectedCompany = companies.find(c => c.id === selectedId) || companies[0] || null;
const entries = selectedCompany ? await q.listEntries(selectedCompany.id) : [];
const limit = selectedCompany ? await q.getLimit(selectedCompany) : 0;
return res.render('index', {
entries, limit, used: entries.length,
user: req.user, isAdmin: true,
companies, selectedCompany,
error: req.query.error || null,
message: req.query.message || null,
wasNormalized: req.query.wasNormalized === '1',
// CSRF-токен для форм (add, edit, delete) на странице
csrfToken: generateCsrfToken(req, res),
});
}
// Обычный пользователь: только его компания
const company = await q.getOrCreateCompany(clientId, companyName);
const limit = await q.getLimit(company);
const entries = await q.listEntries(company.id);
res.render('index', {
entries, limit, used: entries.length,
user: req.user, isAdmin: false,
companies: null, selectedCompany: company,
error: req.query.error || null,
message: req.query.message || null,
wasNormalized: req.query.wasNormalized === '1',
// CSRF-токен для форм (add, edit, delete)
csrfToken: generateCsrfToken(req, res),
});
} catch (e) {
console.error('GET / error:', e);
res.render('index', {
entries: [], limit: 15, used: 0,
user: req.user, isAdmin,
companies: null, selectedCompany: null,
error: 'Ошибка загрузки данных: ' + e.message,
message: null, wasNormalized: false,
csrfToken: generateCsrfToken(req, res),
});
}
});
// ── Добавление записи ─────────────────────────────────────────────────────────
// mutationLimiter: не более 30 POST-запросов/мин с одного IP.
// doubleCsrfProtection: проверяет CSRF-токен из скрытого поля формы.
app.post('/add', mutationLimiter, doubleCsrfProtection, async (req, res) => {
const { value, comment } = req.body;
const { clientId, companyName, email, isAdmin } = req.user;
let company;
try {
if (isAdmin && req.body.company_id) {
// Администратор добавляет в выбранную компанию (скрытое поле company_id)
const id = parseInt(req.body.company_id, 10);
if (!Number.isFinite(id) || id <= 0) throw new Error('Некорректный company_id');
company = await q.getCompanyById(id);
if (!company) throw new Error('Компания не найдена');
} else {
// Пользователь: только своя компания (из токена)
company = await q.getOrCreateCompany(clientId, companyName);
}
const result = await q.createEntry(company.id, value, comment, email);
const msg = result.wasNormalized
? `Адрес нормализован в ${result.entry.value_cidr}`
: 'Добавлено';
res.redirect(backUrl(isAdmin, company.id, {
message: msg,
wasNormalized: result.wasNormalized ? '1' : '0',
}));
} catch (e) {
// При ошибке возвращаемся к тому же виду что был
const cid = company ? company.id : req.body.company_id;
res.redirect(backUrl(isAdmin, cid, { error: e.message }));
}
});
// ── Редактирование записи ─────────────────────────────────────────────────────
app.post('/edit/:id', mutationLimiter, doubleCsrfProtection, async (req, res) => {
const { value, comment } = req.body;
const { clientId, companyName, email, isAdmin } = req.user;
const entryId = parseInt(req.params.id, 10);
let company;
try {
if (isAdmin && req.body.company_id) {
const id = parseInt(req.body.company_id, 10);
if (!Number.isFinite(id) || id <= 0) throw new Error('Некорректный company_id');
company = await q.getCompanyById(id);
if (!company) throw new Error('Компания не найдена');
} else {
company = await q.getOrCreateCompany(clientId, companyName);
}
const result = await q.updateEntry(entryId, company.id, value, comment, email);
const msg = result.wasNormalized
? `Адрес нормализован в ${result.entry.value_cidr}`
: 'Сохранено';
res.redirect(backUrl(isAdmin, company.id, {
message: msg,
wasNormalized: result.wasNormalized ? '1' : '0',
}));
} catch (e) {
const cid = company ? company.id : req.body.company_id;
res.redirect(backUrl(isAdmin, cid, { error: e.message }));
}
});
// ── Удаление записи (soft delete) ────────────────────────────────────────────
app.post('/delete/:id', mutationLimiter, doubleCsrfProtection, async (req, res) => {
const { clientId, companyName, email, isAdmin } = req.user;
const entryId = parseInt(req.params.id, 10);
let company;
try {
if (isAdmin && req.body.company_id) {
const id = parseInt(req.body.company_id, 10);
if (!Number.isFinite(id) || id <= 0) throw new Error('Некорректный company_id');
company = await q.getCompanyById(id);
if (!company) throw new Error('Компания не найдена');
} else {
company = await q.getOrCreateCompany(clientId, companyName);
}
await q.deleteEntry(entryId, company.id, email);
res.redirect(backUrl(isAdmin, company.id, { message: 'Запись удалена' }));
} catch (e) {
const cid = company ? company.id : req.body.company_id;
res.redirect(backUrl(isAdmin, cid, { error: e.message }));
}
});
// GET /, POST /add, POST /edit/:id, POST /delete/:id.
// src/routes/entries.js содержит логику и комментарии.
app.use(require('./src/routes/entries').createRouter({
q,
doubleCsrfProtection,
mutationLimiter,
generateCsrfToken,
backUrl,
}));
// ── Журнал аудита (только admin) ──────────────────────────────────────────────
// ТЗ 4.6: «журнал доступен для просмотра только администратору»