Domain-Check: fuehrt die Domain wirklich hierher?

Eine Domain zu registrieren ist der kleinste Teil der Arbeit — DNS,
Reverse Proxy und Zertifikat liegen ausserhalb dieser Anwendung. Fehlt
dort etwas, merkt man es am gedruckten Code, der nicht aufgeht.

Der Knopf "Pruefen" geht den Weg der Scanner nach: Namen aufloesen,
Zertifikat ansehen, Adresse aufrufen — und sich beweisen lassen, dass am
anderen Ende diese Installation steht. Der Beweis ist ein HMAC ueber eine
frische Zufallszahl; das Geheimnis dafuer entsteht beim ersten Check und
verlaesst den Server nie.

Geprueft wird nur ein Hostname aus der Datenbank, nie einer aus der
Anfrage — sonst waere die Route ein Werkzeug zum Abklopfen fremder
Adressen. Scheitert schon DNS, fallen die drei Folgeschritte aus, statt
dreimal dasselbe zu melden.

Die Grenze der Aussage steht im Bericht selbst: Der Aufruf startet dort,
wo der Container laeuft.

Refs #7
This commit is contained in:
Florian Wilde
2026-08-21 03:06:54 +02:00
parent b96e9ddbe5
commit 69c34c4f0c
9 changed files with 733 additions and 0 deletions
+40
View File
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from "react";
import { DomainBericht } from "@/components/domain-bericht";
import { Icon } from "@/components/icons";
import { Modal } from "@/components/modal";
import {
@@ -14,6 +15,7 @@ import {
TextInput,
} from "@/components/ui";
import { ApiError, apiJson } from "@/lib/api-client";
import type { Pruefbericht } from "@/lib/domain-check";
type Grant = { groupId: string; groupName: string; isDefault: boolean };
@@ -44,6 +46,10 @@ export function DomainAdmin({ baseUrlHostname }: { baseUrlHostname: string }) {
const [deleting, setDeleting] = useState<AdminDomain | null>(null);
/** Prüfberichte je Domain — bleiben stehen, bis erneut geprüft wird. */
const [reports, setReports] = useState<Record<string, Pruefbericht>>({});
const [checking, setChecking] = useState<string | null>(null);
const load = useCallback(async () => {
try {
const result = await apiJson<{
@@ -122,6 +128,26 @@ export function DomainAdmin({ baseUrlHostname }: { baseUrlHostname: string }) {
}
};
const check = async (domain: AdminDomain) => {
setChecking(domain.id);
setError(null);
try {
const { report } = await apiJson<{ report: Pruefbericht }>(
`/api/admin/domains/${domain.id}/check`,
{ method: "POST" },
);
setReports((bisher) => ({ ...bisher, [domain.id]: report }));
} catch (caught) {
setError(
caught instanceof ApiError
? caught.message
: "Die Prüfung ließ sich nicht ausführen.",
);
} finally {
setChecking(null);
}
};
if (domains === null) {
return <p className="text-sm text-slate-500">Domains werden geladen …</p>;
}
@@ -192,6 +218,16 @@ export function DomainAdmin({ baseUrlHostname }: { baseUrlHostname: string }) {
<Icon name="pencil" className="size-3.5" />
Gruppen zuteilen
</Button>
<Button
type="button"
variant="secondary"
size="sm"
disabled={checking !== null}
onClick={() => void check(domain)}
>
<Icon name="scan" className="size-3.5" />
{checking === domain.id ? "Prüft …" : "Prüfen"}
</Button>
<Button
type="button"
variant="secondary"
@@ -213,6 +249,10 @@ export function DomainAdmin({ baseUrlHostname }: { baseUrlHostname: string }) {
Löschen
</Button>
</div>
{reports[domain.id] ? (
<DomainBericht bericht={reports[domain.id]} />
) : null}
</div>
</div>
</Card>