generated from Dicken/dickendock
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { useServices } from "../../hooks/useServices.js";
|
|
import { useDetectedApis, type DetectedApiEntry } from "../../hooks/useDetectedApis.js";
|
|
import { AdminPageHeader } from "./AdminPageHeader.js";
|
|
|
|
function formatDate(iso: string): string {
|
|
try {
|
|
return new Date(iso).toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" });
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
export function ApisPage() {
|
|
const { data: services } = useServices();
|
|
const { data: apis, isLoading, isError } = useDetectedApis();
|
|
|
|
const serviceById = new Map((services ?? []).map((s) => [s.id, s]));
|
|
const groups = new Map<string, DetectedApiEntry[]>();
|
|
for (const entry of apis ?? []) {
|
|
const list = groups.get(entry.serviceId) ?? [];
|
|
list.push(entry);
|
|
groups.set(entry.serviceId, list);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<AdminPageHeader
|
|
title="APIs"
|
|
description='Von einem eigenen, separaten Scan gefundene API-Endpunkte bekannter Dienste (siehe Admin -> Scanner -> "APIs jetzt scannen"). Läuft nie automatisch mit.'
|
|
/>
|
|
|
|
{isLoading ? (
|
|
<p className="text-sm text-black/40 dark:text-white/40">Lade …</p>
|
|
) : isError ? (
|
|
<p className="text-sm text-red-500">Konnte nicht geladen werden.</p>
|
|
) : groups.size === 0 ? (
|
|
<p className="text-sm text-black/40 dark:text-white/40">
|
|
Noch keine APIs gefunden. Auf der Scanner-Seite einmal „APIs jetzt scannen" ausführen.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{Array.from(groups.entries()).map(([serviceId, entries]) => {
|
|
const service = serviceById.get(serviceId);
|
|
return (
|
|
<div key={serviceId} className="rounded-2xl border border-black/10 p-4 dark:border-white/10">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<h3 className="font-medium text-black dark:text-white">
|
|
{service?.displayName ?? "Unbekannter Dienst"}
|
|
</h3>
|
|
<span className="text-xs text-black/30 dark:text-white/30">
|
|
zuletzt geprüft: {formatDate(entries[0].detectedAt)}
|
|
</span>
|
|
</div>
|
|
<ul className="flex flex-wrap gap-2">
|
|
{entries.map((e) => (
|
|
<li key={e.id}>
|
|
<a
|
|
href={`${service?.url ?? ""}${e.path}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
title={`HTTP ${e.status}`}
|
|
className="flex items-center gap-1.5 rounded-full bg-black/5 px-2.5 py-1 text-xs
|
|
text-black/70 hover:bg-black/10 dark:bg-white/10 dark:text-white/70
|
|
dark:hover:bg-white/20"
|
|
>
|
|
{e.type} · {e.path}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|