Add Better Auth admin authentication

This commit is contained in:
2026-06-04 12:05:07 +02:00
parent 0f10bd6400
commit e660ec24aa
41 changed files with 2225 additions and 284 deletions

View File

@@ -1,9 +1,41 @@
import { ArrowRight, LockKeyhole, UserPlus } from "lucide-react";
"use client"
import { type FormEvent, useState } from "react";
import { useRouter } from "next/navigation";
import { ArrowRight, LockKeyhole } from "lucide-react";
import { signInMock, signUpMock } from "@/app/actions/auth";
import { authClient } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
export function AuthEntry() {
const [error, setError] = useState<string | null>(null);
const [pending, setPending] = useState(false);
const router = useRouter();
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
setPending(true);
const formData = new FormData(event.currentTarget);
const email = String(formData.get("email") ?? "");
const password = String(formData.get("password") ?? "");
const result = await authClient.signIn.email({
email,
password,
});
setPending(false);
if (result.error) {
setError(result.error.message ?? "Authentifizierung fehlgeschlagen.");
return;
}
router.replace("/dashboard");
router.refresh();
}
return (
<main className="flex min-h-dvh items-center justify-center bg-background px-6 py-10">
<section className="grid w-full max-w-5xl overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm md:grid-cols-[1.05fr_0.95fr]">
@@ -19,8 +51,9 @@ export function AuthEntry() {
Lokale Webdesign-Leads recherchieren, auditieren und freigeben.
</h1>
<p className="mt-4 max-w-lg text-sm leading-6 text-muted-foreground sm:text-base">
Melde dich an, um Kampagnen, Lead-Qualitaet, Audit-Freigaben und
Outreach-Schritte in einem Arbeitsbereich zu steuern.
Melde dich mit dem Admin-Konto an, um Kampagnen, Lead-Qualitaet,
Audit-Freigaben und Outreach-Schritte in einem Arbeitsbereich zu
steuern.
</p>
</div>
@@ -43,37 +76,56 @@ export function AuthEntry() {
<div className="flex flex-col justify-center p-6 lg:p-8">
<div className="mx-auto w-full max-w-sm">
<h2 className="text-2xl font-semibold tracking-normal">
Sign in oder sign up
Admin Login
</h2>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
Die Authentifizierung ist in TASK-1 noch simuliert. Beide
Aktionen setzen eine lokale Mock-Session und leiten ins Dashboard.
Melde dich mit E-Mail und Passwort an.
</p>
<div className="mt-8 grid gap-3">
<form action={signInMock}>
<Button className="w-full justify-between" size="lg">
<span className="inline-flex items-center gap-2">
<LockKeyhole />
Sign in
</span>
<ArrowRight />
</Button>
</form>
<form action={signUpMock}>
<Button
className="w-full justify-between"
size="lg"
variant="outline"
<form className="mt-8 grid gap-3" onSubmit={handleSubmit}>
<label className="block space-y-2 text-sm font-medium">
<span>E-Mail</span>
<input
name="email"
type="email"
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
autoComplete="email"
required
placeholder="admin@firma.de"
/>
</label>
<label className="block space-y-2 text-sm font-medium">
<span>Passwort</span>
<input
name="password"
type="password"
className="h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
autoComplete="current-password"
required
minLength={8}
placeholder="mindestens 8 Zeichen"
/>
</label>
{error ? (
<p
className="text-sm leading-6 text-destructive"
role="alert"
>
<span className="inline-flex items-center gap-2">
<UserPlus />
Sign up
</span>
<ArrowRight />
</Button>
</form>
</div>
{error}
</p>
) : null}
<Button
className="w-full justify-between"
size="lg"
disabled={pending}
>
<span className="inline-flex items-center gap-2">
<LockKeyhole />
Anmelden
</span>
{pending ? "..." : <ArrowRight />}
</Button>
</form>
</div>
</div>
</section>

View File

@@ -1,8 +1,11 @@
"use client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import type { ReactNode } from "react";
import { authClient } from "@/lib/auth-client";
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!convexUrl) {
@@ -11,6 +14,20 @@ if (!convexUrl) {
const convex = new ConvexReactClient(convexUrl);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
export function ConvexClientProvider({
children,
initialToken,
}: {
children: ReactNode;
initialToken?: string | null;
}) {
return (
<ConvexBetterAuthProvider
client={convex}
authClient={authClient}
initialToken={initialToken}
>
<ConvexProvider client={convex}>{children}</ConvexProvider>
</ConvexBetterAuthProvider>
);
}

View File

@@ -4,14 +4,18 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { LogOut } from "lucide-react";
import { signOutMock } from "@/app/actions/auth";
import { authClient } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
import { dashboardNavigation } from "@/lib/dashboard-navigation";
import type { MockSession } from "@/lib/mock-auth";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
export function DashboardSidebar({ session }: { session: MockSession }) {
export function DashboardSidebar() {
const pathname = usePathname();
const router = useRouter();
const [isSigningOut, setIsSigningOut] = useState(false);
const { data: session, isPending } = authClient.useSession();
return (
<aside className="flex w-full shrink-0 flex-col border-b bg-sidebar text-sidebar-foreground md:sticky md:top-0 md:min-h-dvh md:w-72 md:border-b-0 md:border-r">
@@ -59,17 +63,27 @@ export function DashboardSidebar({ session }: { session: MockSession }) {
<div className="border-t p-3 md:mt-auto">
<div className="mb-3 rounded-lg border bg-background p-3 md:block">
<p className="truncate text-sm font-medium">{session.name}</p>
<p className="truncate text-sm font-medium">
{isPending ? "Lade..." : session?.user?.name ?? "Admin"}
</p>
<p className="truncate text-xs text-muted-foreground">
{session.email}
{session?.user?.email ?? "admin@local"}
</p>
</div>
<form action={signOutMock}>
<Button className="w-full justify-start" variant="outline">
<LogOut />
Sign out
</Button>
</form>
<Button
className="w-full justify-start"
variant="outline"
onClick={async () => {
setIsSigningOut(true);
await authClient.signOut();
router.replace("/login");
router.refresh();
}}
disabled={isSigningOut}
>
<LogOut />
{isSigningOut ? "Abmeldung..." : "Abmelden"}
</Button>
</div>
</aside>
);