From 4edf07e055f682eb2688a4a72314ce537d523caa Mon Sep 17 00:00:00 2001 From: Shaurya Rahlon Date: Wed, 21 May 2025 23:13:44 +0530 Subject: [PATCH] #issue-66 fix: improved regex for error messages and updated input validation for login and signup forms --- backend/controllers/auth.go | 14 +- frontend/package-lock.json | 42 ++++- frontend/package.json | 1 + frontend/src/Pages/Authentication/forms.tsx | 180 ++++++++++++-------- 4 files changed, 154 insertions(+), 83 deletions(-) diff --git a/backend/controllers/auth.go b/backend/controllers/auth.go index 25595a8..a898d12 100644 --- a/backend/controllers/auth.go +++ b/backend/controllers/auth.go @@ -23,13 +23,13 @@ func SignUp(ctx *gin.Context) { var request structs.SignUpRequest if err := ctx.ShouldBindJSON(&request); err != nil { - ctx.JSON(400, gin.H{"error": "Invalid input", "message": err.Error()}) + ctx.JSON(400, gin.H{"error": "Invalid input", "message": "Invalid email or password format"}) return } err := signUpWithCognito(cfg.Cognito.AppClientId, cfg.Cognito.AppClientSecret, request.Email, request.Password, ctx) if err != nil { - ctx.JSON(500, gin.H{"error": "Failed to sign up", "message": err.Error()}) + ctx.JSON(500, gin.H{"error": "Failed to sign up", "message": "Failed to sign up with Cognito"}) return } @@ -65,13 +65,13 @@ func Login(ctx *gin.Context) { var request structs.LoginRequest if err := ctx.ShouldBindJSON(&request); err != nil { - ctx.JSON(400, gin.H{"error": "Invalid input", "message": "Check email and password format"}) + ctx.JSON(400, gin.H{"error": "Invalid input", "message": "Invalid email or password format. Please check your input and try again."}) return } token, err := loginWithCognito(cfg.Cognito.AppClientId, cfg.Cognito.AppClientSecret, request.Email, request.Password, ctx) if err != nil { - ctx.JSON(401, gin.H{"error": "Failed to sign in", "message": "Invalid email or password"}) + ctx.JSON(401, gin.H{"error": "Failed to sign in", "message": "Invalid email or password. Please check your credentials and try again."}) return } @@ -86,13 +86,13 @@ func ForgotPassword(ctx *gin.Context) { var request structs.ForgotPasswordRequest if err := ctx.ShouldBindJSON(&request); err != nil { - ctx.JSON(400, gin.H{"error": "Invalid input", "message": "Check email format"}) + ctx.JSON(400, gin.H{"error": "Invalid input", "message": "Invalid email format. Please check your input and try again."}) return } _, err := initiateForgotPassword(cfg.Cognito.AppClientId, cfg.Cognito.AppClientSecret, request.Email, ctx) if err != nil { - ctx.JSON(500, gin.H{"error": "Failed to initiate password reset", "message": err.Error()}) + ctx.JSON(500, gin.H{"error": "Failed to initiate password reset", "message": "Failed to initiate password reset. Please try again later."}) return } @@ -325,4 +325,4 @@ func validateTokenWithCognito(userPoolId, token string, ctx *gin.Context) (bool, } return true, nil -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d134963..687a320 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "config": "^3.3.12", + "flag": "^5.0.1", "lucide-react": "^0.446.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -24202,14 +24203,12 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -24703,6 +24702,11 @@ "dev": true, "license": "MIT" }, + "node_modules/async-ref": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/async-ref/-/async-ref-0.1.6.tgz", + "integrity": "sha512-gIvfC7ahv452pM+nwnxZJd/vhUh8UFMrd1DCvIvWjoy9WrRmYroXTTDxwg91oiD+4CqQKrg+11uCxZrzat4UvQ==" + }, "node_modules/auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -26195,7 +26199,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/csv-parse": { @@ -27318,6 +27321,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/flag/-/flag-5.0.1.tgz", + "integrity": "sha512-4P/rvPGKcFBneboyYHMOKbKaJL5ZhNjScbY7bGToas7FgBvrTWbh76yxRaoSoFe3HSjWc6IJ0EajUwSadcP6Lg==", + "dependencies": { + "@types/react": "^18.0.5", + "async-ref": "^0.1.6" + }, + "peerDependencies": { + "react": "^18" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -51143,14 +51158,12 @@ "@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" }, "@types/react": { "version": "18.3.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", - "devOptional": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -51457,6 +51470,11 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "async-ref": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/async-ref/-/async-ref-0.1.6.tgz", + "integrity": "sha512-gIvfC7ahv452pM+nwnxZJd/vhUh8UFMrd1DCvIvWjoy9WrRmYroXTTDxwg91oiD+4CqQKrg+11uCxZrzat4UvQ==" + }, "auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -52465,8 +52483,7 @@ "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "csv-parse": { "version": "5.5.6", @@ -53234,6 +53251,15 @@ "path-exists": "^4.0.0" } }, + "flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/flag/-/flag-5.0.1.tgz", + "integrity": "sha512-4P/rvPGKcFBneboyYHMOKbKaJL5ZhNjScbY7bGToas7FgBvrTWbh76yxRaoSoFe3HSjWc6IJ0EajUwSadcP6Lg==", + "requires": { + "@types/react": "^18.0.5", + "async-ref": "^0.1.6" + } + }, "flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 102f792..d62a54d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "config": "^3.3.12", + "flag": "^5.0.1", "lucide-react": "^0.446.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/Pages/Authentication/forms.tsx b/frontend/src/Pages/Authentication/forms.tsx index c580dca..f58e68c 100644 --- a/frontend/src/Pages/Authentication/forms.tsx +++ b/frontend/src/Pages/Authentication/forms.tsx @@ -1,25 +1,47 @@ -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { useContext, useState } from 'react'; -import { AuthContext } from '../../context/authContext'; // Adjust import path - - +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useContext, useState } from "react"; +import { AuthContext } from "../../context/authContext"; // Adjust import path interface LoginFormProps { startForgotPassword: () => void; infoMessage?: string; } -export const LoginForm: React.FC = ({ startForgotPassword, infoMessage }) => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [passwordVisible, setPasswordVisible] = useState(false) +export const LoginForm: React.FC = ({ + startForgotPassword, + infoMessage, +}) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [passwordVisible, setPasswordVisible] = useState(false); const authContext = useContext(AuthContext); if (!authContext) { - throw new Error('LoginForm must be used within an AuthProvider'); + throw new Error("LoginForm must be used within an AuthProvider"); } + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setEmailError("Invalid email format"); + return false; + } + setEmailError(""); + return true; + }; + + const validatePassword = (password: string) => { + if (password.length < 8) { + setPasswordError("Password must be at least 8 characters"); + return false; + } + setPasswordError(""); + return true; + }; + const { login, error, loading } = authContext; const handleSubmit = async (e: React.FormEvent) => { @@ -29,68 +51,84 @@ export const LoginForm: React.FC = ({ startForgotPassword, infoM return (
- {infoMessage &&

{infoMessage}

} + {infoMessage && ( +

{infoMessage}

+ )} setEmail(e.target.value)} + onChange={(e) => { + setEmail(e.target.value); + validateEmail(e.target.value); + }} className="mb-2" /> + {emailError &&

{emailError}

} setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + validatePassword(e.target.value); + }} className="mb-1" /> -
-
+ {passwordError && ( +

{passwordError}

+ )} +
+
setPasswordVisible(e.target.checked)} />
-
show password
+
show password
{error &&

{error}

}

