diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 4f086c8..b3f4e2c 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -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); + } } diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 0fe58e3..f61fb5f 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -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 { - return console.log("User logged out:", userId); this.logger.log("User logged out:", userId); + return console.log("User logged out:", userId); } } diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index 0a80721..3457624 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -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("FRONTEND_URL") || "https://placebo.mk"; - const resetLink = `${frontendUrl}/forgot-password?token=${token}`; + const frontendUrl = "http://localhost:5173"; + // this.configService.get("FRONTEND_URL") || "http://localhost:5147"; + const resetLink = `${frontendUrl}/reset-password?token=${token}`; const mailOptions = { from: this.from, to: email, diff --git a/frontend/src/components/auth/ResetPassword.jsx b/frontend/src/components/auth/ResetPassword.jsx index 1e081e5..59cff10 100644 --- a/frontend/src/components/auth/ResetPassword.jsx +++ b/frontend/src/components/auth/ResetPassword.jsx @@ -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(() => { - if (!token) { - navigate('/forgot-password'); - } - }, [token, navigate]); + const checkToken = async () => { + console.log("token received from validate-token ", token); + if (!token) { + console.log("no token found"); + navigate("/forgot-password"); + return; + } + 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 && (
{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"}
@@ -154,4 +191,4 @@ export default function ResetPassword() { ); -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useAuth.jsx b/frontend/src/hooks/useAuth.jsx index e90bb26..1d7bae6 100644 --- a/frontend/src/hooks/useAuth.jsx +++ b/frontend/src/hooks/useAuth.jsx @@ -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 ( - + {children} ); diff --git a/frontend/src/pages/reset/Reset.jsx b/frontend/src/pages/reset/Reset.jsx new file mode 100644 index 0000000..e69de29