feat(PROJ-25): User-Profil & Einstellungen — Passwort, E-Mail, 2FA

Backend:
- PATCH /api/auth/password — Passwort ändern (bcrypt, LDAP-Guard, Audit-Log)
- PATCH /api/auth/email — E-Mail ändern (Unique-Check, LDAP-Guard, Audit-Log)
- userstore: UpdatePassword, UpdateEmail, GetPasswordHash

Frontend:
- UserNav.tsx: Dropdown-Menü (Profil & Einstellungen, Abmelden)
- navbar.tsx: UserNav eingebunden
- /settings: Passwort ändern, E-Mail ändern, 2FA verwalten (QR-Code + Deaktivieren)
- api.ts: changePassword, changeEmail, getTOTPSetup, confirmTOTPSetup, disableTOTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-18 01:05:33 +01:00
parent 89a6651b62
commit 280034679e
7 changed files with 753 additions and 22 deletions
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { useRouter } from "next/navigation";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ChevronDown } from "lucide-react";
import { logout } from "@/lib/api";
interface UserNavProps {
username: string;
role: string;
}
export function UserNav({ username, role }: UserNavProps) {
const router = useRouter();
async function handleLogout() {
try {
await logout();
} catch {
// ignore logout errors
}
router.push("/");
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="flex items-center gap-2"
aria-label="Benutzermenu"
>
<span className="text-sm font-medium">{username}</span>
<Badge variant="secondary" className="text-xs">
{role}
</Badge>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => router.push("/settings")}>
Profil &amp; Einstellungen
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="text-red-600">
Abmelden
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+2 -22
View File
@@ -1,10 +1,7 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { logout } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { UserNav } from "@/components/UserNav";
interface NavbarProps {
username: string;
@@ -12,17 +9,6 @@ interface NavbarProps {
}
export function Navbar({ username, role }: NavbarProps) {
const router = useRouter();
async function handleLogout() {
try {
await logout();
} catch {
// ignore logout errors
}
router.push("/");
}
return (
<nav
className="border-b bg-background"
@@ -63,13 +49,7 @@ export function Navbar({ username, role }: NavbarProps) {
</Link>
)}
</div>
<div className="flex items-center gap-3">
<span className="text-sm">{username}</span>
<Badge variant="secondary">{role}</Badge>
<Button variant="outline" size="sm" onClick={handleLogout}>
Abmelden
</Button>
</div>
<UserNav username={username} role={role} />
</div>
</nav>
);