diff --git a/apps/OpenSign/public/locales/de/translation.json b/apps/OpenSign/public/locales/de/translation.json index 7374eb7b9d..a31168674b 100644 --- a/apps/OpenSign/public/locales/de/translation.json +++ b/apps/OpenSign/public/locales/de/translation.json @@ -165,7 +165,8 @@ "daily-mail-quota": "Tägliches E-Mail-Kontingent", "Save as template": "Als Vorlage speichern", "Fix & resend": "Korrigieren und erneut senden", - "Kiosk Mode": "Kiosk Modus" + "Kiosk Mode": "Kiosk Modus", + "Reset password": "Passwort zurücksetzen" }, "report-heading": { "Sr.No": "Nr.", @@ -1230,5 +1231,16 @@ "save-as-template-msg": "Speichern Sie dieses Dokument als wiederverwendbare Vorlage, die Sie für zukünftige Dokumente erneut nutzen können.", "public-credit-alert": "Das Signieren über diesen Link verbraucht Ihre kostenlosen E-Mail-Credits. Als kostenloser Nutzer erhalten Sie 15 E-Mail-Credits pro Monat, um zu verhindern, dass Spammer unsere Systeme missbrauchen. Um höhere Limits und ununterbrochenen Zugriff zu genießen, abonnieren Sie die <1>OpenSign™-Bezahlpläne.", "know-more-about": "Mehr erfahren über", - "free-unlimited-signatures": "Kostenlose unbegrenzte Signaturen" + "free-unlimited-signatures": "Kostenlose unbegrenzte Signaturen", + "email-quota-updated": "Tägliches E-Mail-Kontingent erfolgreich aktualisiert.", + "password-has-been-reset": "Passwort wurde zurückgesetzt.", + "Unauthorized to reset your own password.": "Nicht berechtigt, Ihr eigenes Passwort zurückzusetzen.", + "User not found or not allowed.": "Benutzer nicht gefunden oder nicht erlaubt.", + "User not found.": "Benutzer nicht gefunden.", + "Admin user tenant not found.": "Admin-Benutzer-Tenant nicht gefunden.", + "User is not authenticated.": "Benutzer ist nicht authentifiziert.", + "Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.", + "reset-password": "Benutzerpasswort zurücksetzen", + "enter-strong-password": "Geben Sie ein sicheres Passwort ein oder generieren Sie eines automatisch.", + "autogenerate": "Automatisch generieren" } \ No newline at end of file diff --git a/apps/OpenSign/public/locales/en/translation.json b/apps/OpenSign/public/locales/en/translation.json index 8f5cbfd54e..acb87b405d 100644 --- a/apps/OpenSign/public/locales/en/translation.json +++ b/apps/OpenSign/public/locales/en/translation.json @@ -165,7 +165,8 @@ "daily-mail-quota": "Daily Email Quota", "Save as template": "Save as template", "Fix & resend": "Fix & Resend", - "Kiosk Mode": "Kiosk Mode" + "Kiosk Mode": "Kiosk Mode", + "Reset password": "Reset password" }, "report-heading": { "Sr.No": "Sr.No", @@ -1231,5 +1232,16 @@ "save-as-template-msg": "Save this document as a reusable template that you can use again for future documents.", "public-credit-alert": "Signing through this link consumes your free email credits. As a Free user, you are allotted 15 email credits per month to prevent spammers from abusing our systems. To enjoy higher limits and uninterrupted access, Subscribe to <1>OpenSign™ Paid plans.", "know-more-about": "Know more about", - "free-unlimited-signatures": "Free Unlimited Signatures" + "free-unlimited-signatures": "Free Unlimited Signatures", + "email-quota-updated": "Daily email quota updated successfully.", + "password-has-been-reset": "Password has been reset.", + "Unauthorized to reset your own password.": "Unauthorized to reset your own password.", + "User not found or not allowed.": "User not found or not allowed.", + "User not found.": "User not found.", + "Admin user tenant not found.": "Admin user tenant not found.", + "User is not authenticated.": "User is not authenticated.", + "Please provide required parameters.": "Please provide required parameters.", + "reset-password": "Reset User Password", + "enter-strong-password": "Enter a strong password or auto-generate one.", + "autogenerate": "Autogenerate" } \ No newline at end of file diff --git a/apps/OpenSign/public/locales/es/translation.json b/apps/OpenSign/public/locales/es/translation.json index 744652fab9..4a48cc1f41 100644 --- a/apps/OpenSign/public/locales/es/translation.json +++ b/apps/OpenSign/public/locales/es/translation.json @@ -165,7 +165,8 @@ "daily-mail-quota": "Cuota diaria de correos electrónicos", "Save as template": "Guardar como plantilla", "Fix & resend": "Corregir y reenviar", - "Kiosk Mode": "Modo Kiosco" + "Kiosk Mode": "Modo Kiosco", + "Reset password": "Restablecer contraseña" }, "report-heading": { "Sr.No": "Nº", @@ -1231,5 +1232,16 @@ "save-as-template-msg": "Guarde este documento como una plantilla reutilizable que podrá usar nuevamente para futuros documentos.", "public-credit-alert": "Firmar a través de este enlace consumirá sus créditos de correo electrónico gratuitos. Como usuario gratuito, se le asignan 15 créditos de correo electrónico por mes para evitar que los spammers abusen de nuestros sistemas. Para disfrutar de límites más altos y acceso ininterrumpido, suscríbase a los planes de pago de <1>OpenSign™.", "know-more-about": "Saber más sobre", - "free-unlimited-signatures": "¿Firmas ilimitadas gratis" + "free-unlimited-signatures": "¿Firmas ilimitadas gratis", + "email-quota-updated": "La cuota diaria de correos se actualizó correctamente.", + "password-has-been-reset": "La contraseña ha sido restablecida.", + "Unauthorized to reset your own password.": "No está autorizado para restablecer su propia contraseña.", + "User not found or not allowed.": "Usuario no encontrado o no autorizado.", + "User not found.": "Usuario no encontrado.", + "Admin user tenant not found.": "Inquilino de usuario administrador no encontrado.", + "User is not authenticated.": "El usuario no está autenticado.", + "Please provide required parameters.": "Por favor, proporcione los parámetros requeridos.", + "reset-password": "Restablecer contraseña de usuario", + "enter-strong-password": "Ingrese una contraseña segura o genere una automáticamente.", + "autogenerate": "Generar automáticamente" } \ No newline at end of file diff --git a/apps/OpenSign/public/locales/fr/translation.json b/apps/OpenSign/public/locales/fr/translation.json index d7a1e2f036..288ed0d0d9 100644 --- a/apps/OpenSign/public/locales/fr/translation.json +++ b/apps/OpenSign/public/locales/fr/translation.json @@ -200,7 +200,8 @@ "daily-mail-quota": "Quota d'e-mails quotidien", "Save as template": "Enregistrer comme modèle", "Fix & resend": "Corriger et renvoyer", - "Kiosk Mode": "Mode Kiosque" + "Kiosk Mode": "Mode Kiosque", + "Reset password": "Réinitialiser le mot de passe" }, "report-help": { "Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.", @@ -426,7 +427,7 @@ "initial-alert": "Mon initiale est introuvable", "copy-title": "Copier le widget vers", "contact-delete-alert": "Êtes-vous sûr de vouloir supprimer ce contact ?", - "reset-password": "Le lien de réinitialisation du mot de passe a été envoyé à votre identifiant de messagerie", + "reset-password-alert-1": "Le lien de réinitialisation du mot de passe a été envoyé à votre identifiant de messagerie", "reset-password-alert-2": "Veuillez configurer l'adaptateur de messagerie", "reset-password-alert-3": "Réinitialisez votre mot de passe", "faild-animation": "Échec du chargement de l'animation", @@ -1230,5 +1231,16 @@ "save-as-template-msg": "Enregistrez ce document comme modèle réutilisable que vous pourrez utiliser à nouveau pour de futurs documents.", "public-credit-alert": "Signer via ce lien consomme vos crédits e-mail gratuits. En tant qu'utilisateur gratuit, vous disposez de 15 crédits e-mail par mois afin d’empêcher les spammeurs d’abuser de nos systèmes. Pour profiter de limites plus élevées et d'un accès ininterrompu, abonnez-vous aux forfaits payants de <1>OpenSign™.", "know-more-about": "En savoir plus sur", - "free-unlimited-signatures": "Signatures illimitées gratuites" + "free-unlimited-signatures": "Signatures illimitées gratuites", + "email-quota-updated": "Quota quotidien d'e-mails mis à jour avec succès.", + "password-has-been-reset": "Le mot de passe a été réinitialisé.", + "Unauthorized to reset your own password.": "Non autorisé à réinitialiser votre propre mot de passe.", + "User not found or not allowed.": "Utilisateur introuvable ou non autorisé.", + "User not found.": "Utilisateur introuvable.", + "Admin user tenant not found.": "Locataire administrateur introuvable.", + "User is not authenticated.": "L'utilisateur n'est pas authentifié.", + "Please provide required parameters.": "Veuillez fournir les paramètres requis.", + "reset-password": "Réinitialiser le mot de passe utilisateur", + "enter-strong-password": "Entrez un mot de passe sécurisé ou générez-en un automatiquement.", + "autogenerate": "Générer automatiquement" } \ No newline at end of file diff --git a/apps/OpenSign/public/locales/hi/translation.json b/apps/OpenSign/public/locales/hi/translation.json index 76be44849f..a4ac53c549 100644 --- a/apps/OpenSign/public/locales/hi/translation.json +++ b/apps/OpenSign/public/locales/hi/translation.json @@ -165,7 +165,8 @@ "daily-mail-quota": "दैनिक ईमेल कोटा", "Save as template": "टेम्पलेट के रूप में सहेजें", "Fix & resend": "ठीक करें और पुनः भेजें", - "Kiosk Mode": "कियोस्क मोड" + "Kiosk Mode": "कियोस्क मोड", + "Reset password": "पासवर्ड रीसेट करें" }, "report-heading": { "Sr.No": "क्रमांक", @@ -1230,5 +1231,16 @@ "save-as-template-msg": "इस दस्तावेज़ को एक पुन: प्रयोज्य टेम्पलेट के रूप में सहेजें जिसे आप भविष्य के दस्तावेज़ों के लिए फिर से उपयोग कर सकते हैं।", "public-credit-alert": "इस लिंक के माध्यम से हस्ताक्षर करने पर आपके निःशुल्क ईमेल क्रेडिट का उपभोग होगा। एक निःशुल्क उपयोगकर्ता के रूप में, आपको स्पैमर्स को हमारी प्रणाली का दुरुपयोग करने से रोकने के लिए प्रति माह 15 ईमेल क्रेडिट आवंटित किए जाते हैं। उच्च सीमा और निर्बाध पहुँच का आनंद लेने के लिए, <1>OpenSign™ पेड प्लान की सदस्यता लें।", "know-more-about": "इसके बारे में और जानें", - "free-unlimited-signatures": "मुफ़्त असीमित हस्ताक्षर" + "free-unlimited-signatures": "मुफ़्त असीमित हस्ताक्षर", + "email-quota-updated": "दैनिक ईमेल कोटा सफलतापूर्वक अपडेट किया गया।", + "password-has-been-reset": "पासवर्ड रीसेट कर दिया गया है।", + "Unauthorized to reset your own password.": "अपना पासवर्ड रीसेट करने के लिए अधिकृत नहीं है।", + "User not found or not allowed.": "उपयोगकर्ता नहीं मिला या अनुमति नहीं है।", + "User not found.": "उपयोगकर्ता नहीं मिला।", + "Admin user tenant not found.": "एडमिन उपयोगकर्ता टेनेंट नहीं मिला।", + "User is not authenticated.": "उपयोगकर्ता प्रमाणीकृत नहीं है।", + "Please provide required parameters.": "कृपया आवश्यक पैरामीटर प्रदान करें।", + "reset-password": "उपयोगकर्ता पासवर्ड रीसेट करें", + "enter-strong-password": "एक मज़बूत पासवर्ड दर्ज करें या स्वचालित रूप से उत्पन्न करें।", + "autogenerate": "स्वतः उत्पन्न करें" } \ No newline at end of file diff --git a/apps/OpenSign/public/locales/it/translation.json b/apps/OpenSign/public/locales/it/translation.json index 30c1884dd0..95e6bb24c4 100644 --- a/apps/OpenSign/public/locales/it/translation.json +++ b/apps/OpenSign/public/locales/it/translation.json @@ -165,7 +165,8 @@ "daily-mail-quota": "Quota e-mail giornaliera", "Save as template": "Salva come modello", "Fix & resend": "Correggi e reinvia", - "Kiosk Mode": "Modalità Kiosk" + "Kiosk Mode": "Modalità Kiosk", + "Reset password": "Reimposta password" }, "report-heading": { "Sr.No": "Nr.", @@ -1230,5 +1231,16 @@ "save-as-template-msg": "Salva questo documento come modello riutilizzabile che potrai usare di nuovo per documenti futuri.", "public-credit-alert": "Firmare tramite questo link consumerà i tuoi crediti e-mail gratuiti. Come utente gratuito, ti vengono assegnati 15 crediti e-mail al mese per impedire agli spammer di abusare dei nostri sistemi. Per usufruire di limiti più elevati e accesso ininterrotto, abbonati ai piani a pagamento di <1>OpenSign™.", "know-more-about": "Scopri di più su", - "free-unlimited-signatures": "Firme illimitate gratuite" + "free-unlimited-signatures": "Firme illimitate gratuite", + "email-quota-updated": "Quota e-mail giornaliera aggiornata con successo.", + "password-has-been-reset": "La password è stata reimpostata.", + "Unauthorized to reset your own password.": "Non sei autorizzato a reimpostare la tua password.", + "User not found or not allowed.": "Utente non trovato o non autorizzato.", + "User not found.": "Utente non trovato.", + "Admin user tenant not found.": "Tenant utente amministratore non trovato.", + "User is not authenticated.": "Utente non autenticato.", + "Please provide required parameters.": "Fornisci i parametri richiesti.", + "reset-password": "Reimposta password utente", + "enter-strong-password": "Inserisci una password sicura o generane una automaticamente.", + "autogenerate": "Genera automaticamente" } \ No newline at end of file diff --git a/apps/OpenSign/src/App.jsx b/apps/OpenSign/src/App.jsx index 435fc2a323..ec4dfc36f6 100644 --- a/apps/OpenSign/src/App.jsx +++ b/apps/OpenSign/src/App.jsx @@ -19,7 +19,6 @@ import Loader from "./primitives/Loader"; import UserList from "./pages/UserList"; import { serverUrl_fn } from "./constant/appinfo"; import DocSuccessPage from "./pages/DocSuccessPage"; -import ValidateSession from "./primitives/ValidateSession"; import DragProvider from "./components/DragProivder"; import Title from "./components/Title"; const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf")); @@ -88,13 +87,7 @@ function App() { path="/forgetpassword" element={} /> - - - - } - > + }> } /> { const { t, i18n } = useTranslation(); @@ -67,6 +68,8 @@ const Header = ({ isConsole, setIsLoggingOut }) => { await Parse.User.logOut(); } catch (err) { console.log("Err while logging out", err); + } finally { + dispatch(sessionStatus(true)); } let appdata = localStorage.getItem("userSettings"); let applogo = localStorage.getItem("appLogo"); diff --git a/apps/OpenSign/src/layout/HomeLayout.jsx b/apps/OpenSign/src/layout/HomeLayout.jsx index b7a80851e0..548fb5062a 100644 --- a/apps/OpenSign/src/layout/HomeLayout.jsx +++ b/apps/OpenSign/src/layout/HomeLayout.jsx @@ -4,21 +4,22 @@ import Footer from "../components/Footer"; import Sidebar from "../components/sidebar/Sidebar"; import Tour from "../primitives/Tour"; import axios from "axios"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import Parse from "parse"; -import ModalUi from "../primitives/ModalUi"; -import { useNavigate, useLocation, Outlet } from "react-router"; +import { useNavigate, Outlet } from "react-router"; import Loader from "../primitives/Loader"; import { useTranslation } from "react-i18next"; +import { sessionStatus } from "../redux/reducers/userReducer"; +import SessionExpiredModal from "../primitives/SessionExpiredModal"; const HomeLayout = () => { const appName = "OpenSign™"; const { t, i18n } = useTranslation(); const navigate = useNavigate(); - const location = useLocation(); + const dispatch = useDispatch(); const arr = useSelector((state) => state.TourSteps); - const [isUserValid, setIsUserValid] = useState(true); + const isValidSession = useSelector((state) => state.user.isValidSession); const [isLoader, setIsLoader] = useState(true); const [isCloseBtn, setIsCloseBtn] = useState(true); const [isTour, setIsTour] = useState(false); @@ -35,28 +36,31 @@ const HomeLayout = () => { }, []); useEffect(() => { - if (!tenantId) { - setIsUserValid(false); - } else { - (async () => { - try { - // Use the session token to validate the user - const userQuery = new Parse.Query(Parse.User); - const user = await userQuery.get(Parse?.User?.current()?.id, { - sessionToken: localStorage.getItem("accesstoken") - }); - if (user) { - localStorage.setItem("profileImg", user.get("ProfilePic") || ""); - setIsUserValid(true); - setIsLoader(false); - } else { - setIsUserValid(false); + if (localStorage.getItem("accesstoken")) { + if (!tenantId) { + dispatch(sessionStatus(false)); + } else { + (async () => { + try { + // Use the session token to validate the user + const userQuery = new Parse.Query(Parse.User); + const user = await userQuery.get(Parse?.User?.current()?.id, { + sessionToken: localStorage.getItem("accesstoken") + }); + if (user) { + localStorage.setItem("profileImg", user.get("ProfilePic") || ""); + dispatch(sessionStatus(true)); + setIsLoader(false); + } else { + dispatch(sessionStatus(true)); + } + } catch (error) { + console.log("catch"); + // Session token is invalid or there was an error + dispatch(sessionStatus(false)); } - } catch (error) { - // Session token is invalid or there was an error - setIsUserValid(false); - } - })(); + })(); + } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -169,77 +173,56 @@ const HomeLayout = () => { } } - const handleLoginBtn = async () => { - try { - await Parse?.User?.logOut(); - } catch (err) { - console.log("err ", err); - } finally { - localStorage.removeItem("accesstoken"); - navigate("/", { replace: true, state: { from: location } }); - } - }; - return ( + return isValidSession && localStorage.getItem("accesstoken") ? (
{/* HEADER */}
{!isLoader &&
}
- {isUserValid ? ( + {isLoader ? ( +
+ +
+ ) : ( <> - {isLoader ? ( -
+ {isLoggingOut && ( +
- ) : ( - <> - {isLoggingOut && ( -
- + )} + {/* BODY */} +
+ {/* SIDEBAR with width animation */} + + {/* MAIN (includes both content + footer in one scrollable column) */} +
+
+ {/* your page content */} +
{}
+ {/* sticky-but-scrollable footer */} +
+
- )} - {/* BODY */} -
- {/* SIDEBAR with width animation */} - - {/* MAIN (includes both content + footer in one scrollable column) */} -
-
- {/* your page content */} -
{}
- {/* sticky-but-scrollable footer */} -
-
-
-
-
- - - )} - - ) : ( - -
-

{t("session-expired")}

- +
- + + )}
+ ) : ( + ); }; diff --git a/apps/OpenSign/src/pages/Form.jsx b/apps/OpenSign/src/pages/Form.jsx index 495bc76d7c..0b11fee64c 100644 --- a/apps/OpenSign/src/pages/Form.jsx +++ b/apps/OpenSign/src/pages/Form.jsx @@ -30,6 +30,8 @@ import ModalUi from "../primitives/ModalUi"; import { Tooltip } from "react-tooltip"; import Loader from "../primitives/Loader"; import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { sessionStatus } from "../redux/reducers/userReducer"; // `Form` render all type of Form on this basis of their provided in path function Form() { @@ -50,6 +52,7 @@ const Forms = (props) => { const abortController = new AbortController(); const inputFileRef = useRef(null); const navigate = useNavigate(); + const dispatch = useDispatch(); const [signers, setSigners] = useState([]); const [folder, setFolder] = useState({ ObjectId: "", Name: "" }); const [formData, setFormData] = useState({ @@ -277,7 +280,11 @@ const Forms = (props) => { console.log("err in docx to pdf ", err); const error = t("docx-error"); - alert(error); + if (err?.code === 209) { + dispatch(sessionStatus(false)); + } else { + alert(error); + } return; } } @@ -335,7 +342,11 @@ const Forms = (props) => { setSelectedFiles([]); } } catch (error) { - alert(error.message); + if (error?.code === 209) { + dispatch(sessionStatus(false)); + } else { + alert(error.message); + } setSelectedFiles([]); removeFile(e); } @@ -472,7 +483,9 @@ const Forms = (props) => { } } catch (err) { console.log("err ", err); - if (err.message === "only 15 reminder allowed") { + if (err?.code === 209) { + dispatch(sessionStatus(false)); + } else if (err.message === "only 15 reminder allowed") { setIsAlert({ type: "danger", message: t("only-15-reminder-allowed") @@ -604,7 +617,9 @@ const Forms = (props) => { } } catch (err) { removeFile(); - if (err?.response?.status === 401) { + if (err?.code === 209) { + dispatch(sessionStatus(false)); + } else if (err?.response?.status === 401) { setIsPassword(true); setIsCorrectPass(false); } else { diff --git a/apps/OpenSign/src/pages/UserList.jsx b/apps/OpenSign/src/pages/UserList.jsx index 63dc24d840..3868a3da60 100644 --- a/apps/OpenSign/src/pages/UserList.jsx +++ b/apps/OpenSign/src/pages/UserList.jsx @@ -12,6 +12,7 @@ import { } from "react-i18next"; import DeleteUserModal from "../primitives/DeleteUserModal"; import axios from "axios"; +import PasswordResetModal from "../primitives/PasswordResetModal"; const actions = [ { @@ -23,6 +24,14 @@ const actions = [ action: "delete", restrictAdmin: true }, + { + btnId: "1910", + hoverLabel: "Reset password", + btnIcon: "fa-light fa-key", + redirectUrl: "", + action: "resetpassword", + restrictAdmin: true + }, ]; const heading = ["Sr.No", "Name", "Email", "Phone", "Role", "Team", "Active"]; const UserList = () => { @@ -208,9 +217,9 @@ const UserList = () => { }; // `showAlert` handle show/hide alert - const showAlert = (type, msg) => { + const showAlert = (type, msg, timer = 1500) => { setIsAlert({ type, msg }); - setTimeout(() => setIsAlert({ type: "success", msg: "" }), 1500); + setTimeout(() => setIsAlert({ type: "success", msg: "" }), timer); }; const handleDeleteAccount = async (item) => { @@ -247,9 +256,7 @@ const UserList = () => { }; const handleActionBtn = async (act, item) => { - if (act.action === "delete") { - setIsActModal({ [`delete_${item.objectId}`]: true }); - } + setIsActModal({ [`${act.action}_${item.objectId}`]: true }); }; const handleBtnVisibility = (act, item) => { if (act.restrictAdmin) { @@ -274,6 +281,21 @@ const UserList = () => { return item?.objectId !== extClass?.[0]?.objectId; } }; + + async function submitPassword(userId, password) { + setIsLoader(true); + setIsActModal({}); + try { + const params = { userId, password }; + await Parse.Cloud.run("resetpassword", params); + showAlert("success", t("password-has-been-reset")); + } catch (err) { + console.log("err while reset password", err); + showAlert("danger", t(err.message), 2000); + } finally { + setIsLoader(false); + } + } return (
{isLoader && ( @@ -447,6 +469,17 @@ const UserList = () => { deleteRes={deleteUserRes} handleClose={handleCloseModal} /> + ))}
diff --git a/apps/OpenSign/src/primitives/PasswordResetModal.jsx b/apps/OpenSign/src/primitives/PasswordResetModal.jsx new file mode 100644 index 0000000000..05c7826c5e --- /dev/null +++ b/apps/OpenSign/src/primitives/PasswordResetModal.jsx @@ -0,0 +1,205 @@ +import { useMemo, useState } from "react"; +import ModalUi from "./ModalUi"; +import { useTranslation } from "react-i18next"; + +/** + * PasswordResetModal + * - Validates password rules (length >= 8, upper, lower, digit, special) + * - Buttons: Submit, Autogenerate (12 chars), Copy (Font Awesome icon only) + * - `Autogenerate` fills a valid 12-char password but remains editable + * - `Copy` copies the current password to clipboard + * + * Props: + * - isOpen: boolean + * - onClose: () => void + * - onSubmit: (password: string) => Promise | void + */ +export default function PasswordResetModal({ + userId, + isOpen, + onClose, + onSubmit, + showAlert +}) { + const { t } = useTranslation(); + const [password, setPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [copied, setCopied] = useState(false); + + // Validation helpers + const rules = useMemo( + () => ({ + hasUpper: /[A-Z]/, + hasLower: /[a-z]/, + hasDigit: /\d/, + // Richer special set including common keyboard symbols + hasSpecial: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/, + minLen: 8 + }), + [] + ); + + const checks = useMemo(() => { + return { + lenOK: password.length >= rules.minLen, + upperLowerDigitOK: + rules.hasUpper.test(password) && + rules.hasLower.test(password) && + rules.hasDigit.test(password), + specialOK: rules.hasSpecial.test(password) + }; + }, [password, rules]); + + // Always-present condition list with per-condition status + const conditions = useMemo( + () => [ + { + key: "len", + ok: checks.lenOK, + text: "password-length" + }, + { + key: "uld", + ok: checks.upperLowerDigitOK, + text: "password-case" + }, + { + key: "spec", + ok: checks.specialOK, + text: "password-special-case" + } + ], + [checks] + ); + + const allValid = conditions.every((c) => c.ok) && password.length > 0; + + // Autogenerate a valid random password (12 chars) + const handleAutogen = () => { + const length = 12; + const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // omit ambiguous I/O + const lower = "abcdefghijkmnopqrstuvwxyz"; // omit ambiguous l + const digits = "23456789"; // omit 0/1 + const special = "!@#$%^&*()-_=+[]{};:,.?"; + const all = upper + lower + digits + special; + + function pick(str) { + return str[Math.floor(Math.random() * str.length)]; + } + function shuffle(arr) { + return arr.sort(() => Math.random() - 0.5); + } + + let pwd = [pick(upper), pick(lower), pick(digits), pick(special)]; + while (pwd.length < length) pwd.push(pick(all)); + pwd = shuffle(pwd).join(""); + + setPassword(pwd); + }; + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(password); + showAlert("success", t("copied"), 1200); + setCopied(true); + setTimeout(() => setCopied(false, 1200)); + } catch (e) { + console.error("Clipboard error", e); + } + }; + + const handleSubmit = async (e) => { + e?.preventDefault?.(); + if (!allValid) return; // guard + try { + setSubmitting(true); + await onSubmit?.(userId, password); + setSubmitting(false); + onClose?.(); + setPassword(""); + } catch (err) { + console.error(err); + setSubmitting(false); + } + }; + + return ( + <> + { + setPassword(""); + onClose?.(); + }} + > +
+

+ {t("enter-strong-password")} +

+ + + {/* When everything is valid, show just a right tick */} +
    + {conditions.map((c) => ( +
  • + {c.ok ? "✓" : "✗"} {t(c.text)} +
  • + ))} +
+ +
+ + + +
+
+
+ + ); +} diff --git a/apps/OpenSign/src/primitives/ValidateSession.jsx b/apps/OpenSign/src/primitives/SessionExpiredModal.jsx similarity index 61% rename from apps/OpenSign/src/primitives/ValidateSession.jsx rename to apps/OpenSign/src/primitives/SessionExpiredModal.jsx index 2c80b4928c..98b196fdaf 100644 --- a/apps/OpenSign/src/primitives/ValidateSession.jsx +++ b/apps/OpenSign/src/primitives/SessionExpiredModal.jsx @@ -1,26 +1,29 @@ -import React from "react"; import { useTranslation } from "react-i18next"; -import ModalUi from "./ModalUi"; -import { useNavigate } from "react-router"; +import { useLocation, useNavigate } from "react-router"; +import { useDispatch } from "react-redux"; +import { sessionStatus } from "../redux/reducers/userReducer"; import Parse from "parse"; +import ModalUi from "./ModalUi"; -const ValidateSession = ({ children }) => { +const SessionExpiredModal = () => { const { t } = useTranslation(); + const location = useLocation(); const navigate = useNavigate(); + const dispatch = useDispatch(); const handleLoginBtn = async () => { try { await Parse?.User?.logOut(); } catch (err) { - console.log("err ", err); + console.log(`err: ${err}`); } finally { localStorage.removeItem("accesstoken"); - navigate("/", { replace: true }); + dispatch(sessionStatus(true)); + navigate("/", { replace: true, state: { from: location } }); } }; - return localStorage.getItem("accesstoken") ? ( - children - ) : ( + + return (

{t("session-expired")}

@@ -32,4 +35,4 @@ const ValidateSession = ({ children }) => { ); }; -export default ValidateSession; +export default SessionExpiredModal; diff --git a/apps/OpenSign/src/primitives/Validate.jsx b/apps/OpenSign/src/primitives/Validate.jsx index 1cb6de19e9..dc0d937c1f 100644 --- a/apps/OpenSign/src/primitives/Validate.jsx +++ b/apps/OpenSign/src/primitives/Validate.jsx @@ -1,12 +1,9 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import Parse from "parse"; -import { Outlet, useNavigate, useLocation } from "react-router"; -import ModalUi from "./ModalUi"; -import { useTranslation } from "react-i18next"; +import { Outlet } from "react-router"; +import SessionExpiredModal from "./SessionExpiredModal"; + const Validate = () => { - const navigate = useNavigate(); - const { t } = useTranslation(); - const location = useLocation(); const [isUserValid, setIsUserValid] = useState(true); useEffect(() => { (async () => { @@ -37,30 +34,7 @@ const Validate = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleLoginBtn = async () => { - try { - await Parse.User.logOut(); - } catch (err) { - console.log("err ", err); - } finally { - localStorage.removeItem("accesstoken"); - navigate("/", { replace: true, state: { from: location } }); - } - }; - return isUserValid ? ( -
- -
- ) : ( - -
-

{t("session-expired")}

- -
-
- ); + return isUserValid ? : ; }; export default Validate; diff --git a/apps/OpenSign/src/redux/reducers/userReducer.js b/apps/OpenSign/src/redux/reducers/userReducer.js new file mode 100644 index 0000000000..7656139082 --- /dev/null +++ b/apps/OpenSign/src/redux/reducers/userReducer.js @@ -0,0 +1,15 @@ +import { createSlice } from "@reduxjs/toolkit"; +const initialState = { isValidSession: true }; +const userReducer = createSlice({ + name: "user", + initialState: initialState, + reducers: { + sessionStatus: (state, action) => { + state.isValidSession = action.payload; + } + } +}); + +export const { sessionStatus } = userReducer.actions; + +export default userReducer.reducer; diff --git a/apps/OpenSign/src/redux/store.js b/apps/OpenSign/src/redux/store.js index 0d2f9863d8..016433392d 100644 --- a/apps/OpenSign/src/redux/store.js +++ b/apps/OpenSign/src/redux/store.js @@ -4,6 +4,7 @@ import ShowTenant from "./reducers/ShowTenant"; import TourStepsReducer from "./reducers/TourStepsReducer"; import widgetReducer from "./reducers/widgetSlice"; import sidebarReducer from "./reducers/sidebarReducer"; +import userReducer from "./reducers/userReducer"; export const store = configureStore({ reducer: { @@ -11,6 +12,7 @@ export const store = configureStore({ TourSteps: TourStepsReducer, ShowTenant, widget: widgetReducer, - sidebar: sidebarReducer + sidebar: sidebarReducer, + user: userReducer } }); diff --git a/apps/OpenSign/src/reports/document/DocumentsReport.jsx b/apps/OpenSign/src/reports/document/DocumentsReport.jsx index cce0f53301..7fd38d5d76 100644 --- a/apps/OpenSign/src/reports/document/DocumentsReport.jsx +++ b/apps/OpenSign/src/reports/document/DocumentsReport.jsx @@ -106,6 +106,15 @@ const DocumentsReport = (props) => { } }, [props.isSearchResult]); + // Close dropdown when clicking outside or on button again + useEffect(() => { + const onDocClick = (e) => { + if (!e.target.closest('[data-dropdown-root="1"]')) setIsOption({}); + }; + document.addEventListener("click", onDocClick); + return () => document.removeEventListener("click", onDocClick); + }, []); + const getPaginationRange = () => { const totalPageNumbers = 7; // Adjust this value to show more/less page numbers const pages = []; @@ -1174,6 +1183,7 @@ const DocumentsReport = (props) => { {handleBtnVisibility(act, item) && (
handleActionBtn(act, item)} diff --git a/apps/OpenSign/src/reports/template/TemplatesReport.jsx b/apps/OpenSign/src/reports/template/TemplatesReport.jsx index 5b14cb4377..4ef51dec06 100644 --- a/apps/OpenSign/src/reports/template/TemplatesReport.jsx +++ b/apps/OpenSign/src/reports/template/TemplatesReport.jsx @@ -165,6 +165,15 @@ const TemplatesReport = (props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Close dropdown when clicking outside or on button again + useEffect(() => { + const onDocClick = (e) => { + if (!e.target.closest('[data-dropdown-root="1"]')) setIsOption({}); + }; + document.addEventListener("click", onDocClick); + return () => document.removeEventListener("click", onDocClick); + }, []); + // `fetchTeamList` is used to fetch team list for share with functionality const fetchTeamList = async () => { try { @@ -1152,6 +1161,7 @@ const TemplatesReport = (props) => { role="button" data-tut={act?.selector} key={index} + data-dropdown-root="1" onClick={() => handleActionBtn(act, item)} title={t(`btnLabel.${act.hoverLabel}`)} className={ diff --git a/apps/OpenSignServer/cloud/main.js b/apps/OpenSignServer/cloud/main.js index 0d5244a591..25a0932df9 100644 --- a/apps/OpenSignServer/cloud/main.js +++ b/apps/OpenSignServer/cloud/main.js @@ -56,6 +56,7 @@ import loginUser from './parsefunction/loginUser.js'; import addUser from './parsefunction/addUser.js'; import filterDocs from './parsefunction/filterDocs.js'; import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js'; +import resetPassword from './parsefunction/resetPassword.js'; // This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic. Parse.Cloud.afterSave('contracts_Document', DocumentAftersave); @@ -122,3 +123,4 @@ Parse.Cloud.define('loginuser', loginUser); Parse.Cloud.define('adduser', addUser); Parse.Cloud.define('filterdocs', filterDocs); Parse.Cloud.define('senddeleterequest', sendDeleteUserMail); +Parse.Cloud.define('resetpassword', resetPassword); diff --git a/apps/OpenSignServer/cloud/parsefunction/resetPassword.js b/apps/OpenSignServer/cloud/parsefunction/resetPassword.js new file mode 100644 index 0000000000..cd93650f0d --- /dev/null +++ b/apps/OpenSignServer/cloud/parsefunction/resetPassword.js @@ -0,0 +1,62 @@ +export default async function resetPassword(request) { + const userId = request.params.userId; + const newPassword = request.params.password; + + if (!userId || !newPassword) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide required parameters.'); + } + + if (!request.user) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.'); + } + + if (request.user.id === userId) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Unauthorized to reset your own password.'); + } + + try { + // 1. Get Admin User TenantId + const adminUserQuery = new Parse.Query('contracts_Users'); + adminUserQuery.equalTo('UserId', request.user); + const adminUser = await adminUserQuery.first({ useMasterKey: true }); + + const tenantId = adminUser?.get('TenantId'); + if (!tenantId) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Admin user tenant not found.'); + } + const isAdmin = + adminUser.get('UserRole') === 'contracts_Admin' || + adminUser.get('UserRole') === 'contracts_OrgAdmin'; + if (!isAdmin) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Unauthorized.'); + } + + // 2. Verify the user belongs to the same tenant and is not an admin + const targetUserQuery = new Parse.Query('contracts_Users'); + targetUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId }); + targetUserQuery.equalTo('TenantId', tenantId); + targetUserQuery.notEqualTo('UserRole', 'contracts_Admin'); + const targetUser = await targetUserQuery.first({ useMasterKey: true }); + + if (!targetUser) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found or not allowed.'); + } + + // 3. Update user's password + const userQuery = new Parse.Query(Parse.User); + userQuery.equalTo('objectId', userId); + const user = await userQuery.first({ useMasterKey: true }); + + if (!user) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.'); + } + + user.set('password', newPassword); + await user.save(null, { useMasterKey: true }); + + return { status: 'success', message: 'Password has been reset.' }; + } catch (error) { + console.error('Error while resetting password:', error); + throw error; + } +}