Add forget/reset password

close #50

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-20 16:17:47 +01:00
parent ef8e62d488
commit 7117c6a210
8 changed files with 632 additions and 0 deletions

View File

@@ -52,6 +52,8 @@ const CreateFrameworkPage = lazy(() => import("./pages/CreateFrameworkPage"));
const CreateControlPage = lazy(() => import("./pages/CreateControlPage"));
const UpdateFrameworkPage = lazy(() => import("./pages/UpdateFrameworkPage"));
const UpdateControlPage = lazy(() => import("./pages/UpdateControlPage"));
const ForgotPasswordPage = lazy(() => import("./pages/ForgotPasswordPage"));
const ResetPasswordPage = lazy(() => import("./pages/ResetPasswordPage"));
// Policy pages
const PolicyListPage = lazy(() => import("./pages/PolicyListPage"));
const PolicyOverviewPage = lazy(() => import("./pages/PolicyOverviewPage"));
@@ -296,6 +298,26 @@ function App() {
</Suspense>
}
/>
<Route
path="forgot-password"
element={
<Suspense>
<VisitorErrorBoundaryWithLocation>
<ForgotPasswordPage />
</VisitorErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="reset-password"
element={
<Suspense>
<VisitorErrorBoundaryWithLocation>
<ResetPasswordPage />
</VisitorErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="confirm-email"
element={

View File

@@ -0,0 +1,125 @@
import { useState } from "react";
import { Link } from "react-router";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { buildEndpoint } from "@/utils";
import { useToast } from "@/hooks/use-toast";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const { toast } = useToast();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const response = await fetch(
buildEndpoint("/api/console/v1/auth/forget-password"),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to request password reset");
}
setIsSubmitted(true);
toast({
title: "Check your email",
description: "If an account exists, you'll receive reset instructions",
});
} catch (error) {
toast({
title: "Error",
description:
error instanceof Error ? error.message : "An error occurred",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
return (
<>
<Helmet>
<title>Forgot Password - Probo</title>
</Helmet>
<div className="flex flex-col items-center justify-center min-h-[70vh] p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-bold text-center">
Forgot Password
</CardTitle>
<CardDescription className="text-center">
Enter your email address and we{"'"}ll send you a link to reset
your password
</CardDescription>
</CardHeader>
<CardContent>
{isSubmitted ? (
<div className="space-y-4">
<div className="p-3 text-sm text-green-600 bg-green-50 dark:bg-green-900/20 dark:text-green-400 rounded-md">
Check your email for instructions to reset your password. If
you don{"'"}t see it, check your spam folder.
</div>
<Button asChild className="w-full">
<Link to="/login">Return to Login</Link>
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Sending..." : "Send Reset Link"}
</Button>
</form>
)}
</CardContent>
<CardFooter className="flex justify-center">
<Link
to="/login"
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Back to Login
</Link>
</CardFooter>
</Card>
</div>
</>
);
}

View File

@@ -0,0 +1,205 @@
import { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { buildEndpoint } from "@/utils";
import { useToast } from "@/hooks/use-toast";
import { Link } from "react-router";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export default function ResetPasswordPage() {
const [isLoading, setIsLoading] = useState(false);
const [isReset, setIsReset] = useState(false);
const [error, setError] = useState<string | null>(null);
const [token, setToken] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [confirmPassword, setConfirmPassword] = useState<string>("");
const location = useLocation();
const navigate = useNavigate();
const { toast } = useToast();
useEffect(() => {
// Extract token from URL and prefill the form
const searchParams = new URLSearchParams(location.search);
const urlToken = searchParams.get("token");
if (urlToken) {
setToken(urlToken);
}
}, [location.search]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);
if (!token.trim()) {
setError("Please enter a reset token");
setIsLoading(false);
return;
}
if (!password) {
setError("Please enter a password");
setIsLoading(false);
return;
}
if (password !== confirmPassword) {
setError("Passwords do not match");
setIsLoading(false);
return;
}
if (password.length < 8) {
setError("Password must be at least 8 characters long");
setIsLoading(false);
return;
}
try {
const response = await fetch(
buildEndpoint("/api/console/v1/auth/reset-password"),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token: token.trim(),
password: password,
}),
}
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to reset password");
}
setIsReset(true);
toast({
title: "Success",
description: "Your password has been reset successfully",
});
} catch (error) {
setError(
error instanceof Error
? error.message
: "Failed to reset password. Please try again."
);
} finally {
setIsLoading(false);
}
};
return (
<>
<Helmet>
<title>Reset Password - Probo</title>
</Helmet>
<div className="flex flex-col items-center justify-center min-h-[70vh] p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-bold text-center">
Reset Password
</CardTitle>
<CardDescription className="text-center">
Enter your new password below
</CardDescription>
</CardHeader>
<CardContent>
{isReset ? (
<div className="space-y-4 text-center">
<p className="text-green-600 dark:text-green-400">
Your password has been reset successfully!
</p>
<Button onClick={() => navigate("/login")} className="w-full">
Proceed to Login
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 rounded-md">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="token">Reset Token</Label>
<Input
id="token"
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter your reset token"
disabled={isLoading}
required
/>
<p className="text-xs text-gray-500 dark:text-gray-400">
The token has been automatically filled from the URL if
available
</p>
</div>
<div className="space-y-2">
<Label htmlFor="password">New Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter new password"
disabled={isLoading}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm New Password</Label>
<Input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm your new password"
disabled={isLoading}
required
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Resetting..." : "Reset Password"}
</Button>
</form>
)}
</CardContent>
<CardFooter className="flex justify-center">
{!isReset && (
<Link
to="/login"
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Back to Login
</Link>
)}
</CardFooter>
</Card>
</div>
</>
);
}