import type { FastifyInstance } from "fastify"; import * as serviceRepo from "../db/repositories/services.js"; import * as apiRepo from "../db/repositories/apis.js"; import * as logRepo from "../db/repositories/logs.js"; import { detectApis } from "../scanner/apiDetector.js"; import { startJob, updateJobProgress, finishJob, failJob, getJobStatus, isJobRunning } from "../scanJobs.js"; /** * API-Scanner: eigener, von den Geräte-/FritzBox-Scannern komplett * unabhängiger manueller Scan (siehe Scanner-Seite) - durchsucht bereits * bekannte Dienste nach üblichen API-Pfaden (siehe scanner/apiDetector.ts) * und speichert die Funde. Läuft NIE automatisch, nur auf Knopfdruck. * * Läuft als Hintergrund-Job im Backend (siehe scanJobs.ts), nicht mehr * innerhalb einer einzigen HTTP-Anfrage: bei vielen Diensten (jeweils ~40 * geprüfte Pfade) kann das insgesamt länger dauern, als ein * Reverse-Proxy/Browser auf eine Antwort wartet (führte zu HTTP 504). Der * POST-Aufruf startet den Job nur und antwortet sofort, der Fortschritt wird * über GET .../status abgefragt - das macht den laufenden Scan zudem * geräteübergreifend sichtbar (der Status lebt im Backend, nicht im Browser). */ export async function apiRoutes(app: FastifyInstance): Promise { app.get("/api/detected-apis", async () => { return apiRepo.listDetectedApis(); }); app.get("/api/scan/apis/status", async () => { return getJobStatus("apis"); }); app.post("/api/scan/apis", async (_request, reply) => { if (isJobRunning("apis")) { return reply.code(409).send({ error: "Es läuft bereits ein API-Scan." }); } const services = serviceRepo.listServices(); startJob("apis", services.length); // Bewusst NICHT awaited - der Job läuft im Hintergrund weiter, die // HTTP-Antwort kommt sofort zurück (siehe Doku oben). void (async () => { let servicesWithApi = 0; let totalFound = 0; const newFindings: { serviceId: string; serviceName: string; apis: apiRepo.DetectedApiEntry[] }[] = []; try { for (let i = 0; i < services.length; i++) { const service = services[i]; updateJobProgress("apis", i, service.displayName); const found = await detectApis(service.url); if (found.length > 0) { const { added } = apiRepo.replaceApisForService(service.id, found); servicesWithApi++; totalFound += found.length; if (added.length > 0) { newFindings.push({ serviceId: service.id, serviceName: service.displayName, apis: added }); } } else { apiRepo.deleteApisForService(service.id); } } logRepo.logScan({ type: "api", targetId: null, level: "info", message: `API-Scan: ${services.length} Dienst(e) geprüft, bei ${servicesWithApi} Dienst(en) ${totalFound} API-Endpunkt(e) gefunden.`, }); finishJob("apis", { checked: services.length, servicesWithApi, totalFound, newFindings }); } catch (err) { failJob("apis", err instanceof Error ? err.message : "Unbekannter Fehler"); } })(); return reply.code(202).send({ started: true }); }); app.post("/api/scan/apis/:serviceId", async (request, reply) => { const { serviceId } = request.params as { serviceId: string }; const service = serviceRepo.getService(serviceId); if (!service) { return reply.code(404).send({ error: "Dienst nicht gefunden" }); } const found = await detectApis(service.url); const { saved, added } = found.length > 0 ? apiRepo.replaceApisForService(service.id, found) : { saved: [], added: [] }; if (found.length === 0) apiRepo.deleteApisForService(service.id); return { serviceId, apis: saved, added }; }); }