import type { FastifyPluginAsync } from 'fastify' import { repos } from '@evofw/db' import { createIpListBodySchema, listEntriesBodySchema, deleteListEntryBodySchema, isManualListType, } from '@evofw/shared' import { AppError } from '../plugins/error-handler.js' import { refreshIpList } from '../services/lists/refresh.js' import { addListEntries, deleteListEntry, mapListDetail, } from '../services/lists/entries.js' import type { AppConfig } from '../config.js' import { auditMutation } from '../services/audit.js' import { maskListConfig, sealListConfig } from '../services/secret-cipher.js' import { applyPagination } from '../services/pagination.js' export const listsRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( app, opts, ) => { const { config } = opts app.get<{ Querystring: { limit?: string; offset?: string } }>( '/lists', async (req) => { const lists = repos.listIpLists(app.db) const counts = repos.countEntriesByListIds( app.db, lists.map((l) => l.id), ) const items = lists.map((l) => ({ id: l.id, name: l.name, type: l.type, config_json: maskListConfig(l.configJson), content_hash: l.contentHash, refreshed_at: l.refreshedAt, last_error: l.lastError, entry_count: counts.get(l.id) ?? 0, created_at: l.createdAt, updated_at: l.updatedAt, })) const paged = applyPagination(items, req.query) return { items: paged.items, total: paged.total } }, ) app.post('/lists', async (req) => { const body = createIpListBodySchema.parse(req.body) const type = body.type === 'domains' ? 'static' : body.type const id = crypto.randomUUID() const list = repos.insertIpList(app.db, { id, name: body.name, type, configJson: sealListConfig({ ...(body.config ?? {}) }), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) if (body.entries?.length && isManualListType(type)) { try { await addListEntries(app.db, id, { values: body.entries }) } catch (err) { repos.deleteIpList(app.db, id) throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } } else if (!isManualListType(type)) { await refreshIpList(app.db, id) } auditMutation(app, config, req, { action: 'list.create', targetType: 'app_resource', targetId: list!.id, summary: `Создан список: ${list!.name}`, details: { list_id: list!.id, type: list!.type }, }) return { id: list!.id, name: list!.name, type: list!.type, config_json: maskListConfig(list!.configJson), created_at: list!.createdAt, updated_at: list!.updatedAt, } }) app.get<{ Params: { id: string } }>('/lists/:id', async (req) => { const detail = mapListDetail(app.db, req.params.id) if (!detail) throw new AppError('NOT_FOUND', 'List not found', 404) return detail }) app.post<{ Params: { id: string } }>( '/lists/:id/entries', async (req) => { const l = repos.getIpList(app.db, req.params.id) if (!l) throw new AppError('NOT_FOUND', 'List not found', 404) const body = listEntriesBodySchema.parse(req.body) try { const result = await addListEntries(app.db, l.id, { values: body.values, items: body.items, }) auditMutation(app, config, req, { action: 'list.entries.add', targetType: 'app_resource', targetId: l.id, summary: `Добавлены записи в список: ${l.name}`, details: { list_id: l.id, entry_count: result.entries.length, }, }) return mapListDetail(app.db, l.id) ?? result } catch (err) { throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } }, ) app.delete<{ Params: { id: string } }>( '/lists/:id/entries', async (req) => { const l = repos.getIpList(app.db, req.params.id) if (!l) throw new AppError('NOT_FOUND', 'List not found', 404) const body = deleteListEntryBodySchema.parse(req.body) try { await deleteListEntry(app.db, l.id, body.value) auditMutation(app, config, req, { action: 'list.entries.delete', severity: 'warning', targetType: 'app_resource', targetId: l.id, summary: `Удалена запись из списка: ${l.name}`, details: { list_id: l.id, value: body.value }, }) return mapListDetail(app.db, l.id) } catch (err) { throw new AppError( 'VALIDATION_ERROR', err instanceof Error ? err.message : String(err), 400, ) } }, ) app.post<{ Params: { id: string } }>('/lists/:id/refresh', async (req) => { const l = repos.getIpList(app.db, req.params.id) await refreshIpList(app.db, req.params.id) const detail = mapListDetail(app.db, req.params.id) if (!detail) throw new AppError('NOT_FOUND', 'List not found', 404) auditMutation(app, config, req, { action: 'list.refresh', targetType: 'app_resource', targetId: req.params.id, summary: `Обновлён список: ${l?.name ?? req.params.id}`, details: { list_id: req.params.id }, }) return detail }) app.delete<{ Params: { id: string } }>('/lists/:id', async (req) => { const l = repos.getIpList(app.db, req.params.id) repos.deleteIpList(app.db, req.params.id) if (l) { auditMutation(app, config, req, { action: 'list.delete', severity: 'warning', targetType: 'app_resource', targetId: l.id, summary: `Список удалён: ${l.name}`, details: { list_id: l.id }, }) } return { ok: true } }) }