- Forgot your password?{' '} - + Forgot your password?{" "} + Reset Password

); }; - interface SignUpFormProps { startOtpVerification: (email: string) => void; } -export const SignUpForm: React.FC = ({ startOtpVerification }) => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [passwordVisible, setPasswordVisible] = useState(false) - const [confirmPassword, setConfirmPassword] = useState(''); +export const SignUpForm: React.FC = ({ + startOtpVerification, +}) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [passwordVisible, setPasswordVisible] = useState(false); + const [confirmPassword, setConfirmPassword] = useState(""); const authContext = useContext(AuthContext); if (!authContext) { - throw new Error('SignUpForm must be used within an AuthProvider'); + throw new Error("SignUpForm must be used within an AuthProvider"); } const { signup, error, loading } = authContext; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + if (password !== confirmPassword) { - authContext.handleError('Passwords do not match'); + authContext.handleError("Passwords do not match"); return; } @@ -121,19 +159,19 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) onChange={(e) => setConfirmPassword(e.target.value)} className="mb-4" /> -
-
+
+
setPasswordVisible(e.target.checked)} />
-
show password
+
show password
{error &&

{error}

} ); @@ -144,12 +182,15 @@ interface OTPVerificationFormProps { handleOtpVerified: () => void; } -export const OTPVerificationForm: React.FC = ({ email, handleOtpVerified }) => { - const [otp, setOtp] = useState(''); +export const OTPVerificationForm: React.FC = ({ + email, + handleOtpVerified, +}) => { + const [otp, setOtp] = useState(""); const authContext = useContext(AuthContext); if (!authContext) { - throw new Error('OTPVerificationForm must be used within an AuthProvider'); + throw new Error("OTPVerificationForm must be used within an AuthProvider"); } const { verifyEmail, error, loading } = authContext; @@ -163,7 +204,9 @@ export const OTPVerificationForm: React.FC = ({ email, return (

Verify Your Email

-

Enter the OTP sent to your email to complete the sign-up process.

+

+ Enter the OTP sent to your email to complete the sign-up process. +

= ({ email, /> {error &&

{error}

}
); }; - interface ForgotPasswordFormProps { startResetPassword: (email: string) => void; // Accept the new prop } @@ -189,31 +231,31 @@ interface ForgotPasswordFormProps { export const ForgotPasswordForm: React.FC = ({ startResetPassword, }) => { - const [email, setEmail] = useState(''); - const [error, setError] = useState(''); + const [email, setEmail] = useState(""); + const [error, setError] = useState(""); const baseURL = import.meta.env.VITE_BASE_URL; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); + setError(""); try { const response = await fetch(`${baseURL}/forgotPassword`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); if (!response.ok) { - setError('Failed to send reset password code. Please try again.'); + setError("Failed to send reset password code. Please try again."); return; } // Move to the ResetPasswordForm startResetPassword(email); } catch { - setError('An unexpected error occurred. Please try again later.'); + setError("An unexpected error occurred. Please try again later."); } }; @@ -238,7 +280,6 @@ export const ForgotPasswordForm: React.FC = ({ ); }; - interface ResetPasswordFormProps { email: string; handlePasswordReset: () => void; @@ -249,24 +290,27 @@ interface ResetPasswordFormProps { handlePasswordReset: () => void; } -export const ResetPasswordForm: React.FC = ({ email, handlePasswordReset }) => { - const [code, setCode] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmNewPassword, setConfirmNewPassword] = useState(''); - const [passwordVisible, setPasswordVisible] = useState(false) +export const ResetPasswordForm: React.FC = ({ + email, + handlePasswordReset, +}) => { + const [code, setCode] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmNewPassword, setConfirmNewPassword] = useState(""); + const [passwordVisible, setPasswordVisible] = useState(false); const authContext = useContext(AuthContext); if (!authContext) { - throw new Error('ResetPasswordForm must be used within an AuthProvider'); + throw new Error("ResetPasswordForm must be used within an AuthProvider"); } const { confirmForgotPassword, login, error, loading } = authContext; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + if (newPassword !== confirmNewPassword) { - authContext.handleError('Passwords do not match'); + authContext.handleError("Passwords do not match"); return; } @@ -300,21 +344,21 @@ export const ResetPasswordForm: React.FC = ({ email, han placeholder="Confirm New Password" className="w-full mb-4" /> -
-
- setPasswordVisible(e.target.checked)} - /> +
+
+ setPasswordVisible(e.target.checked)} + /> +
+
show password
-
show password
-
{error &&

{error}

}
); -}; \ No newline at end of file +};