feat(api, web): enhance agent management and linting capabilities
- Added a new linting command for OpenAPI specifications in the package.json, improving code quality checks. - Updated frontend documentation to clarify component usage and structure, including detailed descriptions for `SettingsShell` and `Auth callback`. - Refactored agent-related API routes to streamline control-plane functionalities, consolidating multiple routes for better organization. - Improved error handling in the API to provide more informative responses for validation errors, enhancing user feedback during interactions. These changes enhance the overall development experience and improve the management of agents within the application.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
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 } from '../services/row-mappers.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: repos
|
||||
.listPolicyRules(app.db, s.id)
|
||||
.map((r) => mapPolicyRule(r, app.db)),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
app.get<{ Querystring: { set_id?: string; agent_id?: string } }>(
|
||||
'/rules',
|
||||
async (req) => {
|
||||
if (req.query.agent_id) {
|
||||
return {
|
||||
items: repos
|
||||
.listPolicyRulesForAgent(app.db, req.query.agent_id)
|
||||
.map((r) => mapPolicyRule(r, app.db)),
|
||||
}
|
||||
}
|
||||
const items = repos
|
||||
.listPolicyRules(app.db, req.query.set_id)
|
||||
.map((r) => mapPolicyRule(r, app.db))
|
||||
return { items }
|
||||
},
|
||||
)
|
||||
|
||||
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: repos
|
||||
.listPolicyRules(app.db, s.id)
|
||||
.map((r) => mapPolicyRule(r, 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 }
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user