mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
Merge pull request #8 from Since-3/feature/reset-password
added reset-password feature
This commit is contained in:
commit
a746685f6e
6 changed files with 279 additions and 9 deletions
7
src/app/(auth)/forgot-password/page.tsx
Normal file
7
src/app/(auth)/forgot-password/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { requireGuest } from "@/src/lib/auth/getUser";
|
||||||
|
import ForgotPasswordView from "@/src/modules/auth/ui/views/forgot-password-view";
|
||||||
|
|
||||||
|
export default async function ForgotPasswordPage() {
|
||||||
|
await requireGuest();
|
||||||
|
return <ForgotPasswordView />;
|
||||||
|
}
|
||||||
5
src/app/(auth)/reset-password/page.tsx
Normal file
5
src/app/(auth)/reset-password/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ResetPasswordView from "@/src/modules/auth/ui/views/reset-password-view";
|
||||||
|
|
||||||
|
export default async function ResetPasswordPage() {
|
||||||
|
return <ResetPasswordView />;
|
||||||
|
}
|
||||||
|
|
@ -5,15 +5,16 @@ import { NextResponse, type NextRequest } from "next/server";
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
// Protected routes
|
// Protected routes
|
||||||
const protectedRoutes = ["/dashboard", "/profile", "/trainings", "/courses"];
|
const protectedRoutes = ["/dashboard", "/profile", "/trainings", "/courses"];
|
||||||
const authRoutes = ["/login", "/register"];
|
const authRoutes = ["/login", "/register", "/forgot-password"];
|
||||||
|
|
||||||
const isProtectedRoute = protectedRoutes.some((route) =>
|
const isProtectedRoute = protectedRoutes.some((route) =>
|
||||||
request.nextUrl.pathname.startsWith(route)
|
request.nextUrl.pathname.startsWith(route)
|
||||||
);
|
);
|
||||||
const isAuthRoute = authRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
|
const isAuthRoute = authRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
|
||||||
|
const isPasswordReset = request.nextUrl.pathname.startsWith("/reset-password");
|
||||||
|
|
||||||
// Skip auth check if not a protected or auth route
|
// Skip auth check if not a protected, auth, or reset route
|
||||||
if (!isProtectedRoute && !isAuthRoute) {
|
if (!isProtectedRoute && !isAuthRoute && !isPasswordReset) {
|
||||||
return NextResponse.next({ request });
|
return NextResponse.next({ request });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +49,8 @@ export async function middleware(request: NextRequest) {
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user && isAuthRoute) {
|
// Redirect logged-in users away from auth routes (except password reset)
|
||||||
|
if (user && isAuthRoute && !isPasswordReset) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
url.pathname = "/dashboard";
|
url.pathname = "/dashboard";
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
|
|
|
||||||
110
src/modules/auth/ui/views/forgot-password-view.tsx
Normal file
110
src/modules/auth/ui/views/forgot-password-view.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
"use client";
|
||||||
|
import InputComponent from "@/src/components/layout/InputComponent";
|
||||||
|
import AuthSidebarComponent from "../components/AuthSidebarComponent";
|
||||||
|
import { Button } from "@/src/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Spinner } from "@/src/components/ui/spinner";
|
||||||
|
import { createClient } from "@/src/lib/supabase/client";
|
||||||
|
|
||||||
|
const ForgotPasswordView = () => {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [emailError, setEmailError] = useState("");
|
||||||
|
const [emailSent, setEmailSent] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
setEmailError("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
setEmailError("Bitte E-Mail-Adresse eingeben");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||||
|
redirectTo: `${window.location.origin}/reset-password`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setEmailError("E-Mail konnte nicht gesendet werden");
|
||||||
|
} else {
|
||||||
|
setEmailSent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<AuthSidebarComponent
|
||||||
|
title="Passwort zurücksetzen"
|
||||||
|
description="Keine Sorge, wir helfen Ihnen dabei, wieder Zugang zu Ihrem Konto zu erhalten."
|
||||||
|
footer="info@since3.de"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Forgot Password Formular */}
|
||||||
|
<div className="w-full lg:w-5/6 lg:ml-auto flex justify-center items-center min-h-screen px-4">
|
||||||
|
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
|
||||||
|
<h1 className="text-2xl font-extrabold text-blue mb-8">Passwort vergessen?</h1>
|
||||||
|
|
||||||
|
{!emailSent ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen
|
||||||
|
Ihres Passworts.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<InputComponent
|
||||||
|
isLabel
|
||||||
|
label="E-mail Adresse"
|
||||||
|
type="email"
|
||||||
|
id="forgot-email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
error={emailError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleReset} disabled={loading} className="w-full mt-6">
|
||||||
|
{loading ? <Spinner /> : "Link zum Zurücksetzen senden"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mt-4">
|
||||||
|
<h2 className="text-lg font-semibold text-green-800 mb-2">E-Mail wurde versendet!</h2>
|
||||||
|
<p className="text-sm text-green-700">
|
||||||
|
Wir haben Ihnen einen Link zum Zurücksetzen Ihres Passworts an{" "}
|
||||||
|
<strong>{email}</strong> gesendet. Bitte überprüfen Sie Ihr Postfach und folgen Sie
|
||||||
|
den Anweisungen in der E-Mail.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-green-600 mt-4">
|
||||||
|
Hinweis: Der Link ist 60 Minuten gültig. Falls Sie keine E-Mail erhalten haben,
|
||||||
|
überprüfen Sie bitte Ihren Spam-Ordner.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 items-center mt-8 text-sm">
|
||||||
|
<p>Zurück zum</p>
|
||||||
|
<p
|
||||||
|
className="text-yellow hover:underline cursor-pointer"
|
||||||
|
onClick={() => router.push("/login")}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForgotPasswordView;
|
||||||
|
|
@ -62,10 +62,6 @@ const LoginView = () => {
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleLogin} disabled={loading} className="w-full mt-6">
|
|
||||||
{loading ? <Spinner /> : "Anmelden"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between w-full mt-6 gap-4 text-sm">
|
<div className="flex flex-col sm:flex-row sm:justify-between w-full mt-6 gap-4 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -78,9 +74,18 @@ const LoginView = () => {
|
||||||
Beim nächsten Mal an mich erinnern
|
Beim nächsten Mal an mich erinnern
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<p className="hover:underline cursor-pointer text-blue">Passwort vergessen?</p>
|
<p
|
||||||
|
className="hover:underline cursor-pointer text-blue"
|
||||||
|
onClick={() => router.push("/forgot-password")}
|
||||||
|
>
|
||||||
|
Passwort vergessen?
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleLogin} disabled={loading} className="w-full mt-6">
|
||||||
|
{loading ? <Spinner /> : "Anmelden"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<hr className="mt-6 border-blue" />
|
<hr className="mt-6 border-blue" />
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 mt-4">
|
<div className="flex flex-col sm:flex-row gap-4 mt-4">
|
||||||
|
|
|
||||||
141
src/modules/auth/ui/views/reset-password-view.tsx
Normal file
141
src/modules/auth/ui/views/reset-password-view.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
"use client";
|
||||||
|
import InputComponent from "@/src/components/layout/InputComponent";
|
||||||
|
import AuthSidebarComponent from "../components/AuthSidebarComponent";
|
||||||
|
import { Button } from "@/src/components/ui/button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Spinner } from "@/src/components/ui/spinner";
|
||||||
|
import { createClient } from "@/src/lib/supabase/client";
|
||||||
|
|
||||||
|
const ResetPasswordView = () => {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [passwordError, setPasswordError] = useState("");
|
||||||
|
const [confirmPasswordError, setConfirmPasswordError] = useState("");
|
||||||
|
const [resetSuccess, setResetSuccess] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
supabase.auth.getUser().then(({ data }) => {
|
||||||
|
if (!data.user) {
|
||||||
|
router.replace("/login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [router, supabase]);
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setPasswordError("");
|
||||||
|
setConfirmPasswordError("");
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
setPasswordError("Mindestens 8 Zeichen");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setConfirmPasswordError("Passwörter stimmen nicht überein");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.auth.updateUser({ password });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setPasswordError("Passwort konnte nicht gesetzt werden");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResetSuccess(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
} catch (signOutError) {
|
||||||
|
console.error("Sign out error:", signOutError);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/login");
|
||||||
|
}, 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<AuthSidebarComponent
|
||||||
|
title="Neues Passwort setzen"
|
||||||
|
description="Wählen Sie ein sicheres Passwort für Ihr Konto."
|
||||||
|
footer="info@since3.de"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reset Password Formular */}
|
||||||
|
<div className="w-full lg:w-5/6 lg:ml-auto flex justify-center items-center min-h-screen px-4">
|
||||||
|
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
|
||||||
|
<h1 className="text-2xl font-extrabold text-blue mb-8">Passwort zurücksetzen</h1>
|
||||||
|
|
||||||
|
{!resetSuccess ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
Bitte geben Sie Ihr neues Passwort ein. Es sollte mindestens 8 Zeichen lang sein.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<InputComponent
|
||||||
|
isLabel
|
||||||
|
label="Neues Passwort"
|
||||||
|
type="password"
|
||||||
|
id="new-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
error={passwordError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputComponent
|
||||||
|
isLabel
|
||||||
|
label="Passwort bestätigen"
|
||||||
|
type="password"
|
||||||
|
id="confirm-password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
error={confirmPasswordError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleResetPassword} disabled={loading} className="w-full mt-6">
|
||||||
|
{loading ? <Spinner /> : "Passwort zurücksetzen"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mt-4">
|
||||||
|
<h2 className="text-lg font-semibold text-green-800 mb-2">
|
||||||
|
Passwort erfolgreich zurückgesetzt!
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-green-700">
|
||||||
|
Ihr Passwort wurde erfolgreich geändert. Sie werden in Kürze zum Login
|
||||||
|
weitergeleitet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 items-center mt-8 text-sm">
|
||||||
|
<p>Zurück zum</p>
|
||||||
|
<p
|
||||||
|
className="text-yellow hover:underline cursor-pointer"
|
||||||
|
onClick={() => router.push("/login")}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordView;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue