From d2bb39d8153bfa966066e981bb263ce8fef556d6 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Fri, 17 Jul 2026 18:18:34 +0200 Subject: [PATCH] Auth: Login mit E-Mail/Passwort und Passkey, Logout --- app/login/page.tsx | 135 ++++++++++++++++++++++++++++++++++++++++++++ app/logout/route.ts | 8 +++ 2 files changed, 143 insertions(+) create mode 100644 app/login/page.tsx create mode 100644 app/logout/route.ts diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..1c73697 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { createClient } from "@/lib/supabase/client"; + +function germanAuthError(message: string): string { + if (/invalid login credentials/i.test(message)) + return "E-Mail-Adresse oder Passwort ist falsch."; + if (/email not confirmed/i.test(message)) + return "Die E-Mail-Adresse wurde noch nicht bestätigt."; + if (/cancelled|aborted|NotAllowedError/i.test(message)) + return "Passkey-Anmeldung wurde abgebrochen."; + return `Anmeldung fehlgeschlagen: ${message}`; +} + +export default function LoginPage() { + const router = useRouter(); + const supabase = createClient(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + async function redirectByRole(userId: string) { + const { data: profile } = await supabase + .from("profiles") + .select("role") + .eq("id", userId) + .single(); + router.replace(profile?.role === "admin" ? "/admin" : "/intern"); + router.refresh(); + } + + async function signInWithPassword(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setBusy(true); + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error || !data.user) { + setError(germanAuthError(error?.message ?? "Unbekannter Fehler")); + setBusy(false); + return; + } + await redirectByRole(data.user.id); + } + + async function signInWithPasskey() { + setError(null); + setBusy(true); + try { + const { data, error } = await supabase.auth.signInWithPasskey(); + if (error || !data?.user) { + setError( + germanAuthError(error?.message ?? "Passkey-Anmeldung fehlgeschlagen") + ); + setBusy(false); + return; + } + await redirectByRole(data.user.id); + } catch (err) { + setError(germanAuthError(err instanceof Error ? err.message : String(err))); + setBusy(false); + } + } + + return ( +
+
+

+ Anmeldung +

+
+
+ + setEmail(e.target.value)} + autoComplete="username webauthn" + required + /> +
+
+ + setPassword(e.target.value)} + autoComplete="current-password" + required + /> +
+ {error &&

{error}

} + +
+ +
+ + oder + +
+ + + +

+ + ← Zur Speisekarte + +

+
+
+ ); +} diff --git a/app/logout/route.ts b/app/logout/route.ts new file mode 100644 index 0000000..ad221c5 --- /dev/null +++ b/app/logout/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from "next/server"; +import { createClient } from "@/lib/supabase/server"; + +export async function POST(request: Request) { + const supabase = await createClient(); + await supabase.auth.signOut(); + return NextResponse.redirect(new URL("/login", request.url), { status: 303 }); +}