feat(api): API-Tokens, kurze Anfragen und OpenAPI für die CLI

- ApiToken (nur SHA-256), Bearer-Anmeldung, POST /auth/token für proxdeploy login, Verwaltung in den Einstellungen
- anfrageErgaenzen: Storage, Bridge, Node, Benutzer und Schlüssel aus Server-Standards, Verbindungstest und Anmeldung – auch beim Template-Bau
- Abschlussbox serverseitig (abschluss), Web und CLI zeigen dieselben Zeilen
- Antwortschemas als zod, daraus /api/v1/openapi.json und cli/api/openapi.json (pnpm openapi)

Refs #013

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 15:08:27 +02:00
co-authored by Claude Opus 5.5
parent 1e195f9bca
commit 9fdced62db
27 changed files with 1386 additions and 58 deletions
+31 -1
View File
@@ -23,7 +23,7 @@ test("SSH-Schlüssel hinterlegen, im Assistenten vorausgewählt, wieder entferne
if (alt.name.startsWith(E2E_PRAEFIX)) await page.request.delete(`/api/v1/schluessel/${alt.id}`);
}
await page.reload();
await page.getByLabel("Name").fill(name);
await page.locator("#schluessel-name").fill(name);
await page.getByLabel("Öffentlicher Schlüssel").fill("-----BEGIN OPENSSH PRIVATE KEY-----");
await page.getByRole("button", { name: "Hinzufügen" }).click();
await expect(page.getByText(/nie den privaten Schlüssel/)).toBeVisible();
@@ -47,3 +47,33 @@ test("SSH-Schlüssel hinterlegen, im Assistenten vorausgewählt, wieder entferne
await page.getByRole("button", { name: `Schlüssel ${name} entfernen` }).click();
await expect(page.getByText(name)).toHaveCount(0);
});
test("API-Token anlegen, einmal sehen, damit die API nutzen, widerrufen", async ({ page }) => {
const name = `${E2E_PRAEFIX}-token-${test.info().project.name}`;
await anmelden(page, "/einstellungen");
for (const alt of (await (await page.request.get("/api/v1/tokens")).json()) as Array<{
id: string;
name: string;
}>) {
if (alt.name.startsWith(E2E_PRAEFIX)) await page.request.delete(`/api/v1/tokens/${alt.id}`);
}
await page.reload();
await page.locator("#token-name").fill(name);
await page.getByRole("button", { name: "Token anlegen" }).click();
const anzeige = page.getByRole("status").filter({ hasText: "nur jetzt angezeigt" });
await expect(anzeige).toBeVisible();
const token = (await anzeige.locator("code").first().innerText()).trim();
expect(token).toMatch(/^pd_/);
await keinQuerscrollen(page);
const ich = await page.request.get("/api/v1/ich", {
headers: { authorization: `Bearer ${token}`, cookie: "" },
});
expect(ich.status()).toBe(200);
page.once("dialog", (d) => void d.accept());
await page.getByRole("button", { name: `Token ${name} widerrufen` }).click();
await expect(page.getByText(`${name} ·`)).toHaveCount(0);
const danach = await page.request.get("/api/v1/ich", { headers: { authorization: `Bearer ${token}` } });
expect(danach.status()).toBe(401);
});