From 650541176196c056300665ad6617df11e893c9c5 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Mon, 12 Oct 2026 12:00:03 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20API-Schl=C3=BCssel=20nur=20mit=20Rec?= =?UTF-8?q?hten=20anbieten,=20die=20der=20Nutzer=20selbst=20hat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Server lehnt Rechte ab, die der Anlegende nicht selbst hat. Das UI bietet jetzt auch nur diese an (z. B. kein shop-sync:write ohne eigenes Recht) und erklärt, wenn gar keines übrig bleibt. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../settings/automation/api-keys-tab.spec.tsx | 23 +++++++++++++++++++ .../settings/automation/api-keys-tab.tsx | 15 ++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.spec.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.spec.tsx index cfe78c2..19c77c8 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.spec.tsx @@ -7,6 +7,12 @@ import { ApiKeysTab } from "./api-keys-tab"; import { createApiKey, fetchApiKeys, revokeApiKey } from "@/lib/api-keys/api"; import type { ApiKey } from "@/lib/api-keys/types"; +// Rechte des angemeldeten Nutzers; je Test anpassbar. +let ownPermissions: string[] = []; +vi.mock("@/lib/auth/auth-context", () => ({ + useAuth: () => ({ hasPermission: (permission: string) => ownPermissions.includes(permission) }), +})); + vi.mock("@/lib/api-keys/api", () => ({ createApiKey: vi.fn(), fetchApiKeys: vi.fn(), @@ -38,6 +44,7 @@ function key(overrides: Partial): ApiKey { describe("ApiKeysTab", () => { beforeEach(() => { + ownPermissions = ["api-keys:manage", "shop-sync:read", "shop-sync:write", "erp-inventory:read"]; vi.mocked(fetchApiKeys).mockReset().mockResolvedValue([]); vi.mocked(createApiKey).mockReset(); vi.mocked(revokeApiKey).mockReset(); @@ -94,4 +101,20 @@ describe("ApiKeysTab", () => { expect(await screen.findByText("ERPNext")).toBeInTheDocument(); expect(screen.getByText("Darf: Inventar-Spiegel lesen (ERPNext)")).toBeInTheDocument(); }); + + it("bietet nur Rechte an, die der angemeldete Nutzer selbst hat", () => { + ownPermissions = ["api-keys:manage", "shop-sync:read", "erp-inventory:read"]; + renderWithClient(); + expect(screen.getByRole("checkbox", { name: /Shop-Daten lesen/ })).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: /Inventar-Spiegel lesen/ })).toBeInTheDocument(); + expect(screen.queryByRole("checkbox", { name: /Verkäufe und Rückbuchungen/ })).toBeNull(); + }); + + it("wählt nichts vor, was der Nutzer nicht vergeben darf, und erklärt es", () => { + ownPermissions = ["api-keys:manage"]; + renderWithClient(); + expect(screen.queryAllByRole("checkbox")).toHaveLength(0); + expect(screen.getByText(/keine Rechte, die ein API-Schlüssel tragen kann/)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Schlüssel anlegen" })).toBeDisabled(); + }); }); diff --git a/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.tsx index e6fefe9..8598901 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/api-keys-tab.tsx @@ -18,6 +18,7 @@ import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { ConfirmDialog } from "@/components/confirm-dialog"; import { ToggleChip } from "@/components/form/toggle-chip"; +import { useAuth } from "@/lib/auth/auth-context"; import { ApiKeyAllowlist, parseAllowlist } from "./api-key-allowlist"; // Vorschlag wie bisher: der Wabenhain-Shop liest. Alles andere wird bewusst angekreuzt. @@ -56,10 +57,15 @@ function keyState(apiKey: ApiKey): { label: string; active: boolean } { */ export function ApiKeysTab() { const queryClient = useQueryClient(); + const { hasPermission } = useAuth(); + // Der Server lässt nur Rechte zu, die der Anlegende selbst hat - das UI + // bietet deshalb auch nur diese an, statt erst beim Anlegen abzulehnen. + const grantable = API_KEY_SCOPES.filter((scope) => hasPermission(scope)); const [name, setName] = useState("Wabenhain-Shop"); const [expiresInDays, setExpiresInDays] = useState(""); const [allowedIps, setAllowedIps] = useState(""); - const [scopes, setScopes] = useState([...DEFAULT_SCOPES]); + const [chosenScopes, setScopes] = useState([...DEFAULT_SCOPES]); + const scopes = chosenScopes.filter((scope) => grantable.includes(scope)); const [created, setCreated] = useState<{ name: string; token: string; @@ -202,8 +208,13 @@ export function ApiKeysTab() {
Was darf der Schlüssel? + {grantable.length === 0 ? ( +

+ Sie haben selbst keine Rechte, die ein API-Schlüssel tragen kann. +

+ ) : null}
- {API_KEY_SCOPES.map((scope) => ( + {grantable.map((scope) => (