Files
ipwhitelist-app/v1_legacy/src/api/routes/entries.js
T

173 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const express = require('express');
const { aggregateCIDRs } = require('../../validators');
function createEntriesRouter({ q }) {
const router = express.Router();
const json = express.json({ limit: '32kb' });
// Маппинг сообщений ошибок на HTTP-коды
function handleQueryError(e, res) {
const msg = e.message || '';
// 409: бизнес-конфликты (дубликат, пересечение, лимит)
if (
msg.includes('Лимит исчерпан') ||
msg.includes('Такой адрес уже существует') ||
msg.includes('Пересечение с существующей записью')
) {
return res.status(409).json({ error: msg });
}
// 404: запись не найдена
if (msg.includes('Запись не найдена')) {
return res.status(404).json({ error: msg });
}
// 400: ошибки валидации от validate() и blocked-ranges
if (
msg.includes('Некорректн') ||
msg.includes('Маска должна быть') ||
msg.includes('Пустое значение') ||
msg.includes('IPv6 не поддерживается') ||
msg.includes('пересекается с запрещённым')
) {
return res.status(400).json({ error: msg });
}
return res.status(500).json({ error: msg });
}
// Определить компанию из запроса.
// Admin с ?company=<id> → компания по ID.
// User с ?client_id=<id> (мульти-компания) → компания по clientId.
// Иначе → компания пользователя (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;
}
// Мульти-компания: client_id разрешён только если есть в профилях (IAM) или allClientIds (JWT fallback)
const requestedId = (req.query.client_id || '').trim();
const allowedIds = req.user.profiles && req.user.profiles.length
? req.user.profiles.map(p => p.client_id)
: (req.user.allClientIds || []);
// Без верифицированного списка — client_id не принимаем
const effectiveClientId = (requestedId && allowedIds.length && allowedIds.includes(requestedId))
? requestedId
: req.user.clientId;
// companyName: для переключённой компании — находим в profiles или используем clientId
let effectiveName = effectiveClientId === req.user.clientId
? req.user.companyName
: effectiveClientId;
if (req.user.profiles) {
const p = req.user.profiles.find(p => p.client_id === effectiveClientId);
if (p) effectiveName = p.company_name;
}
return q.getOrCreateCompany(effectiveClientId, effectiveName, req.user.email);
}
// GET /api/v1/entries — список записей + лимит
// ?includeDeleted=true — показать soft-deleted (только admin)
router.get('/', async (req, res) => {
try {
const company = await resolveCompany(req);
const includeDeleted = req.user.isAdmin && req.query.includeDeleted === 'true';
const [entries, limit] = await Promise.all([
q.listEntries(company.id, includeDeleted),
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
// По умолчанию JSON. ?format=txt → text/plain.
// Только для админа.
router.get('/export', async (req, res) => {
try {
let companyId = null;
if (!req.user.isAdmin) {
const requestedId = (req.query.client_id || '').trim();
const effectiveClientId = (requestedId && req.user.allClientIds && req.user.allClientIds.includes(requestedId))
? requestedId
: req.user.clientId;
const company = await q.getOrCreateCompany(effectiveClientId, effectiveClientId);
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);
const isText = req.query.format === 'txt';
if (!isText) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const fname = (req.query.filename || 'white-list.json').replace(/[^\w\-_. ]/g, '_');
const disp = req.query.view === '1' ? 'inline' : 'attachment';
res.setHeader('Content-Disposition', `${disp}; filename="${fname}"`);
return res.json({ cidrs: aggregated, count: aggregated.length });
}
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_. ]/g, '_');
const disp = req.query.view === '1' ? 'inline' : 'attachment';
res.setHeader('Content-Disposition', `${disp}; filename="${fname}"`);
res.send(aggregated.join('\n') + (aggregated.length ? '\n' : ''));
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// POST /api/v1/entries — создать запись
router.post('/', json, async (req, res) => {
const { value, comment } = req.body || {};
if (!value) return res.status(400).json({ error: 'value required' });
if (comment && comment.length > 255) return res.status(400).json({ error: 'Комментарий слишком длинный (максимум 255 символов)' });
try {
const company = await resolveCompany(req);
const { entry, wasNormalized } = await q.createEntry(company.id, value, comment || '', req.user.email, req.user.originalUserEmail);
res.status(201).json({ entry, wasNormalized });
} catch (e) {
if (e.status) return res.status(e.status).json({ error: e.message });
handleQueryError(e, res);
}
});
// PATCH /api/v1/entries/:id — обновить запись
router.patch('/:id', json, async (req, res) => {
const { value, comment } = req.body || {};
if (!value) return res.status(400).json({ error: 'value required' });
if (comment && comment.length > 255) return res.status(400).json({ error: 'Комментарий слишком длинный (максимум 255 символов)' });
try {
const company = await resolveCompany(req);
const { entry, wasNormalized } = await q.updateEntry(req.params.id, company.id, value, comment ?? '', req.user.email, req.user.originalUserEmail);
res.json({ entry, wasNormalized });
} catch (e) {
if (e.status) return res.status(e.status).json({ error: e.message });
handleQueryError(e, res);
}
});
// DELETE /api/v1/entries/:id — удалить запись
router.delete('/:id', async (req, res) => {
try {
const company = await resolveCompany(req);
await q.deleteEntry(req.params.id, company.id, req.user.email, req.user.originalUserEmail);
res.status(204).send();
} catch (e) {
if (e.status) return res.status(e.status).json({ error: e.message });
handleQueryError(e, res);
}
});
return router;
}
module.exports = { createEntriesRouter };