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

94 lines
3.6 KiB
TypeScript

import type { FastifyInstance } from "fastify";
import { ServiceCreateSchema, ServiceReorderSchema, ServiceUpdateSchema } from "@launchpad/shared";
import * as deviceRepo from "../db/repositories/devices.js";
import * as serviceRepo from "../db/repositories/services.js";
import { suggestIconsForServiceName } from "../scanner/iconDb.js";
interface ServiceListQuery {
deviceId?: string;
category?: string;
favorite?: string;
}
export async function serviceRoutes(app: FastifyInstance): Promise<void> {
app.get("/api/services", async (request) => {
const query = request.query as ServiceListQuery;
return serviceRepo.listServices({
deviceId: query.deviceId,
category: query.category,
favorite: query.favorite === undefined ? undefined : query.favorite === "true",
});
});
app.get("/api/services/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const service = serviceRepo.getService(id);
if (!service) {
return reply.code(404).send({ error: "Dienst nicht gefunden" });
}
return service;
});
app.post("/api/services", async (request, reply) => {
const parsed = ServiceCreateSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
}
const device = deviceRepo.getDevice(parsed.data.deviceId);
if (!device) {
return reply.code(400).send({ error: `Gerät ${parsed.data.deviceId} existiert nicht` });
}
const service = serviceRepo.createService(parsed.data);
return reply.code(201).send(service);
});
// Muss vor der /:id-Route stehen, damit "reorder" nicht als ID interpretiert wird.
app.patch("/api/services/reorder", async (request, reply) => {
const parsed = ServiceReorderSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
}
return serviceRepo.reorderServices(parsed.data);
});
app.patch("/api/services/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const parsed = ServiceUpdateSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
}
const service = serviceRepo.updateService(id, parsed.data);
if (!service) {
return reply.code(404).send({ error: "Dienst nicht gefunden" });
}
return service;
});
app.delete("/api/services/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const deleted = serviceRepo.deleteService(id);
if (!deleted) {
return reply.code(404).send({ error: "Dienst nicht gefunden" });
}
return reply.code(204).send();
});
// Liefert für jeden Dienst OHNE Favicon eine Liste möglicher Treffer aus
// der externen Icon-Datenbank (siehe scanner/iconDb.ts), damit der Nutzer
// selbst auswählen kann statt dass automatisch geraten wird (mehrdeutige
// Namen wie "LXC Adguard" könnten sonst falsch zugeordnet werden).
app.get("/api/services/favicon-suggestions", async () => {
const services = serviceRepo.listServices().filter((s) => !s.favicon);
const suggestions = [];
for (const service of services) {
const candidates = await suggestIconsForServiceName(service.displayName);
if (candidates.length > 0) {
suggestions.push({ serviceId: service.id, serviceName: service.displayName, candidates });
}
}
return { checked: services.length, suggestions };
});
}