mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
Merge pull request #4 from Since-3/feature/login-protected-routes
added login and protected routes
This commit is contained in:
commit
47cc3466a0
21 changed files with 589 additions and 137 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"prisma.pinToPrisma6": true
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { requireGuest } from "@/src/lib/auth/getUser";
|
||||
import LoginView from "@/src/modules/auth/ui/views/login-view";
|
||||
|
||||
const Login = () => {
|
||||
export default async function LoginPage() {
|
||||
await requireGuest();
|
||||
return <LoginView />;
|
||||
};
|
||||
|
||||
export default Login;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import RegisterUserView from "@/src/modules/auth/ui/views/register-user-view";
|
||||
import { requireGuest } from "@/src/lib/auth/getUser";
|
||||
|
||||
const RegisterUser = () => {
|
||||
export default async function RegisterUser() {
|
||||
await requireGuest();
|
||||
return <RegisterUserView />;
|
||||
};
|
||||
|
||||
export default RegisterUser;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,71 @@
|
|||
const Dashboard = () => {
|
||||
return <div>Du hast dich erfolgreich eingeloggt</div>;
|
||||
};
|
||||
import { LogoutButton } from "@/src/components/layout/LogoutButton";
|
||||
import { requireAuthWithData } from "@/src/lib/auth/getUser";
|
||||
|
||||
export default Dashboard;
|
||||
export default async function DashboardPage() {
|
||||
const userData = await requireAuthWithData();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-4">Willkommen zurück!</h1>
|
||||
<p className="text-gray-600">
|
||||
Schön, dich wiederzusehen, {userData?.name || userData.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-2">Profil</h2>
|
||||
<p className="text-gray-600 text-sm">Verwalte deine persönlichen Daten</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-2">Trainings</h2>
|
||||
<p className="text-gray-600 text-sm">Deine gebuchten Trainingseinheiten</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-2">Einstellungen</h2>
|
||||
<p className="text-gray-600 text-sm">Passe deine Einstellungen an</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Benutzerdaten</h2>
|
||||
<dl className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Name</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData?.name || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">E-Mail</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData.email}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Geburtsdatum</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{userData?.birthday ? new Date(userData.birthday).toLocaleDateString("de-DE") : "—"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Wohnort</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{userData?.plz && userData?.city ? `${userData.plz} ${userData.city}` : "—"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Geschlecht</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData?.gender || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Registriert seit</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{userData?.createdAt ? new Date(userData.createdAt).toLocaleDateString("de-DE") : "—"}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
31
src/app/api/auth/login/route.ts
Normal file
31
src/app/api/auth/login/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// app/api/auth/login/route.ts
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { email, password } = body;
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ error: "E-Mail und Passwort sind erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("❌ Login Error:", error.message);
|
||||
return NextResponse.json({ error: "E-Mail oder Passwort ist falsch" }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ user: data.user, session: data.session }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error in /api/auth/login:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
import { supabaseServer } from "@/src/lib/supabaseClient";
|
||||
// app/api/auth/register/route.ts
|
||||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
** Handles POST request to register and save a User in the DB
|
||||
*
|
||||
*
|
||||
* @param {Request} request - Incoming HTTP request containing user data in JSON format
|
||||
* @returns {Promise<NextResponse>} - JSON response with exisiting or newly created user
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
|
@ -18,11 +12,13 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: "Pflichtfelder fehlen" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data, error } = await supabaseServer.auth.signUp({
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: "http://localhost:3000/login",
|
||||
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"}/login`,
|
||||
data: {
|
||||
name,
|
||||
gender,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import type { Metadata } from "next";
|
|||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
import { AuthInitializer } from "../components/AuthInitializer";
|
||||
|
||||
const salmondFont = localFont({
|
||||
src: "../public/font/salmond.woff",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
title: "Kumasy",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export default function RootLayout({
|
|||
return (
|
||||
<html lang="en" className={salmondFont.className}>
|
||||
<body className={`antialiased`}>
|
||||
<AuthInitializer />
|
||||
<main>{children}</main>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
|
|
|
|||
21
src/components/AuthInitializer.tsx
Normal file
21
src/components/AuthInitializer.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useAuthStore } from "@/src/store/authStore";
|
||||
|
||||
export function AuthInitializer() {
|
||||
const initAuth = useAuthStore((state) => state.initAuth);
|
||||
const initialized = useAuthStore((state) => state.initialized);
|
||||
const hasRun = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent double initialization in development (React StrictMode)
|
||||
if (hasRun.current || initialized) return;
|
||||
|
||||
hasRun.current = true;
|
||||
console.log("🎬 [INIT] AuthInitializer mounting");
|
||||
initAuth();
|
||||
}, [initAuth, initialized]);
|
||||
|
||||
return null;
|
||||
}
|
||||
14
src/components/layout/LogoutButton.tsx
Normal file
14
src/components/layout/LogoutButton.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { useAuthStore } from "@/src/store/authStore";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
|
||||
export function LogoutButton() {
|
||||
const signOut = useAuthStore((state) => state.signOut);
|
||||
|
||||
return (
|
||||
<Button onClick={signOut} variant="destructive" size="sm">
|
||||
Logout
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
122
src/hooks/useAuth.ts
Normal file
122
src/hooks/useAuth.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { userRegisterSchema } from "../modules/auth/validation/userRegisterSchema";
|
||||
import { toast } from "sonner";
|
||||
import { ZodError } from "zod";
|
||||
import { userLoginSchema } from "../modules/auth/validation/userLoginSchema";
|
||||
import { createClient } from "@/src/lib/supabase/client";
|
||||
import { useAuthStore } from "@/src/store/authStore";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { RegisterFormData } from "../types/auth";
|
||||
|
||||
export const useAuth = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const supabase = createClient();
|
||||
const setUser = useAuthStore((state) => state.setUser);
|
||||
const router = useRouter();
|
||||
|
||||
const registerUser = async (formData: RegisterFormData) => {
|
||||
setErrors({});
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
userRegisterSchema.parse(formData);
|
||||
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
name: formData.fullName,
|
||||
birthday: formData.birthday,
|
||||
plz: formData.plz,
|
||||
city: formData.place,
|
||||
street: formData.street,
|
||||
houseNumber: "",
|
||||
gender: formData.gender,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
setErrors({ email: data.error || "Registrierung fehlgeschlagen" });
|
||||
toast.error(data.error || "Registrierung fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
|
||||
toast.success("Bitte bestätige deine E-Mail, um die Registrierung abzuschließen.");
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
const path = issue.path[0];
|
||||
if (typeof path === "string") fieldErrors[path] = issue.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return false;
|
||||
}
|
||||
toast.error("Ein unbekannter Fehler ist aufgetreten.");
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loginUser = async (email: string, password: string, remember: boolean) => {
|
||||
setErrors({});
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
userLoginSchema.parse({ email, password });
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setErrors({ email: "E-Mail oder Passwort ist falsch" });
|
||||
toast.error("E-Mail oder Passwort ist falsch");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update Zustand store
|
||||
setUser(data.user);
|
||||
|
||||
toast.success("Erfolgreich eingeloggt 🎉");
|
||||
|
||||
// Navigate and refresh
|
||||
router.push("/dashboard");
|
||||
router.refresh();
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
const path = issue.path[0];
|
||||
if (typeof path === "string") fieldErrors[path] = issue.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return false;
|
||||
}
|
||||
toast.error("Ein unerwarteter Fehler ist aufgetreten.");
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
errors,
|
||||
registerUser,
|
||||
loginUser,
|
||||
setErrors,
|
||||
};
|
||||
};
|
||||
109
src/lib/auth/getUser.ts
Normal file
109
src/lib/auth/getUser.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { createClient } from "@/src/lib/supabase/server";
|
||||
import { prisma } from "@/src/lib/prisma";
|
||||
import { cache } from "react";
|
||||
|
||||
// Define the user data type
|
||||
export type UserData = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
birthday: Date | null;
|
||||
plz: string | null;
|
||||
city: string | null;
|
||||
street: string | null;
|
||||
houseNumber: string | null;
|
||||
gender: string | null;
|
||||
pbSrc: string | null;
|
||||
createdAt: Date | null;
|
||||
};
|
||||
|
||||
// Cache entry type
|
||||
type CacheEntry = {
|
||||
data: UserData;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
// Simple in-memory cache with timestamp
|
||||
const userDataCache = new Map<string, CacheEntry>();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
let userDataCallCount = 0;
|
||||
|
||||
// Cache Supabase user (per request)
|
||||
export const getUser = cache(async () => {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
console.log("🔐 [AUTH] getUser called - User:", user?.email || "none");
|
||||
return user;
|
||||
});
|
||||
|
||||
// Cache user data from database with time-based cache
|
||||
export const getUserData = cache(async (): Promise<UserData | null> => {
|
||||
userDataCallCount++;
|
||||
console.log(`📊 [DB] getUserData called (count: ${userDataCallCount})`);
|
||||
|
||||
const user = await getUser();
|
||||
if (!user) return null;
|
||||
|
||||
// Check in-memory cache
|
||||
const cached = userDataCache.get(user.id);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && now - cached.timestamp < CACHE_TTL) {
|
||||
console.log(
|
||||
`⚡ [CACHE] Using cached data (${Math.round((now - cached.timestamp) / 1000)}s old)`
|
||||
);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
console.log("🗄️ [DB] Fetching user data from Prisma for:", user.email);
|
||||
|
||||
const userData = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
birthday: true,
|
||||
plz: true,
|
||||
city: true,
|
||||
street: true,
|
||||
houseNumber: true,
|
||||
gender: true,
|
||||
pbSrc: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ [DB] User data fetched:", userData?.name);
|
||||
|
||||
// Store in cache
|
||||
if (userData) {
|
||||
userDataCache.set(user.id, { data: userData, timestamp: now });
|
||||
}
|
||||
|
||||
return userData;
|
||||
});
|
||||
|
||||
export async function requireAuth() {
|
||||
const user = await getUser();
|
||||
return user!;
|
||||
}
|
||||
|
||||
export async function requireAuthWithData() {
|
||||
const userData = await getUserData();
|
||||
if (!userData) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return userData;
|
||||
}
|
||||
|
||||
export async function requireGuest() {
|
||||
const user = await getUser();
|
||||
if (user) {
|
||||
const { redirect } = await import("next/navigation");
|
||||
redirect("/dashboard");
|
||||
}
|
||||
}
|
||||
8
src/lib/supabase/client.ts
Normal file
8
src/lib/supabase/client.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { createBrowserClient } from "@supabase/ssr";
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
}
|
||||
27
src/lib/supabase/server.ts
Normal file
27
src/lib/supabase/server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
);
|
||||
} catch {
|
||||
// Server Component - ignore
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
export const supabaseBrowser = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
||||
throw new Error(
|
||||
"❌ Missing Supabase environment variables! Make sure NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are set."
|
||||
);
|
||||
}
|
||||
|
||||
export const supabaseServer = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
58
src/middleware.ts
Normal file
58
src/middleware.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// middleware.ts (at root level)
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({ request });
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
||||
supabaseResponse = NextResponse.next({ request });
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
// Protected routes
|
||||
const protectedRoutes = ["/dashboard", "/profile", "/trainings"];
|
||||
const isProtectedRoute = protectedRoutes.some((route) =>
|
||||
request.nextUrl.pathname.startsWith(route)
|
||||
);
|
||||
|
||||
if (!user && isProtectedRoute) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// Auth routes (redirect if already logged in)
|
||||
const authRoutes = ["/login", "/register"];
|
||||
const isAuthRoute = authRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
|
||||
|
||||
if (user && isAuthRoute) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/dashboard";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
return supabaseResponse;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
|
||||
};
|
||||
|
|
@ -8,49 +8,20 @@ import { Label } from "@/src/components/ui/label";
|
|||
import { Checkbox } from "@/src/components/ui/checkbox";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { supabaseBrowser } from "@/src/lib/supabaseClient";
|
||||
import { Spinner } from "@/src/components/ui/spinner";
|
||||
import { useAuth } from "@/src/hooks/useAuth";
|
||||
|
||||
const LoginView = () => {
|
||||
//TODO: RIGHT LOGIN with Schema etc.
|
||||
const { loginUser, errors, loading } = useAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { data, error } = await supabaseBrowser.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("❌ Login Error:", error.message);
|
||||
setError("E-Mail oder Passwort ist falsch");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: Session speichern
|
||||
if (remember) {
|
||||
localStorage.setItem("since3_remember_me", "true");
|
||||
} else {
|
||||
localStorage.removeItem("since3_remember_me");
|
||||
}
|
||||
|
||||
// Redirect nach Login
|
||||
const success = await loginUser(email, password, remember);
|
||||
if (success) {
|
||||
router.push("/dashboard");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("Ein unerwarteter Fehler ist aufgetreten");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -79,6 +50,7 @@ const LoginView = () => {
|
|||
id="login-email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
error={errors.email}
|
||||
/>
|
||||
<InputComponent
|
||||
isLabel
|
||||
|
|
@ -87,10 +59,11 @@ const LoginView = () => {
|
|||
id="login-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={errors.password}
|
||||
/>
|
||||
|
||||
<Button onClick={handleLogin} disabled={loading} className="w-full mt-6">
|
||||
{loading ? <Spinner /> : "Weiter"}
|
||||
{loading ? <Spinner /> : "Anmelden"}
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between w-full mt-6 gap-4 text-sm">
|
||||
|
|
@ -119,7 +92,7 @@ const LoginView = () => {
|
|||
<p>Ich habe noch keinen Account</p>
|
||||
<p
|
||||
className="text-yellow hover:underline cursor-pointer"
|
||||
onClick={() => router.push("/register")}
|
||||
onClick={() => router.push("/register/user")}
|
||||
>
|
||||
Sign Up
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@ import { Button } from "@/src/components/ui/button";
|
|||
import AuthButton from "../components/AuthButton";
|
||||
import { FaGoogle, FaFacebook } from "react-icons/fa";
|
||||
import { useAuthFormStore } from "../../store/authFormState";
|
||||
import { userRegisterSchema, userRegisterStep1Schema } from "../../validation/userRegisterSchema";
|
||||
import { userRegisterStep1Schema } from "../../validation/userRegisterSchema";
|
||||
import { ZodError } from "zod";
|
||||
import DropdownComponent from "@/src/components/layout/DropdownComponent";
|
||||
import { Spinner } from "@/src/components/ui/spinner";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/src/hooks/useAuth";
|
||||
|
||||
const RegisterUserView = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { registerUser, loading, errors, setErrors } = useAuth();
|
||||
const router = useRouter();
|
||||
const formData = useAuthFormStore();
|
||||
|
||||
|
|
@ -51,69 +50,26 @@ const RegisterUserView = () => {
|
|||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
setErrors({});
|
||||
setLoading(true);
|
||||
try {
|
||||
userRegisterSchema.parse({
|
||||
fullName: formData.fullName,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
passwordConfirm: formData.passwordConfirm,
|
||||
birthday: formData.birthday,
|
||||
plz: formData.plz,
|
||||
place: formData.place,
|
||||
street: formData.street,
|
||||
gender: formData.gender,
|
||||
});
|
||||
const success = await registerUser({
|
||||
fullName: formData.fullName,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
passwordConfirm: formData.passwordConfirm,
|
||||
birthday: formData.birthday,
|
||||
plz: formData.plz,
|
||||
place: formData.place,
|
||||
street: formData.street,
|
||||
gender: formData.gender,
|
||||
});
|
||||
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
name: formData.fullName,
|
||||
birthday: formData.birthday,
|
||||
plz: formData.plz,
|
||||
city: formData.place,
|
||||
street: formData.street,
|
||||
houseNumber: "",
|
||||
gender: formData.gender,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrors({ email: data.error || "Registrierung fehlgeschlagen" });
|
||||
return;
|
||||
}
|
||||
|
||||
toast.info("Bitte Email bestätigen für erfolgreiche Registrierung");
|
||||
if (success) {
|
||||
formData.resetForm();
|
||||
setStep(1);
|
||||
setErrors({});
|
||||
|
||||
router.push("/login");
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
const path = issue.path[0];
|
||||
if (typeof path === "string") {
|
||||
fieldErrors[path] = issue.message;
|
||||
}
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
} else {
|
||||
toast.error(`Registrierung fehlgeschlagen: ${err}`);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screena">
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar */}
|
||||
<div className="hidden lg:flex lg:w-1/6 ">
|
||||
<div className="fixed p-4 left-0 top-0 h-screen w-1/2 z-10">
|
||||
|
|
@ -173,7 +129,7 @@ const RegisterUserView = () => {
|
|||
/>
|
||||
|
||||
<Button className="w-full mt-6" onClick={handleNext}>
|
||||
Weiter
|
||||
{loading ? <Spinner /> : "Weiter"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
8
src/modules/auth/validation/userLoginSchema.ts
Normal file
8
src/modules/auth/validation/userLoginSchema.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const userLoginSchema = z.object({
|
||||
email: z.string().email("Ungültige E-Mail-Adresse"),
|
||||
password: z.string().min(8, "Passwort muss mindestens 8 Zeichen enthalten"),
|
||||
});
|
||||
|
||||
export type UserLoginSchema = z.infer<typeof userLoginSchema>;
|
||||
60
src/store/authStore.ts
Normal file
60
src/store/authStore.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// src/store/authStore.ts
|
||||
import { create } from "zustand";
|
||||
import { createClient } from "@/src/lib/supabase/client";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
initialized: boolean;
|
||||
setUser: (user: User | null) => void;
|
||||
initAuth: () => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
user: null,
|
||||
loading: true,
|
||||
initialized: false,
|
||||
|
||||
setUser: (user) => {
|
||||
console.log("👤 [ZUSTAND] setUser:", user?.email || "null");
|
||||
set({ user, loading: false });
|
||||
},
|
||||
|
||||
initAuth: async () => {
|
||||
// Prevent multiple initializations
|
||||
if (get().initialized) {
|
||||
console.log("⏭️ [ZUSTAND] Already initialized, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🚀 [ZUSTAND] initAuth starting");
|
||||
const supabase = createClient();
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
console.log("🔐 [ZUSTAND] Initial session:", session?.user?.email || "none");
|
||||
set({ user: session?.user ?? null, loading: false, initialized: true });
|
||||
|
||||
// Listen for auth changes (login/logout only)
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
console.log("🔄 [ZUSTAND] Auth event:", event, session?.user?.email || "none");
|
||||
|
||||
// Only update on actual auth changes, not on token refresh
|
||||
if (event === "SIGNED_IN" || event === "SIGNED_OUT" || event === "USER_UPDATED") {
|
||||
set({ user: session?.user ?? null, loading: false });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
signOut: async () => {
|
||||
console.log("👋 [ZUSTAND] Signing out");
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signOut();
|
||||
set({ user: null });
|
||||
window.location.href = "/login";
|
||||
},
|
||||
}));
|
||||
16
src/types/auth.ts
Normal file
16
src/types/auth.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export interface RegisterFormData {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
birthday: string;
|
||||
plz: string;
|
||||
place: string;
|
||||
street: string;
|
||||
gender: string;
|
||||
}
|
||||
|
||||
export interface LoginFormData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue