reset password form, captacha added

This commit is contained in:
Senthamilselvi 2025-10-28 15:52:54 +05:30
parent eefb85bbde
commit e76dc7b82e
6 changed files with 395 additions and 3 deletions

View File

@ -11,6 +11,7 @@
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-google-recaptcha": "^3.1.0",
"react-router-dom": "^7.9.4"
},
"devDependencies": {
@ -2729,6 +2730,15 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -2810,7 +2820,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@ -3159,6 +3168,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -3295,6 +3316,15 @@
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3444,6 +3474,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -3469,6 +3510,19 @@
"node": ">=0.10.0"
}
},
"node_modules/react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-dom": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
@ -3481,6 +3535,25 @@
"react": "^19.2.0"
}
},
"node_modules/react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
"integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.5.0",
"react-async-script": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",

View File

@ -13,6 +13,7 @@
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-google-recaptcha": "^3.1.0",
"react-router-dom": "^7.9.4"
},
"devDependencies": {

View File

@ -11,6 +11,7 @@ import Login from './pages/Login/Login'
import History from './pages/History/History'
import ChangePassword from './pages/ChangePassword/ChangePassword'
import ForgotPassword from './pages/ForgotPassword/ForgotPassword'
import PublicContactForm from './pages/PublicContactForm'
const RequireRole = ({ allowedRoles = [], children }) => {
let role
@ -102,6 +103,7 @@ function App() {
/>
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<PublicContactForm />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)

View File

@ -110,7 +110,7 @@ const AdminHeader = () => {
className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF]"
onClick={() => {
setProfileOpen(false);
navigate('/change-password');
navigate('/reset-password');
}}
>
Change Password

View File

@ -302,7 +302,7 @@ const Login = () => {
<button
type="button"
className="text-[#92722A] hover:underline cursor-pointer"
onClick={() => navigate('/forgot-password')}
onClick={() => navigate('/reset-password')}
>
Forgot password?
</button>

View File

@ -0,0 +1,316 @@
import React, { useState, useEffect } from "react";
import Logo from "../assets/images/FCSCLogo.svg";
import { useNavigate } from "react-router-dom";
import apiClient from "../services/api/apiClient.js";
const PublicContactForm = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState({
establishmentName: "",
establishmentId: "",
email: "",
contactName: "",
contactPhone: "",
notes: "",
});
const [errors, setErrors] = useState({});
const [isValid, setIsValid] = useState(false);
const [captcha, setCaptcha] = useState("");
const [userCaptcha, setUserCaptcha] = useState("");
const [captchaError, setCaptchaError] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState("");
const [errorMsg, setErrorMsg] = useState("");
// Generate simple CAPTCHA code
const generateCaptcha = () => {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "";
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
setCaptcha(code);
};
useEffect(() => {
generateCaptcha();
}, []);
// Validation rules
const validate = (data = formData) => {
const newErrors = {};
if (!data.establishmentName.trim()) newErrors.establishmentName = "Establishment Name is required.";
if (!data.email.trim()) newErrors.email = "Registered Email is required.";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) newErrors.email = "Enter a valid email address.";
if (!data.contactName.trim()) newErrors.contactName = "Contact Person Name is required.";
if (!data.contactPhone.trim()) {
newErrors.contactPhone = "Contact Phone is required.";
} else if (!/^\d{10,12}$/.test(data.contactPhone)) {
newErrors.contactPhone = "Enter a valid phone number (1012 digits).";
}
setErrors(newErrors);
setIsValid(Object.keys(newErrors).length === 0);
return newErrors;
};
// Restrict Contact Name: only letters and spaces
const handleNameChange = (e) => {
const { value } = e.target;
const filtered = value.replace(/[^A-Za-z\s]/g, ""); // remove numbers/special chars
setFormData((prev) => ({ ...prev, contactName: filtered }));
validate({ ...formData, contactName: filtered });
};
const handlePhoneChange = (e) => {
let { value } = e.target;
// Allow only numbers
value = value.replace(/\D/g, ""); // remove all non-numeric characters
// Limit to 12 digits
if (value.length > 12) value = value.slice(0, 12);
setFormData((prev) => ({ ...prev, contactPhone: value }));
validate({ ...formData, contactPhone: value });
};
const handleChange = (e) => {
const { name, value } = e.target;
const updated = { ...formData, [name]: value };
setFormData(updated);
validate(updated);
};
// CAPTCHA validation
const handleCaptchaChange = (e) => {
setUserCaptcha(e.target.value.toUpperCase());
setCaptchaError("");
};
// Form submit
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMsg("");
setSuccess("");
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) return;
if (userCaptcha !== captcha) {
setCaptchaError("Incorrect CAPTCHA. Please try again.");
generateCaptcha();
return;
}
// Simulate processing
setLoading(true);
setTimeout(() => {
setLoading(false);
// Show success toast/message
setSuccess("Reset Password request successful.");
// Clear fields and regenerate CAPTCHA
setFormData({
establishmentName: "",
establishmentId: "",
email: "",
contactName: "",
contactPhone: "",
notes: "",
});
setUserCaptcha("");
generateCaptcha();
// After 3 seconds clear session + route to login
setTimeout(() => {
try {
sessionStorage.removeItem("auth_token");
sessionStorage.removeItem("user_profile");
sessionStorage.removeItem("user_role");
sessionStorage.removeItem("establishment_id");
} catch {
// ignore storage errors
}
navigate("/login");
}, 3000);
}, 1000); // small delay to simulate "submitting"
};
return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
<div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
{/* Header */}
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
<img src={Logo} alt="FCSC" className="w-[188px] h-[58px] mx-auto mb-2" />
<h1 className="text-base font-semibold text-[#92722A]">
Contact Establishment Info
</h1>
<p className="mt-1 text-sm text-gray-600">
Provide details to contact your establishment.
</p>
</div>
<form onSubmit={handleSubmit} className="px-7 py-8 space-y-4">
{errorMsg && (
<div className="text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
{errorMsg}
</div>
)}
{success && (
<div className="text-sm text-green-700 bg-green-50 border border-green-200 rounded px-3 py-2">
{success}
</div>
)}
{/* Inputs */}
<InputField
label="Establishment Name"
name="establishmentName"
required
value={formData.establishmentName}
onChange={handleChange}
error={errors.establishmentName}
/>
<InputField
label="Establishment ID (if known)"
name="establishmentId"
value={formData.establishmentId}
onChange={handleChange}
/>
<InputField
label="Registered Email"
name="email"
type="email"
required
value={formData.email}
onChange={handleChange}
error={errors.email}
/>
<InputField
label="Contact Person Name"
name="contactName"
required
value={formData.contactName}
onChange={handleNameChange} // new handler
error={errors.contactName}
/>
<InputField
label="Contact Phone"
name="contactPhone"
value={formData.contactPhone}
onChange={handlePhoneChange}
error={errors.contactPhone}
placeholder="Enter phone number"
maxLength={12} // extra safeguard
/>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Additional Notes
</label>
<textarea
name="notes"
rows="3"
value={formData.notes}
onChange={handleChange}
className="block w-full rounded-md border-2 border-[#92722A] focus:border-[#92722A] focus:ring-0 px-4 py-2 text-sm bg-white"
></textarea>
</div>
{/* CAPTCHA */}
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
CAPTCHA Verification <span className="text-red-500">*</span>
</label>
<div className="flex items-center gap-3">
<div
className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none"
aria-hidden="true"
>
{captcha}
</div>
<button
type="button"
onClick={generateCaptcha}
className="text-sm text-[#92722A] hover:underline"
>
Refresh
</button>
</div>
{/* Accessible alternative */}
<p className="text-xs text-gray-500 mt-1">
If you cannot read the code, click "Refresh" or type the following:{" "}
<span className="font-mono">{captcha}</span>
</p>
<input
type="text"
name="captcha"
value={userCaptcha}
onChange={handleCaptchaChange}
placeholder="Enter the code shown above"
className={`mt-2 block w-full h-10 rounded-md border-2 ${
captchaError ? "border-red-500" : "border-[#92722A]"
} focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white`}
/>
{captchaError && (
<p className="text-xs text-red-600 mt-1">{captchaError}</p>
)}
</div>
{/* Submit */}
<button
type="submit"
disabled={!isValid || loading || !userCaptcha}
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${
!isValid || loading || !userCaptcha
? "bg-[#bfa06a] cursor-not-allowed"
: "bg-[#92722A] hover:bg-[#7b5c1f]"
} transition-colors`}
>
{loading ? "Submitting…" : "Submit"}
</button>
<button
type="button"
onClick={() => navigate(-1)}
className="w-full h-10 rounded-md border border-[#92722A] text-[#92722A] hover:bg-[#92722A]/10 text-sm"
>
Cancel
</button>
</form>
</div>
</div>
</div>
);
};
// Input component
const InputField = ({ label, name, value, onChange, placeholder, required, error, type = "text" }) => (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{label} {required && <span className="text-red-500">*</span>}
</label>
<input
type={type}
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
className={`block w-full h-10 rounded-md border-2 ${
error ? "border-red-500" : "border-[#92722A]"
} focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white`}
/>
{error && <p className="text-xs text-red-600 mt-1">{error}</p>}
</div>
);
export default PublicContactForm;