v2: EJS-шаблоны — user, admin, admin-audit, admin-entries

This commit is contained in:
2026-06-13 08:46:09 +04:00
parent 30aaf8a7e4
commit cfaf8f34c3
8 changed files with 216 additions and 254 deletions
+20 -96
View File
@@ -1,41 +1,24 @@
// ═══════════════════════════════════════════════════════════════════════════════
// V2 — Фронтенд обычного пользователя
// V2 — Фронтенд пользователя (Express + EJS)
//
// ЭТО: Express-роутер, который рендерит HTML и принимает POST-формы.
// НЕ: работа с БД (только через crud/), валидация (в crud/).
// ЭТО: Express-роутер, рендерит EJS-шаблоны.
// НЕ: работа с БД (только через crud/).
//
// ЗАЧЕМ:
// 1. Единственное место где генерится HTML для юзера.
// 2. Все действия юзера (add/edit/delete) — через POST-формы.
// 3. Выбор компании — всегда выпадающий список (даже если одна компания).
// 4. Сообщения об ошибках/успехе — через query-параметры (?msg=, ?error=).
//
// КОНТЕКСТ (из resolveContext middleware):
// req.clientId — W-номер текущей компании
// req.email — почта юзера
// req.impersonatedBy — null или почта админа
// req.profiles — список компаний для выпадающего списка
//
// МАРШРУТЫ:
// GET / — список записей + форма добавления
// POST /add — добавить CIDR (→ crud.add → 302 с ?msg=)
// POST /edit/:id — изменить запись
// POST /delete/:id — удалить запись (soft delete)
// ШАБЛОН: views/v2/user.ejs — данные: entries, limit, used, companies,
// activeClientId, user, includeDeleted, version.
// ═══════════════════════════════════════════════════════════════════════════════
const express = require('express');
const crud = require('../crud'); // ← единственная зависимость от данных
const crud = require('../crud');
const config = require('../config');
function createUserRouter() {
const router = express.Router();
// ── GET / — главная страница ──────────────────────────────────────────
// ── GET / — список записей + форма ──────────────────────────────────
router.get('/', async (req, res) => {
try {
const clId = req.clientId;
// Список компаний для выпадающего списка
// Если profiles пуст — создаём fallback из clientId (одна компания)
const allCompanies = req.profiles.length > 0
? req.profiles
: [{ client_id: clId, company_name: req.companyName || clId }];
@@ -43,31 +26,37 @@ function createUserRouter() {
const includeDeleted = req.query.deleted === '1';
const { entries, used, limit } = await crud.list(clId, includeDeleted);
// Переключение компании: запоминаем в сессии и редиректим
// Переключение компании
if (req.query.switchTo && allCompanies.find(c => c.client_id === req.query.switchTo)) {
req.session.user.activeClientId = req.query.switchTo;
return res.redirect('/v2/app');
}
res.send(renderPage({ entries, limit, used, allCompanies, clId, user: req.user, includeDeleted }));
res.render('v2/user', {
entries, limit, used,
companies: allCompanies,
activeClientId: clId,
user: req.user || {},
includeDeleted,
version: config.version,
});
} catch (e) {
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
}
});
// ── POST /add — добавить запись ───────────────────────────────────────
// ── POST /add ───────────────────────────────────────────────────────
router.post('/add', async (req, res) => {
try {
const result = await crud.add(req.clientId, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
const msg = result.wasNormalized ? 'Добавлено (адрес нормализован)' : 'Добавлено';
res.redirect('/v2/app?msg=' + encodeURIComponent(msg));
} catch (e) {
// Ошибка валидации/лимита/дубликата — показываем юзеру
res.redirect('/v2/app?error=' + encodeURIComponent(e.message));
}
});
// ── POST /edit/:id — изменить запись ──────────────────────────────────
// ── POST /edit/:id ──────────────────────────────────────────────────
router.post('/edit/:id', async (req, res) => {
try {
const result = await crud.edit(parseInt(req.params.id), req.clientId, req.body.cidr || '', req.body.comment || '', req.email, req.impersonatedBy);
@@ -78,7 +67,7 @@ function createUserRouter() {
}
});
// ── POST /delete/:id — удалить запись (soft delete) ───────────────────
// ── POST /delete/:id ────────────────────────────────────────────────
router.post('/delete/:id', async (req, res) => {
try {
await crud.remove(parseInt(req.params.id), req.clientId, req.email, req.impersonatedBy);
@@ -91,69 +80,4 @@ function createUserRouter() {
return router;
}
// ── renderPage() → HTML ─────────────────────────────────────────────────────
// Временный inline HTML (будет заменён на EJS-шаблоны).
// Данные: entries, limit, used, allCompanies, clId, user, includeDeleted.
function renderPage({ entries, limit, used, allCompanies, clId, user, includeDeleted }) {
const companyOptions = allCompanies.map(c =>
`<option value="${c.client_id}" ${c.client_id === clId ? 'selected' : ''}>${c.company_name || c.client_id} (${c.client_id})</option>`
).join('');
const rows = entries.map(e => {
const del = e.deleted_at;
const style = del ? 'text-decoration:line-through;opacity:0.5' : '';
const actions = del ? '<span style="color:#888">удалено</span>' : `
<form method="POST" action="/v2/app/edit/${e.id}" style="display:inline">
<input name="cidr" value="${e.value_cidr}" size="18">
<input name="comment" value="${e.comment || ''}" size="20">
<button>изменить</button>
</form>
<form method="POST" action="/v2/app/delete/${e.id}" style="display:inline">
<button onclick="return confirm('Удалить?')">удалить</button>
</form>`;
return `<tr style="${style}"><td>${e.value_cidr}</td><td>${e.comment || ''}</td><td>${e.created_by}</td><td>${new Date(e.created_at).toLocaleString('ru')}</td><td>${actions}</td></tr>`;
}).join('');
return `<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>V2 IP WhiteList</title>
<style>
body{font-family:sans-serif;max-width:960px;margin:20px auto;color:#222}
table{width:100%;border-collapse:collapse;margin:10px 0}
th,td{border:1px solid #ccc;padding:6px;text-align:left}
th{background:#f5f5f5}
.bar{display:flex;justify-content:space-between;align-items:center;background:#eee;padding:10px;margin-bottom:10px}
button,input,select{padding:4px 8px}
.add-form{background:#f0f8ff;padding:10px;margin:10px 0}
</style></head><body>
<div class="bar">
<span><strong>V2 IP WhiteList</strong> <small>v${require('../config').version}</small></span>
<span>${user.email} ${user.isAdmin ? '| <a href="/v2/admin">ADMIN</a>' : ''}</span>
<span>
<form method="GET" action="/v2/app" style="display:inline">
<select name="switchTo" onchange="this.form.submit()">${companyOptions}</select>
</form>
<a href="/v2/logout">Выйти</a>
</span>
</div>
<script>
const p=new URLSearchParams(location.search);
if(p.get('msg')){document.body.insertAdjacentHTML('afterbegin','<div style="background:#4caf50;color:#fff;padding:8px">'+p.get('msg')+'</div>');history.replaceState(null,'','/v2/app')}
if(p.get('error')){document.body.insertAdjacentHTML('afterbegin','<div style="background:#f44336;color:#fff;padding:8px">'+p.get('error')+'</div>');history.replaceState(null,'','/v2/app')}
</script>
<p>Записей: <strong>${used}</strong> из ${limit}</p>
<div class="add-form">
<form method="POST" action="/v2/app/add">
<input name="cidr" placeholder="x.x.x.x/xx" size="18" required>
<input name="comment" placeholder="комментарий" size="30">
<button>Добавить</button>
</form>
</div>
<table>
<tr><th>CIDR</th><th>Комментарий</th><th>Кто</th><th>Когда</th><th></th></tr>
${rows || '<tr><td colspan="5">Нет записей</td></tr>'}
</table>
<p><a href="/v2/app?deleted=${includeDeleted ? '0' : '1'}">${includeDeleted ? 'Скрыть удалённые' : 'Показать удалённые'}</a></p>
</body></html>`;
}
module.exports = { createUserRouter };