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:
@@ -1,12 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const { aggregateCIDRs } = require('../../validators');
|
||||
|
||||
function createEntriesRouter({ q }) {
|
||||
const router = express.Router();
|
||||
const json = express.json({ limit: '32kb' });
|
||||
|
||||
// ── Хелпер: маппинг сообщений ошибок на HTTP-коды ───────────────────────
|
||||
// Маппинг сообщений ошибок на HTTP-коды
|
||||
function handleQueryError(e, res) {
|
||||
const msg = e.message || '';
|
||||
if (
|
||||
@@ -22,12 +23,54 @@ function createEntriesRouter({ q }) {
|
||||
return res.status(500).json({ error: msg });
|
||||
}
|
||||
|
||||
// GET /api/v1/entries — список записей текущей компании
|
||||
// Определить компанию из запроса.
|
||||
// Admin с ?company=<id> → компания по ID.
|
||||
// Иначе → компания пользователя (UPSERT).
|
||||
async function resolveCompany(req) {
|
||||
if (req.user.isAdmin && req.query.company) {
|
||||
const id = parseInt(req.query.company, 10);
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
throw Object.assign(new Error('Invalid company id'), { status: 400 });
|
||||
}
|
||||
const c = await q.getCompanyById(id);
|
||||
if (!c) throw Object.assign(new Error('Company not found'), { status: 404 });
|
||||
return c;
|
||||
}
|
||||
return q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
}
|
||||
|
||||
// GET /api/v1/entries — список записей + лимит
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
const entries = await q.listEntries(company.id);
|
||||
res.json({ entries });
|
||||
const company = await resolveCompany(req);
|
||||
const [entries, limit] = await Promise.all([
|
||||
q.listEntries(company.id),
|
||||
q.getLimit(company),
|
||||
]);
|
||||
res.json({ entries, limit, used: entries.length });
|
||||
} catch (e) {
|
||||
res.status(e.status || 500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/entries/export — агрегированный список CIDR (text/plain)
|
||||
// Доступен всем аутентифицированным пользователям.
|
||||
// Admin: все компании или ?company=<id>. User: только своя компания.
|
||||
router.get('/export', async (req, res) => {
|
||||
try {
|
||||
let companyId = null;
|
||||
if (!req.user.isAdmin) {
|
||||
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
companyId = company.id;
|
||||
} else if (req.query.company) {
|
||||
const id = parseInt(req.query.company, 10);
|
||||
if (Number.isFinite(id) && id > 0) companyId = id;
|
||||
}
|
||||
const cidrs = await q.getExportCIDRs(companyId);
|
||||
const aggregated = aggregateCIDRs(cidrs);
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'inline; filename="whitelist.txt"');
|
||||
res.send(aggregated.join('\n') + (aggregated.length ? '\n' : ''));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
@@ -38,10 +81,11 @@ function createEntriesRouter({ q }) {
|
||||
const { value, comment } = req.body || {};
|
||||
if (!value) return res.status(400).json({ error: 'value required' });
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
const company = await resolveCompany(req);
|
||||
const { entry, wasNormalized } = await q.createEntry(company.id, value, comment || '', req.user.email);
|
||||
res.status(201).json({ entry, wasNormalized });
|
||||
} catch (e) {
|
||||
if (e.status) return res.status(e.status).json({ error: e.message });
|
||||
handleQueryError(e, res);
|
||||
}
|
||||
});
|
||||
@@ -51,10 +95,11 @@ function createEntriesRouter({ q }) {
|
||||
const { value, comment } = req.body || {};
|
||||
if (!value) return res.status(400).json({ error: 'value required' });
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
const company = await resolveCompany(req);
|
||||
const { entry, wasNormalized } = await q.updateEntry(req.params.id, company.id, value, comment ?? '', req.user.email);
|
||||
res.json({ entry, wasNormalized });
|
||||
} catch (e) {
|
||||
if (e.status) return res.status(e.status).json({ error: e.message });
|
||||
handleQueryError(e, res);
|
||||
}
|
||||
});
|
||||
@@ -62,10 +107,11 @@ function createEntriesRouter({ q }) {
|
||||
// DELETE /api/v1/entries/:id — удалить запись
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
const company = await resolveCompany(req);
|
||||
await q.deleteEntry(req.params.id, company.id, req.user.email);
|
||||
res.status(204).send();
|
||||
} catch (e) {
|
||||
if (e.status) return res.status(e.status).json({ error: e.message });
|
||||
handleQueryError(e, res);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user