Files
KanzlAI-mGMT/frontend/src/app/(auth)/login/page.tsx
m bf225284d8 feat: add frontend auth pages, app layout, and Supabase integration (Phase 1E)
- Auth pages: login (password + magic link), register (with firm name), callback
- Supabase client setup: browser client, server client, middleware for session refresh
- App layout: sidebar (Dashboard, Akten, Fristen, Termine, AI Analyse, Einstellungen),
  header with user info and tenant switcher
- Shared: API client with auth headers, TypeScript types matching Go models,
  QueryClientProvider + Toaster providers
- Dependencies: @supabase/supabase-js, @supabase/ssr, @tanstack/react-query,
  lucide-react, date-fns, sonner
2026-03-25 13:39:16 +01:00

190 lines
5.8 KiB
TypeScript

"use client";
import { createClient } from "@/lib/supabase/client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [mode, setMode] = useState<"password" | "magic">("password");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [magicSent, setMagicSent] = useState(false);
const router = useRouter();
const supabase = createClient();
async function handlePasswordLogin(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
setLoading(false);
return;
}
router.push("/");
router.refresh();
}
async function handleMagicLink(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/callback`,
},
});
if (error) {
setError(error.message);
setLoading(false);
return;
}
setMagicSent(true);
setLoading(false);
}
if (magicSent) {
return (
<div className="flex min-h-screen items-center justify-center bg-neutral-50">
<div className="w-full max-w-sm space-y-6 rounded-lg border border-neutral-200 bg-white p-8">
<div className="text-center">
<h1 className="text-lg font-semibold text-neutral-900">
Link gesendet
</h1>
<p className="mt-2 text-sm text-neutral-500">
Wir haben einen Login-Link an{" "}
<span className="font-medium text-neutral-700">{email}</span>{" "}
gesendet. Bitte pruefen Sie Ihren Posteingang.
</p>
</div>
<button
onClick={() => setMagicSent(false)}
className="w-full text-center text-sm text-neutral-500 hover:text-neutral-700"
>
Zurueck zum Login
</button>
</div>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center bg-neutral-50">
<div className="w-full max-w-sm space-y-6 rounded-lg border border-neutral-200 bg-white p-8">
<div className="text-center">
<h1 className="text-lg font-semibold text-neutral-900">
KanzlAI
</h1>
<p className="mt-1 text-sm text-neutral-500">
Melden Sie sich an
</p>
</div>
<div className="flex rounded-md border border-neutral-200 bg-neutral-50 p-0.5">
<button
onClick={() => setMode("password")}
className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
mode === "password"
? "bg-white text-neutral-900 shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
}`}
>
Passwort
</button>
<button
onClick={() => setMode("magic")}
className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
mode === "magic"
? "bg-white text-neutral-900 shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
}`}
>
Magic Link
</button>
</div>
<form
onSubmit={mode === "password" ? handlePasswordLogin : handleMagicLink}
className="space-y-4"
>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-neutral-700"
>
E-Mail
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm placeholder-neutral-400 focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900"
placeholder="anwalt@kanzlei.de"
/>
</div>
{mode === "password" && (
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-neutral-700"
>
Passwort
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm placeholder-neutral-400 focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900"
/>
</div>
)}
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 disabled:opacity-50"
>
{loading
? "..."
: mode === "password"
? "Anmelden"
: "Link senden"}
</button>
</form>
<p className="text-center text-sm text-neutral-500">
Noch kein Konto?{" "}
<a
href="/register"
className="font-medium text-neutral-900 hover:underline"
>
Registrieren
</a>
</p>
</div>
</div>
);
}