feat: UI-слой через /api/v1/* (ui/, views/ui-login.ejs, views/error.ejs)

- ui/api-client.js: HTTP-клиент к /api/v1/* (token из session.token)
- ui/routes/auth.js: /login (mock-пресеты), /login-token (Bearer paste), /logout
- ui/routes/entries.js: /, /add, /edit/:id, /delete/:id через API
- ui/routes/admin.js: /admin, /admin/limit/:id, /audit через API
- ui/routes/export.js: /export → проксирует /api/v1/entries/export
- ui/index.js: createUiRouter() — requireToken + resolveUser middleware
- views/ui-login.ejs: страница входа (mock + token)
- views/error.ejs: шаблон для 403/404/500

API расширения:
- GET /api/v1/entries теперь возвращает limit и used
- GET /api/v1/entries?company=<id> (admin): записи другой компании
- POST/PATCH/DELETE /api/v1/entries?company=<id> (admin): мутации для другой компании
- GET /api/v1/entries/export: агрегированный CIDR (перенесён из монолита)
- GET /api/v1/audit?company=<id>: журнал (перенесён из монолита)

server.js: заменены src/routes/* на ui/createUiRouter(), убран CSRF
This commit is contained in:
2026-05-30 19:02:51 +03:00
parent 87216389e3
commit c85a438ba2
11 changed files with 699 additions and 81 deletions
+88
View File
@@ -0,0 +1,88 @@
'use strict';
/**
* ui/routes/admin.js — /admin, /audit (только для администратора).
* /export — доступен всем аутентифицированным (смотри ui/routes/export.js).
*
* Данные из:
* GET /api/v1/companies
* PATCH /api/v1/companies/:id/limit
* GET /api/v1/audit
*/
const { Router } = require('express');
const api = require('../api-client');
function requireAdmin(req, res, next) {
if (!req.user || !req.user.isAdmin) {
return res.status(403).render('error', {
status: 403,
message: 'Доступ запрещён — требуются права администратора.',
});
}
next();
}
function createRouter() {
const router = Router();
router.use(requireAdmin);
// GET /admin — список компаний с лимитами
router.get('/admin', async (req, res) => {
const token = api.token(req);
try {
const r = await api.get('/api/v1/companies', token);
const companies = r.data.companies || [];
res.render('admin', {
user: req.user,
companies,
error: req.query.error || null,
success: req.query.success || null,
csrfToken: '',
});
} catch (e) {
res.render('admin', {
user: req.user, companies: [],
error: 'Ошибка загрузки: ' + e.message, success: null, csrfToken: '',
});
}
});
// POST /admin/limit/:id — установить лимит компании
router.post('/admin/limit/:id', async (req, res) => {
const token = api.token(req);
const limit = parseInt(req.body.limit, 10);
try {
const r = await api.patch('/api/v1/companies/' + req.params.id + '/limit', token, { limit });
if (r.status === 200) return res.redirect('/admin?success=' + encodeURIComponent('Лимит обновлён'));
res.redirect('/admin?error=' + encodeURIComponent(r.data.error || 'Ошибка'));
} catch (e) {
res.redirect('/admin?error=' + encodeURIComponent(e.message));
}
});
// GET /audit — журнал действий
router.get('/audit', async (req, res) => {
const token = api.token(req);
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
try {
const r = await api.get('/api/v1/audit' + (companyId ? '?company=' + companyId : ''), token);
res.render('audit', {
user: req.user,
rows: r.data.rows || [],
companies: r.data.companies || [],
selectedCompanyId: companyId,
error: null,
});
} catch (e) {
res.render('audit', {
user: req.user, rows: [], companies: [],
selectedCompanyId: null, error: 'Ошибка загрузки: ' + e.message,
});
}
});
return router;
}
module.exports = { createRouter };