Files
LaunchPad/apps/backend/src/routes/apis.ts

95 lines
3.8 KiB
TypeScript

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<void> {
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 };
});
}