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
This commit is contained in:
151
frontend/src/app/(auth)/register/page.tsx
Normal file
151
frontend/src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { api } from "@/lib/api";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [firmName, setFirmName] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const supabase = createClient();
|
||||
|
||||
async function handleRegister(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 1. Create auth user
|
||||
const { data, error: authError } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
setError(authError.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Create tenant via backend (the backend adds the user as owner)
|
||||
if (data.session) {
|
||||
try {
|
||||
await api.post("/tenants", { name: firmName });
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { error?: string };
|
||||
setError(apiErr.error || "Kanzlei konnte nicht erstellt werden");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} else {
|
||||
// Email confirmation required
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
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">
|
||||
Erstellen Sie Ihr Konto
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleRegister} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="firm"
|
||||
className="block text-sm font-medium text-neutral-700"
|
||||
>
|
||||
Kanzleiname
|
||||
</label>
|
||||
<input
|
||||
id="firm"
|
||||
type="text"
|
||||
value={firmName}
|
||||
onChange={(e) => setFirmName(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="Muster & Partner Rechtsanwaelte"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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
|
||||
minLength={8}
|
||||
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"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-neutral-400">Mindestens 8 Zeichen</p>
|
||||
</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 ? "..." : "Konto erstellen"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-sm text-neutral-500">
|
||||
Bereits registriert?{" "}
|
||||
<a
|
||||
href="/login"
|
||||
className="font-medium text-neutral-900 hover:underline"
|
||||
>
|
||||
Anmelden
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user