'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. // User с ?client_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=. User: только своя компания или ?client_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 };