140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { LogIn } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
import { AuthProvider, useAuth } from "@/components/auth";
|
|
import { BrandLogo } from "@/components/brand/brand-logo";
|
|
import { login, User } from "@/lib/api";
|
|
|
|
const schema = z.object({
|
|
email: z.string().email("Bitte gib eine gueltige E-Mail-Adresse ein."),
|
|
password: z.string().min(1, "Bitte gib dein Passwort ein.")
|
|
});
|
|
|
|
type LoginForm = z.infer<typeof schema>;
|
|
|
|
type LoginError = { status: number; message: string };
|
|
|
|
function mapLoginError(error: unknown): LoginError {
|
|
const status = typeof error === "object" && error && "status" in error ? Number((error as { status?: number }).status) : 0;
|
|
switch (status) {
|
|
case 401:
|
|
return { status, message: "E-Mail-Adresse oder Passwort ist falsch." };
|
|
case 403:
|
|
return { status, message: "Dieses Benutzerkonto ist deaktiviert." };
|
|
case 422:
|
|
return { status, message: "Bitte pruefe deine Eingaben." };
|
|
case 500:
|
|
case 502:
|
|
return { status, message: "Der Anmeldedienst ist momentan nicht erreichbar." };
|
|
default:
|
|
return { status, message: "Anmeldung fehlgeschlagen. Bitte erneut versuchen." };
|
|
}
|
|
}
|
|
|
|
function LoginPanel() {
|
|
const auth = useAuth();
|
|
const [loading, setLoading] = useState(false);
|
|
const [toast, setToast] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
|
|
const form = useForm<LoginForm>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: { email: "admin@schubamed.de", password: "" },
|
|
mode: "onSubmit"
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!toast) return;
|
|
const timeout = window.setTimeout(() => setToast(""), 5000);
|
|
return () => window.clearTimeout(timeout);
|
|
}, [toast]);
|
|
|
|
async function handleSubmit(values: LoginForm) {
|
|
setLoading(true);
|
|
setToast("");
|
|
setSuccess("");
|
|
try {
|
|
const result = await login(values.email, values.password);
|
|
const currentUser = (await auth.refreshUser()) ?? (result.user as User);
|
|
auth.setUser(currentUser);
|
|
auth.setToken("authenticated");
|
|
const target = currentUser.must_change_password ? "/profile/security" : "/dashboard";
|
|
setSuccess("Anmeldung erfolgreich");
|
|
window.location.replace(target);
|
|
} catch (error) {
|
|
const mapped = mapLoginError(error);
|
|
if (process.env.NODE_ENV !== "production") {
|
|
console.error("Login failed", error);
|
|
console.error("Login failed mapped", mapped.status, mapped.message);
|
|
}
|
|
setToast(mapped.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
|
|
<section className="w-full max-w-md rounded-lg border border-border bg-surface p-8 shadow-soft">
|
|
<div className="mb-8">
|
|
<BrandLogo />
|
|
<h1 className="mt-2 text-3xl font-semibold text-text">Anmelden</h1>
|
|
<p className="mt-3 text-sm leading-6 text-text-light">Sicherer Zugriff auf Atlas Workspace.</p>
|
|
</div>
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void form.handleSubmit(handleSubmit)(event);
|
|
}}
|
|
className="space-y-5"
|
|
noValidate
|
|
>
|
|
<label className="block">
|
|
<span className="text-sm font-medium text-text">E-Mail</span>
|
|
<input
|
|
type="email"
|
|
className="mt-2 h-12 w-full rounded-lg border border-border bg-white px-4 outline-none transition focus:border-primary"
|
|
{...form.register("email")}
|
|
/>
|
|
{form.formState.errors.email && (
|
|
<p className="mt-2 text-sm text-danger">{form.formState.errors.email.message}</p>
|
|
)}
|
|
</label>
|
|
<label className="block">
|
|
<span className="text-sm font-medium text-text">Passwort</span>
|
|
<input
|
|
type="password"
|
|
className="mt-2 h-12 w-full rounded-lg border border-border bg-white px-4 outline-none transition focus:border-primary"
|
|
{...form.register("password")}
|
|
/>
|
|
{form.formState.errors.password && (
|
|
<p className="mt-2 text-sm text-danger">{form.formState.errors.password.message}</p>
|
|
)}
|
|
</label>
|
|
{toast && <div className="rounded-lg border border-danger/30 bg-white px-4 py-3 text-sm text-danger shadow-soft">{toast}</div>}
|
|
{success && <div className="rounded-lg border border-success/30 bg-white px-4 py-3 text-sm text-success shadow-soft">{success}</div>}
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary px-5 font-semibold text-white shadow-soft transition hover:bg-primary-dark disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
{loading ? <span className="spinner" /> : <LogIn className="h-5 w-5" />}
|
|
<span>{loading ? "Anmeldung läuft…" : "Anmelden"}</span>
|
|
</button>
|
|
</form>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
export default function LoginPage() {
|
|
return (
|
|
<AuthProvider>
|
|
<LoginPanel />
|
|
</AuthProvider>
|
|
);
|
|
}
|