forgot password fix

This commit is contained in:
dimitar 2025-04-07 06:06:42 +02:00
parent 3338209fe5
commit 1e38bd67a0
6 changed files with 123 additions and 48 deletions

View File

@ -181,9 +181,13 @@ export class AuthController {
return this.authService.resetPasswordWithToken(token, newPassword); return this.authService.resetPasswordWithToken(token, newPassword);
} }
// @Post("logout") @Post("validate-token")
// async logout(@Request() req) { async validateToken(@Body() { token }: { token: string }) {
// await this.authService.logout(req.user.userId); if (!token || typeof token !== "string") {
// return { message: "Logged out successfully" }; this.logger.warn("Invalid token input received");
// } throw new BadRequestException("A valid token is required");
}
return this.authService.validateToken(token);
}
} }

View File

@ -327,7 +327,8 @@ export class AuthService {
if (!resetRecord) { if (!resetRecord) {
this.logger.warn("Invalid or expired reset token used"); this.logger.warn("Invalid or expired reset token used");
throw new UnauthorizedException("Invalid or expired reset token"); // throw new UnauthorizedException("Invalid or expired reset token");
return { isValid: false, message: "Invalid or expired token" };
} }
// Hash new password // Hash new password
@ -365,7 +366,7 @@ export class AuthService {
}); });
} }
return { message: "Password reset successful" }; return { isValid: true, message: "Password reset successful" };
} catch (error) { } catch (error) {
this.logger.error("Error in resetPasswordWithToken:", { this.logger.error("Error in resetPasswordWithToken:", {
error: error.message, error: error.message,
@ -376,8 +377,28 @@ export class AuthService {
} }
} }
async validateToken(token: string): Promise<{ isValid: boolean }> {
try {
const resetRecord = await this.prisma.passwordReset.findFirst({
where: {
token,
expiresAt: {
gt: new Date(),
},
used: false,
},
});
if (!resetRecord) {
return { isValid: false };
}
return { isValid: true };
} catch (error) {
return { isValid: false };
}
}
async logout(userId: string): Promise<void> { async logout(userId: string): Promise<void> {
return console.log("User logged out:", userId);
this.logger.log("User logged out:", userId); this.logger.log("User logged out:", userId);
return console.log("User logged out:", userId);
} }
} }

View File

