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

155 lines
6.8 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 разрешён только если есть в allClientIds пользователя
const requestedId = (req.query.client_id || '').trim();
const effectiveClientId = (requestedId && req.user.allClientIds && req.user.allClientIds.includes(requestedId))
? requestedId
: req.user.clientId;
// companyName: для переключённой компании — clientId как имя по умолчанию
const effectiveName = effectiveClientId === req.user.clientId
? req.user.companyName
: effectiveClientId;
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 (text/plain)
// Доступен всем аутентифицированным пользователям.
// Admin: все компании или ?company=<id>. User: только своя компания или ?client_id=<id>.
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);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
const fname = req.query.filename || 'white-list.txt';
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);
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);
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);
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 };