diff --git a/ApiCore-Backend/.env.example b/ApiCore-Backend/.env.example
index ca6c3c7..887da42 100644
--- a/ApiCore-Backend/.env.example
+++ b/ApiCore-Backend/.env.example
@@ -8,3 +8,4 @@ JWT_ACCESS_EXPIRES_IN="15m"
JWT_REFRESH_EXPIRES_IN="7d"
PORT=3001
+FRONTEND_ORIGIN="http://localhost:3000"
diff --git a/ApiCore-Backend/src/app.config.ts b/ApiCore-Backend/src/app.config.ts
index 33bc44b..b9895a1 100644
--- a/ApiCore-Backend/src/app.config.ts
+++ b/ApiCore-Backend/src/app.config.ts
@@ -11,6 +11,11 @@ export function configureApp(app: INestApplication): void {
app.use(helmet());
app.use(cookieParser());
+ app.enableCors({
+ origin: process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000',
+ credentials: true,
+ });
+
app.setGlobalPrefix('api', {
exclude: [
{ path: 'health', method: RequestMethod.GET },
diff --git a/ApiCore-Frontend/proxy.ts b/ApiCore-Frontend/proxy.ts
new file mode 100644
index 0000000..60d2189
--- /dev/null
+++ b/ApiCore-Frontend/proxy.ts
@@ -0,0 +1,26 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+const SESSION_COOKIE = "refreshToken";
+
+export function proxy(request: NextRequest) {
+ const hasSession = request.cookies.has(SESSION_COOKIE);
+ const { pathname } = request.nextUrl;
+
+ if (pathname.startsWith("/login")) {
+ if (hasSession) {
+ return NextResponse.redirect(new URL("/settings/users", request.url));
+ }
+ return NextResponse.next();
+ }
+
+ if (!hasSession) {
+ return NextResponse.redirect(new URL("/login", request.url));
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: ["/login", "/settings/:path*"],
+};
diff --git a/ApiCore-Frontend/public/icon.svg b/ApiCore-Frontend/public/icon.svg
new file mode 100644
index 0000000..7593cc6
--- /dev/null
+++ b/ApiCore-Frontend/public/icon.svg
@@ -0,0 +1,8 @@
+
diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx
new file mode 100644
index 0000000..bdbd915
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/layout.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { ReactNode, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { useAuth } from "@/lib/auth/auth-context";
+import { Button } from "@/components/ui/button";
+
+export default function AppLayout({ children }: { children: ReactNode }) {
+ const router = useRouter();
+ const { user, isLoading, logout } = useAuth();
+
+ useEffect(() => {
+ if (!isLoading && !user) {
+ router.replace("/login");
+ }
+ }, [isLoading, user, router]);
+
+ if (isLoading || !user) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/settings/layout.tsx b/ApiCore-Frontend/src/app/(app)/settings/layout.tsx
new file mode 100644
index 0000000..85a0ee3
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/settings/layout.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { ReactNode } from "react";
+import { useAuth } from "@/lib/auth/auth-context";
+
+export default function SettingsLayout({ children }: { children: ReactNode }) {
+ const { user } = useAuth();
+
+ if (user?.role !== "ADMIN") {
+ return (
+
+
+ Diese Seite ist nur für Administratoren verfügbar.
+
+
+ );
+ }
+
+ return <>{children}>;
+}
diff --git a/ApiCore-Frontend/src/app/(app)/settings/users/create-user-dialog.tsx b/ApiCore-Frontend/src/app/(app)/settings/users/create-user-dialog.tsx
new file mode 100644
index 0000000..44ed10e
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/settings/users/create-user-dialog.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { FormEvent, useState } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { createUser } from "@/lib/users/api";
+import { Role } from "@/lib/users/types";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+const ROLES: Role[] = ["ADMIN", "BEEKEEPER", "STAFF", "READONLY", "API_CLIENT"];
+
+export function CreateUserDialog() {
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [name, setName] = useState("");
+ const [role, setRole] = useState("STAFF");
+ const [error, setError] = useState(null);
+
+ const mutation = useMutation({
+ mutationFn: createUser,
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["users"] });
+ setOpen(false);
+ setEmail("");
+ setPassword("");
+ setName("");
+ setRole("STAFF");
+ setError(null);
+ },
+ onError: () => {
+ setError("Benutzer konnte nicht angelegt werden.");
+ },
+ });
+
+ function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setError(null);
+ mutation.mutate({ email, password, name, role });
+ }
+
+ return (
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/settings/users/page.spec.tsx b/ApiCore-Frontend/src/app/(app)/settings/users/page.spec.tsx
new file mode 100644
index 0000000..f6d2472
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/settings/users/page.spec.tsx
@@ -0,0 +1,51 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import type { ReactElement } from "react";
+import UsersPage from "./page";
+import { deactivateUser, fetchUsers } from "@/lib/users/api";
+
+vi.mock("@/lib/users/api", () => ({
+ fetchUsers: vi.fn(),
+ deactivateUser: vi.fn(),
+ createUser: vi.fn(),
+}));
+
+function renderWithClient(ui: ReactElement) {
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return render({ui});
+}
+
+describe("UsersPage", () => {
+ beforeEach(() => {
+ vi.mocked(fetchUsers).mockReset();
+ vi.mocked(deactivateUser).mockReset();
+ });
+
+ it("renders the fetched users", async () => {
+ vi.mocked(fetchUsers).mockResolvedValueOnce([
+ {
+ id: "1",
+ email: "a@b.com",
+ name: "Admin",
+ role: "ADMIN",
+ isActive: true,
+ createdAt: "",
+ deactivatedAt: null,
+ },
+ ]);
+
+ renderWithClient();
+
+ expect(await screen.findByText("a@b.com")).toBeInTheDocument();
+ expect(screen.getByText("Aktiv")).toBeInTheDocument();
+ });
+
+ it("shows an error message when loading users fails", async () => {
+ vi.mocked(fetchUsers).mockRejectedValueOnce(new Error("boom"));
+
+ renderWithClient();
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(/nicht geladen/i);
+ });
+});
diff --git a/ApiCore-Frontend/src/app/(app)/settings/users/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/users/page.tsx
new file mode 100644
index 0000000..f0ae041
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/settings/users/page.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { deactivateUser, fetchUsers } from "@/lib/users/api";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { CreateUserDialog } from "./create-user-dialog";
+
+export default function UsersPage() {
+ const queryClient = useQueryClient();
+ const { data: users, isLoading, isError } = useQuery({
+ queryKey: ["users"],
+ queryFn: fetchUsers,
+ });
+
+ const deactivateMutation = useMutation({
+ mutationFn: deactivateUser,
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["users"] });
+ },
+ });
+
+ return (
+
+
+
Benutzerverwaltung
+
+
+
+ {isLoading ? Lädt…
: null}
+ {isError ? (
+
+ Benutzer konnten nicht geladen werden.
+
+ ) : null}
+
+ {users ? (
+
+
+
+ Name
+ E-Mail
+ Rolle
+ Status
+ Aktion
+
+
+
+ {users.map((user) => (
+
+ {user.name}
+ {user.email}
+ {user.role}
+
+
+ {user.isActive ? "Aktiv" : "Deaktiviert"}
+
+
+
+
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(auth)/login/page.spec.tsx b/ApiCore-Frontend/src/app/(auth)/login/page.spec.tsx
new file mode 100644
index 0000000..e925bf3
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(auth)/login/page.spec.tsx
@@ -0,0 +1,44 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import LoginPage from "./page";
+
+const pushMock = vi.fn();
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: pushMock }),
+}));
+
+const loginMock = vi.fn();
+vi.mock("@/lib/auth/auth-context", () => ({
+ useAuth: () => ({ login: loginMock, user: null, isLoading: false, logout: vi.fn() }),
+}));
+
+describe("LoginPage", () => {
+ beforeEach(() => {
+ pushMock.mockReset();
+ loginMock.mockReset();
+ });
+
+ it("submits credentials and redirects on success", async () => {
+ loginMock.mockResolvedValueOnce(undefined);
+ render();
+
+ await userEvent.type(screen.getByLabelText("E-Mail"), "admin@apicore.local");
+ await userEvent.type(screen.getByLabelText("Passwort"), "ChangeMe123!");
+ await userEvent.click(screen.getByRole("button", { name: /anmelden/i }));
+
+ expect(loginMock).toHaveBeenCalledWith("admin@apicore.local", "ChangeMe123!");
+ expect(pushMock).toHaveBeenCalledWith("/settings/users");
+ });
+
+ it("shows an error message when login fails", async () => {
+ loginMock.mockRejectedValueOnce(new Error("nope"));
+ render();
+
+ await userEvent.type(screen.getByLabelText("E-Mail"), "admin@apicore.local");
+ await userEvent.type(screen.getByLabelText("Passwort"), "wrong-password");
+ await userEvent.click(screen.getByRole("button", { name: /anmelden/i }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(/fehlgeschlagen/i);
+ });
+});
diff --git a/ApiCore-Frontend/src/app/(auth)/login/page.tsx b/ApiCore-Frontend/src/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..97fcd1b
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(auth)/login/page.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { FormEvent, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useAuth } from "@/lib/auth/auth-context";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+export default function LoginPage() {
+ const router = useRouter();
+ const { login } = useAuth();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setError(null);
+ setIsSubmitting(true);
+ try {
+ await login(email, password);
+ router.push("/settings/users");
+ } catch {
+ setError("Login fehlgeschlagen. Bitte E-Mail und Passwort prüfen.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+ ApiCore Login
+ Digitale Imkereiverwaltung
+
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/manifest.ts b/ApiCore-Frontend/src/app/manifest.ts
index 606578b..8d13d24 100644
--- a/ApiCore-Frontend/src/app/manifest.ts
+++ b/ApiCore-Frontend/src/app/manifest.ts
@@ -11,14 +11,9 @@ export default function manifest(): MetadataRoute.Manifest {
theme_color: "#0a0a0a",
icons: [
{
- src: "/icon-192x192.png",
- sizes: "192x192",
- type: "image/png",
- },
- {
- src: "/icon-512x512.png",
- sizes: "512x512",
- type: "image/png",
+ src: "/icon.svg",
+ sizes: "any",
+ type: "image/svg+xml",
},
],
};
diff --git a/ApiCore-Frontend/src/app/providers.tsx b/ApiCore-Frontend/src/app/providers.tsx
index b7a738a..1f56b8a 100644
--- a/ApiCore-Frontend/src/app/providers.tsx
+++ b/ApiCore-Frontend/src/app/providers.tsx
@@ -2,9 +2,14 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
+import { AuthProvider } from "@/lib/auth/auth-context";
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
- return {children};
+ return (
+
+ {children}
+
+ );
}
diff --git a/ApiCore-Frontend/src/lib/auth/api-client.ts b/ApiCore-Frontend/src/lib/auth/api-client.ts
new file mode 100644
index 0000000..f017c94
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/auth/api-client.ts
@@ -0,0 +1,53 @@
+import { getAccessToken, setAccessToken } from './token-store';
+import { refreshRequest } from './api';
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001/api/v1';
+
+export class ApiError extends Error {
+ status: number;
+
+ constructor(status: number, message: string) {
+ super(message);
+ this.status = status;
+ }
+}
+
+async function rawFetch(path: string, init: RequestInit): Promise {
+ const token = getAccessToken();
+ return fetch(`${API_URL}${path}`, {
+ ...init,
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ ...init.headers,
+ },
+ });
+}
+
+export async function apiFetch(path: string, init: RequestInit = {}): Promise {
+ let response = await rawFetch(path, init);
+
+ if (response.status === 401) {
+ const refreshed = await refreshRequest();
+ if (refreshed) {
+ setAccessToken(refreshed.accessToken);
+ response = await rawFetch(path, init);
+ }
+ }
+
+ if (!response.ok) {
+ const body: unknown = await response.json().catch(() => ({}));
+ const message =
+ typeof body === 'object' && body !== null && 'message' in body
+ ? String((body as { message: unknown }).message)
+ : 'Request failed';
+ throw new ApiError(response.status, message);
+ }
+
+ if (response.status === 204) {
+ return undefined as T;
+ }
+
+ return response.json() as Promise;
+}
diff --git a/ApiCore-Frontend/src/lib/auth/api.ts b/ApiCore-Frontend/src/lib/auth/api.ts
new file mode 100644
index 0000000..be022ab
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/auth/api.ts
@@ -0,0 +1,34 @@
+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001/api/v1';
+
+export interface AccessTokenResponse {
+ accessToken: string;
+}
+
+export async function loginRequest(email: string, password: string): Promise {
+ const response = await fetch(`${API_URL}/auth/login`, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ });
+ if (!response.ok) {
+ throw new Error('Login fehlgeschlagen. Bitte E-Mail und Passwort prüfen.');
+ }
+ return response.json() as Promise;
+}
+
+export async function refreshRequest(signal?: AbortSignal): Promise {
+ const response = await fetch(`${API_URL}/auth/refresh`, {
+ method: 'POST',
+ credentials: 'include',
+ signal,
+ });
+ if (!response.ok) {
+ return null;
+ }
+ return response.json() as Promise;
+}
+
+export async function logoutRequest(): Promise {
+ await fetch(`${API_URL}/auth/logout`, { method: 'POST', credentials: 'include' });
+}
diff --git a/ApiCore-Frontend/src/lib/auth/auth-context.tsx b/ApiCore-Frontend/src/lib/auth/auth-context.tsx
new file mode 100644
index 0000000..d0717e5
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/auth/auth-context.tsx
@@ -0,0 +1,85 @@
+'use client';
+
+import { createContext, useCallback, useContext, useEffect, useState, ReactNode } from 'react';
+import { loginRequest, logoutRequest, refreshRequest } from './api';
+import { decodeAccessToken } from './jwt';
+import { setAccessToken as storeAccessToken } from './token-store';
+
+export interface AuthUser {
+ id: string;
+ email: string;
+ role: string;
+}
+
+interface AuthContextValue {
+ user: AuthUser | null;
+ isLoading: boolean;
+ login: (email: string, password: string) => Promise;
+ logout: () => Promise;
+}
+
+const AuthContext = createContext(undefined);
+
+function applyAccessToken(token: string): AuthUser | null {
+ storeAccessToken(token);
+ const claims = decodeAccessToken(token);
+ if (!claims) {
+ return null;
+ }
+ return { id: claims.sub, email: claims.email, role: claims.role };
+}
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ refreshRequest(controller.signal)
+ .then((result) => {
+ if (!result) {
+ return;
+ }
+ setUser(applyAccessToken(result.accessToken));
+ })
+ .catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === "AbortError") {
+ return;
+ }
+ throw error;
+ })
+ .finally(() => {
+ if (!controller.signal.aborted) {
+ setIsLoading(false);
+ }
+ });
+ return () => {
+ controller.abort();
+ };
+ }, []);
+
+ const login = useCallback(async (email: string, password: string) => {
+ const result = await loginRequest(email, password);
+ setUser(applyAccessToken(result.accessToken));
+ }, []);
+
+ const logout = useCallback(async () => {
+ await logoutRequest();
+ storeAccessToken(null);
+ setUser(null);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth(): AuthContextValue {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
diff --git a/ApiCore-Frontend/src/lib/auth/jwt.spec.ts b/ApiCore-Frontend/src/lib/auth/jwt.spec.ts
new file mode 100644
index 0000000..d3a0a9e
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/auth/jwt.spec.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from "vitest";
+import { decodeAccessToken } from "./jwt";
+
+function makeToken(payload: object): string {
+ const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }));
+ const body = btoa(JSON.stringify(payload));
+ return `${header}.${body}.signature`;
+}
+
+describe("decodeAccessToken", () => {
+ it("decodes a valid JWT payload", () => {
+ const token = makeToken({ sub: "user-1", email: "a@b.com", role: "ADMIN", exp: 123 });
+ expect(decodeAccessToken(token)).toEqual({
+ sub: "user-1",
+ email: "a@b.com",
+ role: "ADMIN",
+ exp: 123,
+ });
+ });
+
+ it("returns null for a malformed token", () => {
+ expect(decodeAccessToken("not-a-jwt")).toBeNull();
+ });
+});
diff --git a/ApiCore-Frontend/src/lib/auth/jwt.ts b/ApiCore-Frontend/src/lib/auth/jwt.ts
new file mode 100644
index 0000000..723edd8
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/auth/jwt.ts
@@ -0,0 +1,17 @@
+export interface AccessTokenClaims {
+ sub: string;
+ email: string;
+ role: string;
+ exp: number;
+}
+
+export function decodeAccessToken(token: string): AccessTokenClaims | null {
+ try {
+ const payload = token.split('.')[1];
+ const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
+ const json = atob(base64);
+ return JSON.parse(json) as AccessTokenClaims;
+ } catch {
+ return null;
+ }
+}
diff --git a/ApiCore-Frontend/src/lib/auth/token-store.ts b/ApiCore-Frontend/src/lib/auth/token-store.ts
new file mode 100644
index 0000000..42a9d37
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/auth/token-store.ts
@@ -0,0 +1,9 @@
+let currentAccessToken: string | null = null;
+
+export function getAccessToken(): string | null {
+ return currentAccessToken;
+}
+
+export function setAccessToken(token: string | null): void {
+ currentAccessToken = token;
+}
diff --git a/ApiCore-Frontend/src/lib/users/api.ts b/ApiCore-Frontend/src/lib/users/api.ts
new file mode 100644
index 0000000..3b71b73
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/users/api.ts
@@ -0,0 +1,17 @@
+import { apiFetch } from "@/lib/auth/api-client";
+import { CreateUserInput, User } from "./types";
+
+export function fetchUsers(): Promise {
+ return apiFetch("/users");
+}
+
+export function createUser(input: CreateUserInput): Promise {
+ return apiFetch("/users", {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
+
+export function deactivateUser(id: string): Promise {
+ return apiFetch(`/users/${id}/deactivate`, { method: "POST" });
+}
diff --git a/ApiCore-Frontend/src/lib/users/types.ts b/ApiCore-Frontend/src/lib/users/types.ts
new file mode 100644
index 0000000..c93730d
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/users/types.ts
@@ -0,0 +1,18 @@
+export type Role = "ADMIN" | "BEEKEEPER" | "STAFF" | "READONLY" | "API_CLIENT";
+
+export interface User {
+ id: string;
+ email: string;
+ name: string;
+ role: Role;
+ isActive: boolean;
+ createdAt: string;
+ deactivatedAt: string | null;
+}
+
+export interface CreateUserInput {
+ email: string;
+ password: string;
+ name: string;
+ role: Role;
+}
diff --git a/TASKS.md b/TASKS.md
index aef344b..9154810 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -19,7 +19,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
- Frontend: `/login`, `/settings/users` (nur Administrator)
- [x] Rollen-Enum + Guards (`JwtAuthGuard`, `RolesGuard`, `@Roles()`) implementiert und in jedem künftigen Modul verpflichtend
- [x] Backend-Modul fertig, getestet, grün, committet
-- [ ] Frontend-Views fertig, getestet, grün, committet
+- [x] Frontend-Views fertig, getestet, grün, committet
### 2. Bienenstände
diff --git a/deploy/k8s/backend/configmap.example.yaml b/deploy/k8s/backend/configmap.example.yaml
index 23ea7d8..bbf7cd3 100644
--- a/deploy/k8s/backend/configmap.example.yaml
+++ b/deploy/k8s/backend/configmap.example.yaml
@@ -6,3 +6,4 @@ data:
PORT: "3001"
JWT_ACCESS_EXPIRES_IN: "15m"
JWT_REFRESH_EXPIRES_IN: "7d"
+ FRONTEND_ORIGIN: "https://apicore.example.com"