import type { FastifyPluginAsync } from 'fastify' import { repos } from '@evofw/db' import { createPolicyRuleBodySchema, patchPolicyRuleBodySchema, reorderPolicyRulesBodySchema, } from '@evofw/shared' import { AppError } from '../plugins/error-handler.js' import { resolveAndStoreHostnameRule, resolveHostnameToCidrs, } from '../services/policy/resolve-hostname.js' import type { AppConfig } from '../config.js' import { auditMutation } from '../services/audit.js' import { mapPolicyRule, mapPolicyRules } from '../services/row-mappers.js' import { applyPagination } from '../services/pagination.js' export const rulesRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( app, opts, ) => { const { config } = opts app.get<{ Params: { id: string } }>( '/policy-sets/:id/rules', async (req) => { const s = repos.getPolicySet(app.db, req.params.id) if (!s) throw new AppError('NOT_FOUND', 'Policy set not found', 404) return { items: mapPolicyRules(repos.listPolicyRules(app.db, s.id), app.db), } }, ) app.get<{ Querystring: { set_id?: string; agent_id?: string; limit?: string; offset?: string } }>('/rules', async (req) => { if (req.query.agent_id) { return { items: mapPolicyRules( repos.listPolicyRulesForAgent(app.db, req.query.agent_id), app.db, ), } } const paged = applyPagination( mapPolicyRules(repos.listPolicyRules(app.db, req.query.set_id), app.db), req.query, ) return { items: paged.items, total: paged.total } }) app.post('/rules', async (req) => { const body = createPolicyRuleBodySchema.parse(req.body) const set = repos.getPolicySet(app.db, body.set_id) if (!set) throw new AppError('NOT_FOUND', 'Policy set not found', 404) const hostname = body.hostname?.trim() || null const cidr = body.cidr?.trim() || null const listId = body.list_id?.trim() || null if (hostname) { try { await resolveHostnameToCidrs(hostname) } catch (err) { throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } } if (listId && !repos.getIpList(app.db, listId)) { throw new AppError('NOT_FOUND', 'IP list not found', 404) } const priority = body.priority ?? repos.nextRulePriority(app.db, body.set_id) const id = crypto.randomUUID() const row = repos.insertPolicyRule(app.db, { id, setId: body.set_id, priority, action: body.action, enabled: body.enabled === false ? 0 : 1, listId, cidr, hostname, comment: body.comment ?? null, createdByUserId: req.authUser?.id, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) if (hostname) { try { await resolveAndStoreHostnameRule(app.db, id, hostname) } catch (err) { repos.deletePolicyRule(app.db, id) throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } } repos.bumpAgentsForSet(app.db, body.set_id) auditMutation(app, config, req, { action: 'rule.create', targetType: 'app_resource', targetId: row!.id, summary: `Создано правило ${body.action} в наборе ${set.name}`, details: { rule_id: row!.id, set_id: body.set_id, action: body.action, priority, }, }) return mapPolicyRule(row!, app.db) }) app.patch<{ Params: { id: string } }>('/rules/:id', async (req) => { const body = patchPolicyRuleBodySchema.parse(req.body) const rule = repos.getPolicyRule(app.db, req.params.id) if (!rule) throw new AppError('NOT_FOUND', 'Rule not found', 404) const updated = repos.updatePolicyRule(app.db, rule.id, { enabled: body.enabled === undefined ? undefined : body.enabled ? 1 : 0, action: body.action, comment: body.comment, priority: body.priority, }) repos.bumpAgentsForSet(app.db, rule.setId) auditMutation(app, config, req, { action: 'rule.update', targetType: 'app_resource', targetId: rule.id, summary: `Обновлено правило ${rule.id}`, details: { rule_id: rule.id, set_id: rule.setId, enabled: body.enabled, action: body.action, priority: body.priority, }, }) return mapPolicyRule(updated!, app.db) }) app.put<{ Params: { id: string } }>( '/policy-sets/:id/rules/reorder', async (req) => { const body = reorderPolicyRulesBodySchema.parse(req.body) const s = repos.getPolicySet(app.db, req.params.id) if (!s) throw new AppError('NOT_FOUND', 'Policy set not found', 404) try { repos.reorderPolicyRules(app.db, s.id, body.ordered_ids) } catch (err) { throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } repos.bumpAgentsForSet(app.db, s.id) auditMutation(app, config, req, { action: 'rule.reorder', targetType: 'app_resource', targetId: s.id, summary: `Порядок правил изменён в наборе ${s.name}`, details: { set_id: s.id, ordered_ids: body.ordered_ids }, }) return { items: mapPolicyRules( repos.listPolicyRules(app.db, s.id), app.db, ), } }, ) app.delete<{ Params: { id: string } }>('/rules/:id', async (req) => { const rule = repos.getPolicyRule(app.db, req.params.id) if (!rule) throw new AppError('NOT_FOUND', 'Rule not found', 404) repos.deletePolicyRule(app.db, req.params.id) repos.bumpAgentsForSet(app.db, rule.setId) auditMutation(app, config, req, { action: 'rule.delete', severity: 'warning', targetType: 'app_resource', targetId: rule.id, summary: `Правило удалено из набора ${rule.setId}`, details: { rule_id: rule.id, set_id: rule.setId }, }) return { ok: true } }) }