feat: Inventar-Export für ERPNext (erp-inventory:read) #1

Merged
Nick.Rosenloecher merged 8 commits from feat/erp-inventar-export into main 2026-10-13 05:39:01 +00:00
2 changed files with 36 additions and 2 deletions
Showing only changes of commit 6505411761 - Show all commits

No files matched your search

@@ -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>): 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(<ApiKeysTab />);
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(<ApiKeysTab />);
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();
});
});
@@ -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<ApiKeyScope[]>([...DEFAULT_SCOPES]);
const [chosenScopes, setScopes] = useState<ApiKeyScope[]>([...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() {
</div>
<fieldset className="flex w-full flex-col gap-1.5">
<legend className="mb-1.5 text-sm font-medium">Was darf der Schlüssel?</legend>
{grantable.length === 0 ? (
<p className="text-sm text-muted-foreground">
Sie haben selbst keine Rechte, die ein API-Schlüssel tragen kann.
</p>
) : null}
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
{API_KEY_SCOPES.map((scope) => (
{grantable.map((scope) => (
<ToggleChip
key={scope}
label={API_KEY_SCOPE_LABELS[scope]}