feat: getrennte Login-Seiten für User (/), Admins (/admin/login)

- /admin/login: neue Login-Seite nur für auditor, admin, domain_admin, superadmin
- /: blockiert Admin-Rollen mit Hinweis auf /admin
- useAuth: neuer loginPage-Parameter für flexiblen Unauthentifiziert-Redirect
- /admin: leitet bei Nicht-Auth zu /admin/login statt /

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-31 23:47:41 +02:00
parent c1a9004720
commit acc0d93318
4 changed files with 117 additions and 7 deletions
+104
View File
@@ -0,0 +1,104 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { login } from "@/lib/api";
import { getCachedUser, setCachedUser } from "@/lib/auth-cache";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
const ADMIN_ROLES = ["auditor", "admin", "domain_admin", "superadmin"];
export default function AdminLoginPage() {
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
const cached = getCachedUser();
if (cached !== null && ADMIN_ROLES.includes(cached.role)) {
router.replace("/admin");
}
}, [router]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await login(username, password);
const role = res?.user?.role ?? "";
if (!ADMIN_ROLES.includes(role)) {
setCachedUser(null);
setError("Kein Zugriff. Dieses Login ist nur für Admins und Auditoren.");
return;
}
if (role === "auditor") {
router.push("/search");
} else {
router.push("/admin");
}
} catch {
setError("Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen.");
} finally {
setLoading(false);
}
}
return (
<div className="flex min-h-screen items-center justify-center px-4">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">archivmail</CardTitle>
<p className="text-sm text-muted-foreground">Admin-Anmeldung</p>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Benutzername</Label>
<Input
id="username"
type="text"
placeholder="Benutzername"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoComplete="username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Passwort</Label>
<Input
id="password"
type="password"
placeholder="Passwort"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>
</div>
{error && (
<p className="text-sm text-destructive" role="alert">
{error}
</p>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Anmelden..." : "Anmelden"}
</Button>
</form>
<div className="mt-4 text-center text-sm text-muted-foreground">
<a href="/" className="hover:text-foreground underline">
Zur Benutzer-Anmeldung
</a>
</div>
</CardContent>
</Card>
</div>
);
}
+1 -1
View File
@@ -84,7 +84,7 @@ import { ResetPasswordDialog, DeleteUserDialog } from "@/components/admin/UserDi
const AUDIT_PAGE_SIZE = 25;
export default function AdminPage() {
const { user, loading: authLoading } = useAuth("domain_admin");
const { user, loading: authLoading } = useAuth("domain_admin", "/admin/login");
const isSuperAdmin = user?.role === "superadmin";
// Dashboard state
+9 -3
View File
@@ -16,13 +16,15 @@ export default function LoginPage() {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const ADMIN_ROLES = ["auditor", "admin", "domain_admin", "superadmin"];
useEffect(() => {
// Only redirect if we have a cached session — no API call, no loop risk
const cached = getCachedUser();
if (cached !== null) {
router.replace(cached.role === "superadmin" ? "/admin" : "/search");
router.replace(ADMIN_ROLES.includes(cached.role) ? "/admin/login" : "/search");
}
}, [router]);
}, [router]); // eslint-disable-line react-hooks/exhaustive-deps
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -32,7 +34,11 @@ export default function LoginPage() {
try {
const res = await login(username, password);
const role = res?.user?.role ?? "";
router.push(role === "superadmin" ? "/admin" : "/search");
if (ADMIN_ROLES.includes(role)) {
setError("Admins und Auditoren bitte über /admin anmelden.");
return;
}
router.push("/search");
} catch {
setError("Anmeldung fehlgeschlagen. Bitte Zugangsdaten pruefen.");
} finally {
+3 -3
View File
@@ -20,7 +20,7 @@ export function hasRole(userRole: string, required: string): boolean {
return (roleLevels[userRole] ?? 0) >= (roleLevels[required] ?? 0);
}
export function useAuth(requireRole?: "admin" | "domain_admin" | "superadmin" | "auditor") {
export function useAuth(requireRole?: "admin" | "domain_admin" | "superadmin" | "auditor", loginPage = "/") {
const router = useRouter();
const cached = getCachedUser();
const [user, setUser] = useState(cached);
@@ -49,9 +49,9 @@ export function useAuth(requireRole?: "admin" | "domain_admin" | "superadmin" |
setLoading(false);
} catch {
setCachedUser(null);
router.replace("/");
router.replace(loginPage);
}
}, [router, requireRole]);
}, [router, requireRole, loginPage]);
useEffect(() => {
checkAuth();