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);
}
// @Post("logout")
// async logout(@Request() req) {
// await this.authService.logout(req.user.userId);
// return { message: "Logged out successfully" };
// }
@Post("validate-token")
async validateToken(@Body() { token }: { token: string }) {
if (!token || typeof token !== "string") {
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) {
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
@ -365,7 +366,7 @@ export class AuthService {
});
}
return { message: "Password reset successful" };
return { isValid: true, message: "Password reset successful" };
} catch (error) {
this.logger.error("Error in resetPasswordWithToken:", {
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> {
return console.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) {
this.logger.log("Sending password reset email to:", email);
const frontendUrl =
this.configService.get<string>("FRONTEND_URL") || "https://placebo.mk";
const resetLink = `${frontendUrl}/forgot-password?token=${token}`;
const frontendUrl = "http://localhost:5173";
// this.configService.get<string>("FRONTEND_URL") || "http://localhost:5147";
const resetLink = `${frontendUrl}/reset-password?token=${token}`;
const mailOptions = {
from: this.from,
to: email,

View File

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

View File

@ -55,8 +55,21 @@ export const AuthProvider = ({ children }) => {
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 (
<AuthContext.Provider value={{ user, isLoading, error, login, logout }}>
<AuthContext.Provider
value={{ user, isLoading, error, login, logout, validateToken }}
>
{children}
</AuthContext.Provider>
);

View File