@ -227,9 +227,9 @@ export class EmailService {
async sendPasswordResetEmail(email: string, name: string, token: string) { async sendPasswordResetEmail(email: string, name: string, token: string) {
this.logger.log("Sending password reset email to:", email); this.logger.log("Sending password reset email to:", email);
const frontendUrl = const frontendUrl = "http://localhost:5173";
this.configService.get<string>("FRONTEND_URL") || "https://placebo.mk"; // this.configService.get<string>("FRONTEND_URL") || "http://localhost:5147";
const resetLink = `${frontendUrl}/forgot-password?token=${token}`; const resetLink = `${frontendUrl}/reset-password?token=${token}`;
const mailOptions = { const mailOptions = {
from: this.from, from: this.from,
to: email, to: email,

View File

@ -1,64 +1,97 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useNavigate, useSearchParams, Link } from 'react-router-dom'; import { useNavigate, useSearchParams, Link } from "react-router-dom";
import { resetPassword } from '../../services/api'; import { resetPassword } from "../../services/api";
import { useAuth } from "../../hooks/useAuth";
export default function ResetPassword() { export default function ResetPassword() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { validateToken } = useAuth(); // Use validateToken from useAuth
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
password: '', password: "",
confirmPassword: '', confirmPassword: "",
}); });
const [status, setStatus] = useState({ type: '', message: '' }); const [status, setStatus] = useState({ type: "", message: "" });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const token = searchParams.get('token'); const token = searchParams.get("token");
useEffect(() => { useEffect(() => {
const checkToken = async () => {
console.log("token received from validate-token ", token);
if (!token) { if (!token) {
navigate('/forgot-password'); console.log("no token found");
navigate("/forgot-password");
return;
} }
}, [token, navigate]); try {
const isValid = await validateToken(token); // Validate token using useAuth
console.log("token validity", isValid);
if (!isValid) {
setStatus({
type: "error",
message:
"Invalid or expired token. Please request a new reset link.",
});
setTimeout(() => navigate("/forgot-password"), 3000);
}
} catch {
console.error("error during token validation", error);
}
};
checkToken();
}, [token, navigate, validateToken]);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
setStatus({ setStatus({
type: 'error', type: "error",
message: 'Passwords do not match' message: "Passwords do not match",
});
return;
}
if (formData.password.length < 6) {
setStatus({
type: 'error',
message: 'Password must be at least 6 characters long'
}); });
return; return;
} }
// if (!isStrongPassword(formData.password)) {
// setStatus({
// type: "error",
// message:
// "Password must be at least 6 characters long and include uppercase, numbers, and special characters.",
// });
// return;
// }
//
setLoading(true); setLoading(true);
setStatus({ type: '', message: '' }); setStatus({ type: "", message: "" });
try { try {
await resetPassword(token, formData.password); await resetPassword(token, formData.password);
setStatus({ setStatus({
type: 'success', type: "success",
message: 'Password reset successful. You can now login with your new password.' message:
"Password reset successful. You can now login with your new password.",
}); });
setFormData({ password: '', confirmPassword: '' }); setFormData({ password: "", confirmPassword: "" });
// Redirect to login after 3 seconds setTimeout(() => navigate("/login"), 3000);
setTimeout(() => navigate('/login'), 3000);
} catch (error) { } catch (error) {
console.error("Password reset error:", error);
setStatus({ setStatus({
type: 'error', type: "error",
message: error.response?.data?.message || 'Failed to reset password. Please try again.' message:
error.response?.data?.message ||
"Failed to reset password. Please try again.",
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const isStrongPassword = (password) => {
const strongPasswordRegex =
/^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{6,}$/;
return strongPasswordRegex.test(password);
};
if (!token) return null; if (!token) return null;
return ( return (
@ -77,9 +110,9 @@ export default function ResetPassword() {
{status.message && ( {status.message && (
<div <div
className={`p-4 rounded-lg ${ className={`p-4 rounded-lg ${
status.type === 'success' status.type === "success"
? 'bg-green-500/10 border border-green-500/20 text-green-200' ? "bg-green-500/10 border border-green-500/20 text-green-200"
: 'bg-red-500/10 border border-red-500/20 text-red-200' : "bg-red-500/10 border border-red-500/20 text-red-200"
}`} }`}
> >
{status.message} {status.message}
@ -97,7 +130,9 @@ export default function ResetPassword() {
type="password" type="password"
required required
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
className="appearance-none relative block w-full px-3 py-2 border className="appearance-none relative block w-full px-3 py-2 border
border-primary-600 bg-primary-700/30 placeholder-neutral-400 border-primary-600 bg-primary-700/30 placeholder-neutral-400
text-white rounded-lg focus:outline-none focus:ring-primary-500 text-white rounded-lg focus:outline-none focus:ring-primary-500
@ -117,7 +152,9 @@ export default function ResetPassword() {
type="password" type="password"
required required
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} onChange={(e) =>
setFormData({ ...formData, confirmPassword: e.target.value })
}
className="appearance-none relative block w-full px-3 py-2 border className="appearance-none relative block w-full px-3 py-2 border
border-primary-600 bg-primary-700/30 placeholder-neutral-400 border-primary-600 bg-primary-700/30 placeholder-neutral-400
text-white rounded-lg focus:outline-none focus:ring-primary-500 text-white rounded-lg focus:outline-none focus:ring-primary-500
@ -138,7 +175,7 @@ export default function ResetPassword() {
focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50
disabled:cursor-not-allowed transition-colors" disabled:cursor-not-allowed transition-colors"
> >
{loading ? 'Resetting...' : 'Reset Password'} {loading ? "Resetting..." : "Reset Password"}
</button> </button>
</div> </div>

View File

@ -55,8 +55,21 @@ export const AuthProvider = ({ children }) => {
setUser(null); setUser(null);
}; };
const validateToken = async (token) => {
try {
const response = await api.post("/auth/validate-token", { token });
console.log(response.data.isValid);
return response.data.isValid; // Assuming the API returns { isValid: true/false }
} catch (error) {
console.error("Token validation error:", error);
return false;
}
};
return ( return (
<AuthContext.Provider value={{ user, isLoading, error, login, logout }}> <AuthContext.Provider
value={{ user, isLoading, error, login, logout, validateToken }}
>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File