Files
EvoFirewall/apps/api/src/routes/install-links.ts
T
Denozordec 7a3f1fad25 perf(api): ретенция статистики, устранение N+1 и пагинация списков
- 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)
2026-09-20 19:20:30 +07:00

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)
},
)
}