- retention: ежедневный cron (03:17) + запуск на старте удаляет сырые
agent_stats_samples старше STATS_RETENTION_DAYS (по умолчанию 30);
lifetime-итоги на агенте и агрегаты сохраняются
- N+1 на поллинг-пути агента: evaluateAgentPolicy теперь делает по одному
батч-запросу на записи списков / резолвы hostname / имена списков вместо
запроса на каждое правило; mapPolicyRules и port-rules GET — аналогично
- пагинация: GET /agents, /lists, /rules, /install-links принимают
limit/offset и возвращают total; без параметров — прежнее поведение
- openapi.yaml: approve-bulk, PUT /policy-sets/{id}/agents, параметры
пагинации (redocly lint OK); тесты (47 passed)
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import type { FastifyPluginAsync } from 'fastify'
|
|
import { repos } from '@evofw/db'
|
|
import { createInstallLinkBodySchema } from '@evofw/shared'
|
|
import { AppError } from '../plugins/error-handler.js'
|
|
import {
|
|
mapInstallLink,
|
|
randomToken,
|
|
} from '../services/install-links.js'
|
|
import { hashToken } from '../plugins/auth.js'
|
|
import type { AppConfig } from '../config.js'
|
|
import { auditMutation } from '../services/audit.js'
|
|
import { applyPagination } from '../services/pagination.js'
|
|
|
|
export const installLinksRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
|
|
app,
|
|
opts,
|
|
) => {
|
|
const { config } = opts
|
|
|
|
app.get<{ Querystring: { limit?: string; offset?: string } }>(
|
|
'/install-links',
|
|
async (req) => {
|
|
const items = repos
|
|
.listInstallLinks(app.db)
|
|
.map((row) => mapInstallLink(row, config.publicBaseUrl))
|
|
const paged = applyPagination(items, req.query)
|
|
return { items: paged.items, total: paged.total }
|
|
},
|
|
)
|
|
|
|
app.post('/install-links', async (req, reply) => {
|
|
const body = createInstallLinkBodySchema.parse(req.body)
|
|
const linkId = randomToken(10)
|
|
const slug = randomToken(16)
|
|
if (
|
|
repos.getInstallLink(app.db, linkId) ||
|
|
repos.getInstallLinkBySlug(app.db, slug)
|
|
) {
|
|
throw new AppError('CONFLICT', 'Retry create (id collision)', 409)
|
|
}
|
|
|
|
const agentId = crypto.randomUUID()
|
|
const inviteToken = `invite:${agentId}`
|
|
const now = new Date().toISOString()
|
|
const name = body.name.trim()
|
|
const platform = body.platform ?? 'linux'
|
|
|
|
app.sqlite.transaction(() => {
|
|
repos.insertAgent(app.db, {
|
|
id: agentId,
|
|
name,
|
|
hostname: null,
|
|
platform,
|
|
tokenPrefix: inviteToken.slice(0, 12),
|
|
tokenHash: hashToken(inviteToken),
|
|
status: 'invited',
|
|
defaultAction: 'accept',
|
|
policyGeneration: 1,
|
|
clientVersion: null,
|
|
settingsJson: '{}',
|
|
createdAt: now,
|
|
})
|
|
repos.insertInstallLink(app.db, {
|
|
id: linkId,
|
|
slug,
|
|
clientName: name,
|
|
platform,
|
|
agentId,
|
|
createdAt: now,
|
|
useCount: 0,
|
|
})
|
|
})()
|
|
|
|
const row = repos.getInstallLink(app.db, linkId)!
|
|
auditMutation(app, config, req, {
|
|
action: 'agent.create',
|
|
targetType: 'app_resource',
|
|
targetId: agentId,
|
|
summary: `Создан агент (invite): ${name}`,
|
|
details: { agent_id: agentId, platform, install_link_id: linkId },
|
|
})
|
|
return reply.code(201).send(mapInstallLink(row, config.publicBaseUrl))
|
|
})
|
|
|
|
app.delete<{ Params: { id: string } }>(
|
|
'/install-links/:id',
|
|
async (req) => {
|
|
const row = repos.getInstallLink(app.db, req.params.id)
|
|
if (!row) throw new AppError('NOT_FOUND', 'Install link not found', 404)
|
|
const updated = repos.revokeInstallLink(app.db, row.id)
|
|
return mapInstallLink(updated!, config.publicBaseUrl)
|
|
},
|
|
)
|
|
}
|