Files
archivmail/src/app/page.tsx
T
sysopsandClaude Sonnet 5 767373b206 feat(PROJ-46): E-Mail als primärer Login-Identifier für Tenant-User
Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt
per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13
(Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username
verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin
Username ODER E-Mail nutzen.

Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf
Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den
IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für
Tenant-User, Datenqualität vorab geprüft (0 Kollisionen).

Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt,
um Timing-basierte Identifier-Enumeration zu verhindern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:37:20 +02:00

122 lines
4.3 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { login } from "@/lib/api";
import { getCachedUser, setCachedUser, clearAuthCache } 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";
export default function LoginPage() {
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
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(ADMIN_ROLES.includes(cached.role) ? "/admin/login" : "/search");
}
}, [router]); // eslint-disable-line react-hooks/exhaustive-deps
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setLoading(true);
try {
clearAuthCache();
const res = await login(username, password);
const role = res?.user?.role ?? "";
if (ADMIN_ROLES.includes(role)) {
setError("ADMIN_REDIRECT");
return;
}
setCachedUser({ username: res.user.username, email: res.user.email, role, list_page_size: res.user.list_page_size });
router.push("/search");
} catch {
setError("Anmeldung fehlgeschlagen. Bitte Zugangsdaten pruefen.");
} 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">
E-Mail-Archiv Anmeldung
</p>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">E-Mail-Adresse</Label>
<Input
id="username"
type="email"
placeholder="E-Mail-Adresse"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoComplete="email"
aria-label="E-Mail-Adresse"
/>
</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"
aria-label="Passwort"
/>
</div>
{error && error !== "ADMIN_REDIRECT" && (
<p className="text-sm text-destructive" role="alert">
{error}
</p>
)}
{error === "ADMIN_REDIRECT" && (
<p className="text-sm text-destructive" role="alert">
Admins und Auditoren bitte{" "}
<a href="/admin/login" className="underline font-medium">
hier anmelden
</a>
.
</p>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Anmelden..." : "Anmelden"}
</Button>
</form>
<div className="mt-4 flex flex-col items-center gap-2 text-sm text-muted-foreground">
<a href="/forgot-password" className="hover:text-foreground underline">
Passwort vergessen?
</a>
<a href="/signup" className="hover:text-foreground underline">
Noch kein Account? Registrieren
</a>
<a href="/admin/login" className="hover:text-foreground underline">
Admin- / Auditor-Login
</a>
</div>
</CardContent>
</Card>
</div>
);
}