import type { FastifyPluginAsync } from 'fastify' import { repos } from '@evofw/db' import { createPolicySetBodySchema, patchPolicySetBodySchema, agentIdsBodySchema, } from '@evofw/shared' import { AppError } from '../plugins/error-handler.js' import type { AppConfig } from '../config.js' import { auditMutation } from '../services/audit.js' import { mapPolicySet, mapPolicySets } from '../services/row-mappers.js' export const policySetsRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( app, opts, ) => { const { config } = opts app.get('/policy-sets', async () => ({ items: mapPolicySets(app.db, repos.listPolicySets(app.db)), })) app.get<{ Params: { id: string } }>('/policy-sets/:id', async (req) => { const s = repos.getPolicySet(app.db, req.params.id) if (!s) throw new AppError('NOT_FOUND', 'Policy set not found', 404) return { ...mapPolicySet(s, app.db), agent_ids: repos.listAgentIdsForSet(app.db, s.id), } }) app.post('/policy-sets', async (req) => { const body = createPolicySetBodySchema.parse(req.body) const row = repos.insertPolicySet(app.db, { id: crypto.randomUUID(), name: body.name.trim(), description: body.description ?? null, enabled: body.enabled === false ? 0 : 1, policyMode: 'blacklist', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) auditMutation(app, config, req, { action: 'policy_set.create', targetType: 'app_resource', targetId: row!.id, summary: `Создан набор политик: ${row!.name}`, details: { set_id: row!.id }, }) return mapPolicySet(row!, app.db) }) app.patch<{ Params: { id: string } }>('/policy-sets/:id', async (req) => { const body = patchPolicySetBodySchema.parse(req.body) const s = repos.getPolicySet(app.db, req.params.id) if (!s) throw new AppError('NOT_FOUND', 'Policy set not found', 404) const updated = repos.updatePolicySet(app.db, s.id, { name: body.name?.trim(), description: body.description, enabled: body.enabled === undefined ? undefined : body.enabled ? 1 : 0, }) if (body.enabled !== undefined) { repos.bumpAgentsForSet(app.db, s.id) } auditMutation(app, config, req, { action: 'policy_set.update', targetType: 'app_resource', targetId: s.id, summary: `Обновлён набор политик: ${updated!.name}`, details: { set_id: s.id, enabled: body.enabled, name: body.name, }, }) return mapPolicySet(updated!, app.db) }) app.delete<{ Params: { id: string } }>('/policy-sets/:id', async (req) => { const s = repos.getPolicySet(app.db, req.params.id) try { const agentIds = repos.listAgentIdsForSet(app.db, req.params.id) repos.deletePolicySet(app.db, req.params.id) for (const id of agentIds) repos.bumpAgentGeneration(app.db, id) if (s) { auditMutation(app, config, req, { action: 'policy_set.delete', severity: 'warning', targetType: 'app_resource', targetId: s.id, summary: `Набор политик удалён: ${s.name}`, details: { set_id: s.id, agents_affected: agentIds.length }, }) } } catch (err) { throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } return { ok: true } }) /** * Replace which agents have this set assigned: listed agents gain the set * (other assignments preserved), unlisted agents lose it. */ app.put<{ Params: { id: string } }>( '/policy-sets/:id/agents', async (req) => { const set = repos.getPolicySet(app.db, req.params.id) if (!set) throw new AppError('NOT_FOUND', 'Policy set not found', 404) const body = agentIdsBodySchema.parse(req.body) const target = new Set(body.agent_ids) for (const agentId of body.agent_ids) { if (!repos.getAgent(app.db, agentId)) { throw new AppError('NOT_FOUND', `Agent not found: ${agentId}`, 404) } } const current = repos.listAgentIdsForSet(app.db, set.id) const toAdd = body.agent_ids.filter((id) => !current.includes(id)) const toRemove = current.filter((id) => !target.has(id)) const applyAssignment = (agentId: string, withSet: boolean) => { const others = repos .listSetsForAgent(app.db, agentId) .map((s) => s.setId) .filter((id) => id !== set.id) const next = withSet ? [...others, set.id] : others repos.setAgentPolicySets(app.db, agentId, next) } app.sqlite.transaction(() => { for (const agentId of toAdd) applyAssignment(agentId, true) for (const agentId of toRemove) applyAssignment(agentId, false) })() auditMutation(app, config, req, { action: 'policy_set.agents.update', targetType: 'app_resource', targetId: set.id, summary: `Назначение набора ${set.name} обновлено`, details: { set_id: set.id, added: toAdd, removed: toRemove, }, }) return { agent_ids: repos.listAgentIdsForSet(app.db, set.id), added: toAdd, removed: toRemove, } }, ) }