Compare commits

..
Author SHA1 Message Date
Andrew 8833018027 fix: password validation issue 2025-09-26 17:57:57 +00:00
52 changed files with 1541 additions and 2250 deletions
+2 -2
View File
@@ -36,13 +36,13 @@
"react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.7.4",
"react-i18next": "^15.7.3",
"react-konva": "^18.2.12",
"react-pdf": "^9.2.1",
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.9.3",
"react-router": "^7.9.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
@@ -165,8 +165,7 @@
"daily-mail-quota": "Tägliches E-Mail-Kontingent",
"Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden",
"Kiosk Mode": "Kiosk Modus",
"Reset password": "Passwort zurücksetzen"
"Kiosk Mode": "Kiosk Modus"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -902,10 +901,6 @@
"thanks-for-feedback": "Danke für Ihr Feedback 🙏",
"share-your-feedback": "Teilen Sie Ihr Feedback",
"share-your-review": "Teilen Sie Ihre Bewertung",
"please-select-rating": "Bitte wählen Sie eine Bewertung aus",
"feedback-optional": "Feedback (optional)",
"feedback-saved": "Feedback wurde gespeichert.",
"feedback-save-error": "Feedback konnte nicht gespeichert werden, bitte versuchen Sie es erneut.",
"date-format": "Datumsformat",
"document-deleted": "Das Dokument wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
"save-as-template-?": "Sind Sie sicher, dass Sie dieses Dokument als Vorlage speichern möchten?",
@@ -1221,26 +1216,6 @@
"p1": "Dies stellt sicher, dass das Abschlusszertifikat im endgültigen PDF-Dokument enthalten ist. Bitte beachten Sie jedoch, dass das Zertifikat nach dem Zusammenführen nicht mehr vom Hauptdokument getrennt werden kann.",
"p2": "Wenn Sie sich entscheiden, nicht zusammenzuführen, wird das Abschlusszertifikat als separate PDF-Datei zusammen mit dem unterzeichneten Dokument bereitgestellt."
},
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
"set-date": "Datum festlegen",
"set-today": "Unterzeichnungsdatum",
"enter-name": "Name eingeben",
"enter-email": "E-Mail eingeben",
"subscribe-to-opensign-msg": "Abonnieren Sie {{appName}} und genießen Sie unbegrenzt kostenlose digitale Signaturen.",
"duplicate-subscribe-msg": "Mit Duplizieren können Sie eine exakte Kopie der ausgewählten Vorlage erstellen, die Sie wiederverwenden oder ändern können, ohne das Original zu beeinflussen.",
"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.</1>",
"know-more-about": "Mehr erfahren über",
"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"
}
"enter-email": "E-Mail eingeben"
}
@@ -165,8 +165,7 @@
"daily-mail-quota": "Daily Email Quota",
"Save as template": "Save as template",
"Fix & resend": "Fix & Resend",
"Kiosk Mode": "Kiosk Mode",
"Reset password": "Reset password"
"Kiosk Mode": "Kiosk Mode"
},
"report-heading": {
"Sr.No": "Sr.No",
@@ -903,10 +902,6 @@
"thanks-for-feedback": "Thanks for your feedback 🙏",
"share-your-feedback": "Share your feedback",
"share-your-review": "Share your review",
"please-select-rating": "Please select a rating",
"feedback-optional": "Feedback (Optional)",
"feedback-saved": "Feedback has been saved.",
"feedback-save-error": "Unable to save feedback, please try again.",
"date-format": "Date format",
"document-deleted": "The document has been deleted or you don't have access. Please contact the sender.",
"save-as-template-?": "Are you sure you want to save this document as template?",
@@ -1213,8 +1208,8 @@
"enter-region-of-space": "Enter region of space",
"enter-access-key": "Enter access key",
"enter-secret-access-key": "Enter secret access key",
"otp-email": "We've sent a verification code to",
"save-as-temp-warn": "Note: This document includes prefilled widgets, which will be automatically removed as they are already incorporated into the base document.",
"otp-email":"Weve sent a verification code to",
"save-as-temp-warn":"Note: This document includes prefilled widgets, which will be automatically removed as they are already incorporated into the base document.",
"edit-document": "Edit document",
"modify": "Modify",
"merge-certificate-to-pdf": "Merge Certificate to PDF",
@@ -1222,26 +1217,6 @@
"p1": "This will ensure that the completion certificate is included in the final PDF document. However, please note that once merged, the certificate cannot be separated from the main document.",
"p2": "If you choose not to merge, the completion certificate will be provided as a separate PDF file along with the signed document."
},
"read-only-date-error": "Read-only date widget must have a default date.",
"set-date": "Set Date",
"set-today": "Signing Date",
"enter-name": "Enter name",
"enter-email": "Enter email",
"subscribe-to-opensign-msg": "Subscribe to {{appName}} and enjoy unlimited free digital signatures.",
"duplicate-subscribe-msg": "Duplicate lets you create an exact copy of the selected template, allowing you to reuse or modify it without affecting the original.",
"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.</1>",
"know-more-about": "Know more about",
"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"
}
"enter-email": "Enter email"
}
@@ -165,8 +165,7 @@
"daily-mail-quota": "Cuota diaria de correos electrónicos",
"Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar",
"Kiosk Mode": "Modo Kiosco",
"Reset password": "Restablecer contraseña"
"Kiosk Mode": "Modo Kiosco"
},
"report-heading": {
"Sr.No": "Nº",
@@ -903,10 +902,6 @@
"thanks-for-feedback": "Gracias por su comentario 🙏",
"share-your-feedback": "Comparta sus comentarios",
"share-your-review": "Comparta su reseña",
"please-select-rating": "Por favor seleccione una calificación",
"feedback-optional": "Comentarios (opcional)",
"feedback-saved": "Los comentarios han sido guardados.",
"feedback-save-error": "No se pudieron guardar los comentarios, inténtelo de nuevo.",
"date-format": "Formato de fecha",
"document-deleted": "El documento ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
"save-as-template-?": "¿Está seguro de que desea guardar este documento como plantilla?",
@@ -1222,26 +1217,6 @@
"p1": "Esto garantizará que el certificado de finalización esté incluido en el documento PDF final. Sin embargo, tenga en cuenta que una vez combinado, el certificado no se podrá separar del documento principal.",
"p2": "Si elige no combinar, el certificado de finalización se proporcionará como un archivo PDF separado junto con el documento firmado."
},
"read-only-date-error": "El widget de fecha de solo lectura debe tener una fecha predeterminada.",
"set-date": "Establecer fecha",
"set-today": "Fecha de firma",
"enter-name": "Ingrese nombre",
"enter-email": "Ingrese correo electrónico",
"subscribe-to-opensign-msg": "Suscríbase a {{appName}} y disfrute de firmas digitales gratuitas ilimitadas.",
"duplicate-subscribe-msg": "Duplicar le permite crear una copia exacta de la plantilla seleccionada, lo que le permite reutilizarla o modificarla sin afectar al original.",
"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™.</1>",
"know-more-about": "Saber más sobre",
"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"
}
"enter-email": "Ingrese correo electrónico"
}
@@ -200,8 +200,7 @@
"daily-mail-quota": "Quota d'e-mails quotidien",
"Save as template": "Enregistrer comme modèle",
"Fix & resend": "Corriger et renvoyer",
"Kiosk Mode": "Mode Kiosque",
"Reset password": "Réinitialiser le mot de passe"
"Kiosk Mode": "Mode Kiosque"
},
"report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -427,7 +426,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-alert-1": "Le lien de réinitialisation du mot de passe a été envoyé à votre identifiant de messagerie",
"reset-password": "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",
@@ -902,10 +901,6 @@
"thanks-for-feedback": "Merci pour votre retour 🙏",
"share-your-feedback": "Partagez votre avis",
"share-your-review": "Partagez votre avis",
"please-select-rating": "Veuillez sélectionner une note",
"feedback-optional": "Retour (optionnel)",
"feedback-saved": "Le retour a été enregistré.",
"feedback-save-error": "Impossible d'enregistrer le retour, veuillez réessayer.",
"date-format": "Format de date",
"document-deleted": "Le document a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
"save-as-template-?": "Êtes-vous sûr de vouloir enregistrer ce document comme modèle ?",
@@ -1221,26 +1216,6 @@
"p1": "Cela garantira que le certificat de finalisation est inclus dans le document PDF final. Cependant, veuillez noter qu'une fois fusionné, le certificat ne peut plus être séparé du document principal.",
"p2": "Si vous choisissez de ne pas fusionner, le certificat de finalisation sera fourni sous forme de fichier PDF distinct accompagné du document signé."
},
"read-only-date-error": "Le widget de date en lecture seule doit avoir une date par défaut.",
"set-date": "Définir la date",
"set-today": "Date de signature",
"enter-name": "Saisir le nom",
"enter-email": "Saisir l'e-mail",
"subscribe-to-opensign-msg": "Abonnez-vous à {{appName}} et profitez de signatures numériques gratuites illimitées.",
"duplicate-subscribe-msg": "Dupliquer vous permet de créer une copie exacte du modèle sélectionné, que vous pouvez réutiliser ou modifier sans affecter l'original.",
"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 dempêcher les spammeurs dabuser 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™.</1>",
"know-more-about": "En savoir plus sur",
"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"
}
"enter-email": "Saisir l'e-mail"
}
@@ -165,8 +165,7 @@
"daily-mail-quota": "दैनिक ईमेल कोटा",
"Save as template": "टेम्पलेट के रूप में सहेजें",
"Fix & resend": "ठीक करें और पुनः भेजें",
"Kiosk Mode": "कियोस्क मोड",
"Reset password": "पासवर्ड रीसेट करें"
"Kiosk Mode": "कियोस्क मोड"
},
"report-heading": {
"Sr.No": "क्रमांक",
@@ -902,10 +901,6 @@
"thanks-for-feedback": "आपकी प्रतिक्रिया के लिए धन्यवाद 🙏",
"share-your-feedback": "अपनी प्रतिक्रिया साझा करें",
"share-your-review": "अपनी समीक्षा साझा करें",
"please-select-rating": "कृपया एक रेटिंग चुनें",
"feedback-optional": "प्रतिक्रिया (वैकल्पिक)",
"feedback-saved": "प्रतिक्रिया सहेज ली गई है।",
"feedback-save-error": "प्रतिक्रिया सहेजी नहीं जा सकी, कृपया पुनः प्रयास करें।",
"date-format": "दिनांक प्रारूप",
"document-deleted": "दस्तावेज़ हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
"save-as-template-?": "क्या आप वाकई इस दस्तावेज़ को टेम्पलेट के रूप में सहेजना चाहते हैं?",
@@ -1221,26 +1216,7 @@
"p1": "यह सुनिश्चित करेगा कि पूर्णता प्रमाणपत्र अंतिम PDF दस्तावेज़ में शामिल हो। हालाँकि, कृपया ध्यान दें कि एक बार मिलाने के बाद, प्रमाणपत्र को मुख्य दस्तावेज़ से अलग नहीं किया जा सकता।",
"p2": "यदि आप मिलाना नहीं चुनते हैं, तो पूर्णता प्रमाणपत्र हस्ताक्षरित दस्तावेज़ के साथ एक अलग PDF फ़ाइल के रूप में प्रदान किया जाएगा।"
},
"read-only-date-error": "रीड-ओनली दिनांक विजेट में एक डिफ़ॉल्ट दिनांक होना आवश्यक है।",
"set-date": "दिनांक सेट करें",
"set-today": "हस्ताक्षर की तिथि",
"enter-name": "नाम दर्ज करें",
"enter-email": "ईमेल दर्ज करें",
"subscribe-to-opensign-msg": "{{appName}} की सदस्यता लें और असीमित निःशुल्क डिजिटल हस्ताक्षरों का आनंद लें।",
"duplicate-subscribe-msg": "डुप्लिकेट करने से आप चयनित टेम्पलेट की एक सटीक प्रति बना सकते हैं, जिसे आप पुनः उपयोग या संशोधित कर सकते हैं बिना मूल को प्रभावित किए।",
"save-as-template-msg": "इस दस्तावेज़ को एक पुन: प्रयोज्य टेम्पलेट के रूप में सहेजें जिसे आप भविष्य के दस्तावेज़ों के लिए फिर से उपयोग कर सकते हैं।",
"public-credit-alert": "इस लिंक के माध्यम से हस्ताक्षर करने पर आपके निःशुल्क ईमेल क्रेडिट का उपभोग होगा। एक निःशुल्क उपयोगकर्ता के रूप में, आपको स्पैमर्स को हमारी प्रणाली का दुरुपयोग करने से रोकने के लिए प्रति माह 15 ईमेल क्रेडिट आवंटित किए जाते हैं। उच्च सीमा और निर्बाध पहुँच का आनंद लेने के लिए, <1>OpenSign™ पेड प्लान</1> की सदस्यता लें।",
"know-more-about": "इसके बारे में और जानें",
"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": "स्वतः उत्पन्न करें"
}
"enter-email": "ईमेल दर्ज करें"
}
@@ -165,8 +165,7 @@
"daily-mail-quota": "Quota e-mail giornaliera",
"Save as template": "Salva come modello",
"Fix & resend": "Correggi e reinvia",
"Kiosk Mode": "Modalità Kiosk",
"Reset password": "Reimposta password"
"Kiosk Mode": "Modalità Kiosk"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -902,10 +901,6 @@
"thanks-for-feedback": "Grazie per il tuo feedback 🙏",
"share-your-feedback": "Condividi il tuo feedback",
"share-your-review": "Condividi la tua recensione",
"please-select-rating": "Si prega di selezionare una valutazione",
"feedback-optional": "Feedback (opzionale)",
"feedback-saved": "Il feedback è stato salvato.",
"feedback-save-error": "Impossibile salvare il feedback, riprova.",
"date-format": "Formato data",
"document-deleted": "Il documento è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
"save-as-template-?": "Sei sicuro di voler salvare questo documento come modello?",
@@ -1221,26 +1216,6 @@
"p1": "Questo garantirà che il certificato di completamento sia incluso nel documento PDF finale. Tuttavia, si noti che una volta unito, il certificato non può essere separato dal documento principale.",
"p2": "Se scegli di non unire, il certificato di completamento sarà fornito come file PDF separato insieme al documento firmato."
},
"read-only-date-error": "Il widget data di sola lettura deve avere una data predefinita.",
"set-date": "Imposta data",
"set-today": "Data di firma",
"enter-name": "Inserisci nome",
"enter-email": "Inserisci e-mail",
"subscribe-to-opensign-msg": "Abbonati a {{appName}} e goditi firme digitali gratuite illimitate.",
"duplicate-subscribe-msg": "Duplicare consente di creare una copia esatta del modello selezionato, permettendoti di riutilizzarlo o modificarlo senza influire sull'originale.",
"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™.</1>",
"know-more-about": "Scopri di più su",
"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"
}
"enter-email": "Inserisci e-mail"
}
+8 -1
View File
@@ -19,6 +19,7 @@ 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"));
@@ -87,7 +88,13 @@ function App() {
path="/forgetpassword"
element={<Lazy Page={ForgetPassword} />}
/>
<Route element={<HomeLayout />}>
<Route
element={
<ValidateSession>
<HomeLayout />
</ValidateSession>
}
>
<Route path="/users" element={<UserList />} />
<Route
path="/changepassword"
-3
View File
@@ -14,7 +14,6 @@ import { useTranslation } from "react-i18next";
import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../redux/reducers/sidebarReducer.js";
import { sessionStatus } from "../redux/reducers/userReducer.js";
const Header = ({ isConsole, setIsLoggingOut }) => {
const { t, i18n } = useTranslation();
@@ -68,8 +67,6 @@ 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");
+3 -13
View File
@@ -75,12 +75,9 @@ function Header(props) {
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
const mb = Math.round(file?.size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
removeFile(e);
return;
}
@@ -137,13 +134,6 @@ function Header(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
+3 -13
View File
@@ -67,12 +67,9 @@ function PdfTools(props) {
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
const mb = Math.round(file?.size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
removeFile(e);
return;
}
@@ -129,13 +126,6 @@ function PdfTools(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
+146 -301
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, forwardRef } from "react";
import { useState, useEffect } from "react";
import BorderResize from "./BorderResize";
import { Rnd } from "react-rnd";
import {
@@ -15,13 +15,7 @@ import {
cellsWidget,
textWidget,
selectFormat,
randomId,
getYear,
getMonth,
months,
years,
getDefaultDate,
getDefaultFormat
randomId
} from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType";
import moment from "moment";
@@ -35,34 +29,47 @@ import {
} from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
import DatePicker from "react-datepicker";
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
//function to convert formated date to new Date() format
const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formated date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else {
date = new Date();
return date;
}
};
function Placeholder(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const { showGuidelines } = useGuidelinesContext();
const statusArr = ["Required", "Optional"];
const widgetData =
props.pos?.options?.defaultValue || props.pos?.options?.response;
const [isDateModal, setIsDateModal] = useState(false);
const [containerScale, setContainerScale] = useState();
const [selectDate, setSelectDate] = useState({});
const [dateFormatList, setDateFormatList] = useState([]);
const [dateFormat, setDateFormat] = useState([]);
const [clickonWidget, setClickonWidget] = useState({});
const [formdata, setFormdata] = useState({
status: props?.pos?.options?.status || "required",
isReadOnly: props?.pos?.options?.isReadOnly || false
});
const [isToday, setIsToday] = useState(
props.pos?.options?.response === "today" ? true : false
const [isDateReadOnly, setIsDateReadOnly] = useState(
props?.pos?.options?.isReadOnly || false
);
const startDate = props?.pos?.options?.response
? getDefaultDate(
props?.pos?.options?.response,
props.pos?.options?.validation?.format
)
: "";
: new Date();
useEffect(() => {
const getPdfPageWidth = props.pdfOriginalWH.find(
(data) => data.pageNumber === props.pageNumber
@@ -95,87 +102,29 @@ function Placeholder(props) {
}
}, [widgetData]);
//function is used to convert date according to selected format
const handleGetFormatDate = (data, isDateChange) => {
const isSpecialDateFormat =
data?.format &&
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
let updateDate = data.date;
let date = "";
if (isSpecialDateFormat && updateDate) {
date = isDateChange
? moment(updateDate).format(changeDateToMomentFormat(data.format))
: updateDate;
} else if (updateDate) {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
const newDate = new Date(updateDate);
date = moment(newDate.getTime()).format(
changeDateToMomentFormat(data?.format)
);
}
setSelectDate({ date: date, format: data?.format });
return date;
};
const saveDateSetting = (data) => {
if (formdata?.isReadOnly && !selectDate.date && !isToday) {
alert(t("read-only-date-error"));
return;
}
handleSaveDate(data);
setIsDateModal(false);
props.setFontColor("");
props.setFontSize("");
};
//function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => {
const date = isToday ? "today" : handleGetFormatDate(data, isDateChange);
//`onChangeInput` is used to save data related to date in a placeholder field
onChangeInput(
date,
props.pos,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
data?.format,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black",
formdata
);
};
//Date-picker- accept date - Thu Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
//if date have in this format - "30/12/2025"
//then first need to be convert in this format - Tue Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
//Using moment - moment("30/12/2025", "DD/MM/YYYY").toDate();
//moment package where we show date according to format in list
//it support date in this type - "2025-09-11"// ISO string, "2025-09-11T14:30:00" // ISO datetime
//function change format array list with selected date and format
const changeDateFormat = () => {
const updateDate = [];
dateFormatArr.map((data) => {
let date = selectDate?.date || new Date();
const isString = typeof date === "string";
if (selectDate.format === "dd-MM-yyyy" && isString) {
const [day, month, year] = date.split("-");
let date;
if (selectDate && selectDate.format === "dd-MM-yyyy") {
const [day, month, year] = selectDate.date.split("-");
date = new Date(`${year}-${month}-${day}`);
} else if (selectDate.format === "dd.MM.yyyy" && isString) {
const [day, month, year] = date.split(".");
} else if (selectDate && selectDate.format === "dd.MM.yyyy") {
const [day, month, year] = selectDate.date.split(".");
date = new Date(`${year}.${month}.${day}`);
} else if (selectDate.format === "dd/MM/yyyy" && isString) {
const [day, month, year] = date.split("/");
} else if (selectDate && selectDate.format === "dd/MM/yyyy") {
const [day, month, year] = selectDate.date.split("/");
date = new Date(`${year}/${month}/${day}`);
} else {
date = selectDate?.date ? new Date(selectDate?.date) : new Date();
date = new Date(selectDate?.date);
}
const milliseconds = date.getTime();
const newDate = moment(milliseconds).format(data);
const dateObj = { date: newDate, format: selectFormat(data) };
updateDate.push(dateObj);
});
setDateFormatList(updateDate);
setDateFormat(updateDate);
};
useEffect(() => {
@@ -338,6 +287,41 @@ function Placeholder(props) {
}
};
//function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => {
const isSpecialDateFormat =
data?.format &&
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
let updateDate = data.date;
let date;
if (isSpecialDateFormat) {
date = isDateChange
? moment(updateDate).format(changeDateToMomentFormat(data.format))
: updateDate;
} else {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
const newDate = new Date(updateDate);
date = moment(newDate.getTime()).format(
changeDateToMomentFormat(data?.format)
);
}
//`onChangeInput` is used to save data related to date in a placeholder field
onChangeInput(
date,
props.pos,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
data?.format,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black",
isDateReadOnly || false
);
setSelectDate({ date: date, format: data?.format });
};
const setCellCount = (key, newCount) => {
props.setXyPosition((prev) => {
const isSignerList = prev.some((d) => d.signerPtr);
@@ -666,40 +650,6 @@ function Placeholder(props) {
return false;
}
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 md:ml-2 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleChangeFormat = (e) => {
const selectedIndex = e.target.value;
e.stopPropagation();
if (
props?.isPlaceholder &&
props?.data?.Role !== "prefill" &&
!selectDate?.date
) {
setSelectDate((prev) => ({
...prev,
format: dateFormatList[selectedIndex]?.format
}));
} else {
setSelectDate(dateFormatList[selectedIndex]);
}
};
const handleClearDate = () => {
setIsToday(false);
setSelectDate((prev) => ({
...prev,
date: ""
}));
};
return (
<>
{/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */}
@@ -939,8 +889,10 @@ function Placeholder(props) {
(props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key))
}
setSelectDate={setSelectDate}
selectDate={selectDate}
startDate={startDate}
handleSaveDate={handleSaveDate}
xPos={props.xPos}
calculateFont={calculateFont}
setCellCount={setCellCount}
@@ -950,18 +902,22 @@ function Placeholder(props) {
)}
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
<div className="text-base-content h-[100%] p-[20px]">
<div className="flex flex-col gap-3">
<div className="flex flex-col md:items-center md:flex-row gap-y-3">
<div className="flex flex-col md:flex-row md:items-center gap-y-3">
<div className="flex flex-row items-center gap-x-1">
<span className="capitalize">{t("format")} :</span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
defaultValue={""}
onChange={(e) => handleChangeFormat(e)}
onChange={(e) => {
const selectedIndex = e.target.value;
e.stopPropagation();
setSelectDate(dateFormat[selectedIndex]);
}}
>
<option value="" disabled>
{t("select-date-format")}
</option>
{dateFormatList.map((data, ind) => {
{dateFormat.map((data, ind) => {
return (
<option className="text-[13px]" value={ind} key={ind}>
{data?.date ? data?.date : "nodata"}
@@ -969,190 +925,79 @@ function Placeholder(props) {
);
})}
</select>
<span className="text-xs text-gray-400 ml-1">
{selectDate.format}
</span>
</div>
{props?.data?.Role !== "prefill" && props?.isPlaceholder && (
<>
<div className="flex flex-col md:flex-row md:items-center gap-2">
<span>{t("set-date")} :</span>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start md:ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) =>
changeYear(value)
}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={getDefaultDate(
selectDate?.date,
selectDate?.format
)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
onChange={(date) => {
setIsToday(false);
const isDateChange = true;
const dateObj = {
date: date,
format: selectDate.format
};
handleGetFormatDate(dateObj, isDateChange);
}}
dateFormat={
selectDate
? selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
portalId="root-portal"
/>
<label className=" flex items-center gap-1 cursor-pointer">
<input
checked={isToday}
type="checkbox"
className="op-checkbox op-checkbox-xs"
onClick={() => {
setSelectDate((prev) => ({
...prev,
date: ""
}));
setIsToday(!isToday);
}}
/>
<span className="ml-[2px]">{t("set-today")}</span>
</label>
<span
onClick={() => handleClearDate()}
className="underline text-blue-500 cursor-pointer md:ml-2"
>
{t("clear")}
</span>
</div>
<div className="flex flex-row items-center gap-[10px]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
name="status"
onChange={() =>
setFormdata({
...formdata,
status: data.toLowerCase()
})
}
checked={
formdata.status.toLowerCase() === data.toLowerCase()
}
/>
<div className="text-[13px]">
{t(`widget-status.${data}`)}
</div>
</div>
);
})}
</div>
</>
)}
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
<div className="flex flex-row items-center">
<span className="capitalize">{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontSize || clickonWidget.options?.fontSize || 12
}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex flex-row gap-1 items-center">
<span className="capitalize">{t("color")} :</span>
<select
value={
props.fontColor ||
clickonWidget.options?.fontColor ||
"black"
}
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
style={{
background:
props.fontColor || props.pos.options?.fontColor || "black"
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div>
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
<label className="flex items-center gap-1 cursor-pointer">
<input
name="isReadOnly"
type="checkbox"
checked={formdata?.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata({
...formdata,
isReadOnly: !formdata?.isReadOnly
})
}
/>
<span className="capitalize ml-[2px]">{t("read-only")}</span>
</label>
)}
<span className="text-xs text-gray-400 ml-1">
{selectDate.format}
</span>
</div>
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2 mt-3">
<div className="flex flex-row items-center">
<span className="capitalize">{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={props.fontSize || clickonWidget.options?.fontSize || 12}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex flex-row gap-1 items-center">
<span className="capitalize">{t("color")} :</span>
<select
value={
props.fontColor || clickonWidget.options?.fontColor || "black"
}
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
style={{
background:
props.fontColor || props.pos.options?.fontColor || "black"
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div>
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
<div className="flex items-center mt-3">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={
isDateReadOnly || props.pos.options?.isReadOnly || false
}
className="op-checkbox op-checkbox-xs"
onChange={() => setIsDateReadOnly(!isDateReadOnly)}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
)}
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
type="button"
className="op-btn op-btn-primary"
onClick={() => {
saveDateSetting(selectDate);
setIsDateModal(false);
handleSaveDate(selectDate);
props.setFontColor("");
props.setFontSize("");
}}
>
{t("save")}
@@ -34,9 +34,8 @@ function PlaceholderType(props) {
((props.isSelfSign || props.isNeedSign) &&
props.data?.signerObjId === props.signerObjId);
const isReadOnly =
props?.data?.Role !== "prefill" &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId);
props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId;
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
@@ -84,7 +83,8 @@ function PlaceholderType(props) {
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
className={`${isReadOnly ? `select-none opacity-25` : ``} ${selectWidgetCls} overflow-hidden`}
className={`${isReadOnly ? `select-none` : ``} ${selectWidgetCls} overflow-hidden`}
disabled={isReadOnly}
onClick={onClick}
ref={ref}
>
@@ -399,9 +399,11 @@ function PlaceholderType(props) {
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
props?.selectDate?.format ||
props.pos?.options?.validation?.format ||
"MM/dd/yyyy"
props.selectDate
? props.selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
/>
) : (
@@ -410,9 +412,11 @@ function PlaceholderType(props) {
className="select-none-cls overflow-hidden"
>
<span>
{props?.selectDate?.format ||
props.pos?.options?.validation?.format ||
"MM/dd/yyyy"}
{props.selectDate
? props.selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"}
</span>
</div>
);
@@ -53,6 +53,7 @@ function PrefillWidgetModal(props) {
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
const prefillImg = useSelector((state) => state.widget.prefillImg);
const [uniqueWidget, setUniqueWidget] = useState([]);
const [image, setImage] = useState(null);
const [imageLoaders, setImageLoaders] = useState({});
const [currentWidget, setCurrentWidget] = useState("");
@@ -67,7 +68,7 @@ function PrefillWidgetModal(props) {
);
// useMemo to memoize the calculation of unique widgets
const uniqueWidget = useMemo(() => {
const memoizedUniqueWidget = useMemo(() => {
//functions to used remove duplicate name values across all pages
if (!props.prefillData) return [];
//This will help us track which name values have already been encountered across all pages.
@@ -118,6 +119,10 @@ function PrefillWidgetModal(props) {
savePrefillImg();
}, [props.xyPosition]);
useEffect(() => {
setUniqueWidget(memoizedUniqueWidget);
}, [memoizedUniqueWidget]);
useEffect(() => {
if (totalImages > 0 && loadedImages === totalImages) {
setLoading(false);
@@ -85,12 +85,9 @@ function RenderAllPdfPage(props) {
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
const mb = Math.round(file?.size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
removeFile(e);
return;
}
@@ -147,13 +144,6 @@ function RenderAllPdfPage(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -21,9 +21,7 @@ import {
convertTextToImg,
convertJpegToPng,
convertBase64ToFile,
generatePdfName,
getDefaultDate,
getDefaultFormat
generatePdfName
} from "../../constant/Utils";
import CellsWidget from "./CellsWidget";
import DatePicker from "react-datepicker";
@@ -47,6 +45,22 @@ import { emailRegex } from "../../constant/const";
import RegexParser from "regex-parser";
import { saveToMySign } from "../../utils/widgetUtils";
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
//function to convert formatted date to new Date() format
const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formatted date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else {
date = new Date();
return date;
}
};
const fontOptions = [
{ value: "Fasthand" },
{ value: "Dancing Script" },
@@ -161,7 +175,7 @@ function WidgetsValueModal(props) {
currWidgetsDetails?.options?.response,
currWidgetsDetails?.options?.validation?.format
)
: ""
: new Date()
);
useEffect(() => {
dispatch(setScrollTriggerId(currWidgetsDetails?.key));
@@ -242,7 +256,9 @@ function WidgetsValueModal(props) {
setXyPosition,
uniqueId,
false,
data?.format
data?.format,
currWidgetsDetails?.options?.fontSize || 12,
currWidgetsDetails?.options?.fontColor || "black"
);
setSelectDate({ date: date, format: data?.format });
};
@@ -864,14 +880,13 @@ function WidgetsValueModal(props) {
onClick={onClick}
ref={ref}
>
{value ? value : "Select date"}
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
//function to set onchange date
const handleOnDateChange = (date) => {
setWidgetValue(date);
setStartDate(date);
};
const handleOnchangeTextBox = (e) => {
@@ -1455,59 +1470,52 @@ function WidgetsValueModal(props) {
);
case "date":
return (
<div className="flex flex-col">
<div
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={startDate}
onChange={(date) => handleOnDateChange(date)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
selectDate
? selectDate?.format
: currWidgetsDetails?.options?.validation?.format
? currWidgetsDetails?.options?.validation?.format
: "MM/dd/yyyy"
}
portalId="root-portal"
/>
</div>
<div className="flex justify-center">
<span className="text-gray-300">
{currWidgetsDetails?.options?.validation?.format}
</span>
</div>
<div
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={startDate}
onChange={(date) => handleOnDateChange(date)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
selectDate
? selectDate?.format
: currWidgetsDetails?.options?.validation?.format
? currWidgetsDetails?.options?.validation?.format
: "MM/dd/yyyy"
}
portalId="root-portal"
/>
</div>
);
case "email":
@@ -1587,6 +1595,9 @@ function WidgetsValueModal(props) {
const isCheckBox =
!currWidgetsDetails.options?.isReadOnly &&
currWidgetsDetails.type === "checkbox";
const isRadio =
!currWidgetsDetails?.options?.isReadOnly &&
currWidgetsDetails?.type === radioButtonWidget;
if (isCheckBox) {
//get minimum required count if exist
const minCount =
+22 -55
View File
@@ -410,14 +410,9 @@ export const changeDateToMomentFormat = (format) => {
return "L";
}
};
export const addWidgetOptions = (
type,
signer,
placeholder,
role,
widgetValue
) => {
export const addWidgetOptions = (type, signer, placeholder, widgetValue) => {
let defaultOpt;
//condition to handle widgets name field
if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length;
@@ -477,7 +472,7 @@ export const addWidgetOptions = (
: "MM/dd/yyyy";
return {
...defaultOpt,
response: role === "prefill" ? getDate(signer?.DateFormat) : "",
response: getDate(signer?.DateFormat),
validation: { format: dateFormat, type: "date-format" }
};
}
@@ -1007,7 +1002,7 @@ export const onChangeInput = (
dateFormat,
fontSize,
fontColor,
dateDetails
isDateReadOnly
) => {
const isSigners = xyPosition.some(
(data) => data.signerPtr || data.Role === "prefill"
@@ -1039,18 +1034,9 @@ export const onChangeInput = (
options: {
...position.options,
response: value,
fontSize: fontSize ? fontSize : position.options?.fontSize,
fontColor: fontColor
? fontColor
: position.options?.fontColor,
isReadOnly:
dateDetails?.isReadOnly !== "undefined"
? dateDetails?.isReadOnly
: position.options?.isReadOnly,
status:
dateDetails?.status !== "undefined"
? dateDetails?.status
: position.options?.status,
fontSize: fontSize,
fontColor: fontColor,
isReadOnly: isDateReadOnly || false,
validation: {
type: "date-format",
format: dateFormat // This indicates the required date format explicitly.
@@ -2010,17 +1996,7 @@ export const embedWidgetsToDoc = async (
} else if (isTextTypeWidget) {
let textContent = "";
if (position?.options?.response) {
if (
position.type === "date" &&
position.options?.response === "today"
) {
const getFormat = changeDateToMomentFormat(
position?.options?.validation?.format
);
textContent = moment().format(getFormat);
} else {
textContent = position.options?.response;
}
textContent = position.options?.response;
} else if (position?.options?.defaultValue) {
textContent = position?.options?.defaultValue;
}
@@ -3472,7 +3448,18 @@ export const updateDateWidgetsRes = (
placeHolder: item?.placeHolder?.map((ph) => ({
...ph,
pos: ph?.pos?.map((widget) => {
if (
// only for date widgets *and* missing response
if (widget.type === "date" && !widget.options.response) {
return {
...widget,
options: {
...widget.options,
response: getDate(
changeDateToMomentFormat(widget.options.validation.format)
)
}
};
} else if (
["name", "email", "job title", "company"].includes(widget.type) &&
!widget.options.defaultValue &&
!widget.options.response
@@ -3606,7 +3593,6 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
//in `defaultValue` variable is used to get how many checkbox checked by default
const defaultValue =
requiredCheckbox[i].options?.defaultValue?.length;
const checkboxValue = response ? response : defaultValue;
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box
if (
parseMin > 0 &&
@@ -3621,10 +3607,7 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
setminRequiredCount(parseMin);
}
//else condition to validate minimum required checkbox
else if (
parseMin > 0 &&
(parseMin > checkboxValue || !checkboxValue)
) {
else if (parseMin > 0 && (parseMin > response || !response)) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
@@ -3823,7 +3806,7 @@ export const sendEmailToSigners = async (
htmlReqBody,
variables
);
} else if (defaultMail?.body && defaultMail?.subject) {
} else if (defaultMail) {
const mailBody = defaultMail?.body;
const mailSubject = defaultMail.subject;
const replacedRequestBody = mailBody.replace(/"/g, "'");
@@ -4051,19 +4034,3 @@ export const handleDeleteWidget = (key, Id, pageNumber, signerPos) => {
}
}
};
//function to convert formatted date to new Date() format
export const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formatted date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else if (dateStr === "today") {
date = new Date();
return date;
}
};
//function to get default format
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
+1 -1
View File
@@ -9,7 +9,7 @@ export const getThemeIconColor = () => {
return theme === "opensigndark" ? "#CCCCCC" : "#686968";
};
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const maxFileSize = 10; // 10MB
export const maxTitleLength = 250; // 250 characters
export const maxNoteLength = 200; // 200 characters
export const maxDescriptionLength = 500; // 500 characters
export const maxFileSize = 80; // for cloud 10MB / 80MB for self-hosted
+84 -67
View File
@@ -4,22 +4,21 @@ import Footer from "../components/Footer";
import Sidebar from "../components/sidebar/Sidebar";
import Tour from "../primitives/Tour";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import Parse from "parse";
import { useNavigate, Outlet } from "react-router";
import ModalUi from "../primitives/ModalUi";
import { useNavigate, useLocation, 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 dispatch = useDispatch();
const location = useLocation();
const arr = useSelector((state) => state.TourSteps);
const isValidSession = useSelector((state) => state.user.isValidSession);
const [isUserValid, setIsUserValid] = useState(true);
const [isLoader, setIsLoader] = useState(true);
const [isCloseBtn, setIsCloseBtn] = useState(true);
const [isTour, setIsTour] = useState(false);
@@ -36,31 +35,28 @@ const HomeLayout = () => {
}, []);
useEffect(() => {
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));
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);
}
})();
}
} catch (error) {
// Session token is invalid or there was an error
setIsUserValid(false);
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -173,56 +169,77 @@ const HomeLayout = () => {
}
}
return isValidSession && localStorage.getItem("accesstoken") ? (
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 (
<div className="flex flex-col h-screen overflow-hidden">
{/* HEADER */}
<header className="z-[501]">
{!isLoader && <Header setIsLoggingOut={setIsLoggingOut} />}
</header>
{isLoader ? (
<div className="flex h-[100vh] justify-center items-center">
<Loader />
</div>
) : (
{isUserValid ? (
<>
{isLoggingOut && (
<div className="inset-0 bg-black/30 z-[1000] fixed flex justify-center items-center">
{isLoader ? (
<div className="flex h-[100vh] justify-center items-center">
<Loader />
</div>
)}
{/* BODY */}
<div className="flex flex-1 overflow-hidden">
{/* SIDEBAR with width animation */}
<Sidebar />
{/* MAIN (includes both content + footer in one scrollable column) */}
<main
id="renderList"
className="flex-1 overflow-auto transition-all duration-300 ease-in-out"
>
<div className="flex flex-col min-h-full">
{/* your page content */}
<div className="p-3">{<Outlet />}</div>
{/* sticky-but-scrollable footer */}
<div className="mt-auto z-30">
<Footer />
) : (
<>
{isLoggingOut && (
<div className="inset-0 bg-black/30 z-[1000] fixed flex justify-center items-center">
<Loader />
</div>
)}
{/* BODY */}
<div className="flex flex-1 overflow-hidden">
{/* SIDEBAR with width animation */}
<Sidebar />
{/* MAIN (includes both content + footer in one scrollable column) */}
<main
id="renderList"
className="flex-1 overflow-auto transition-all duration-300 ease-in-out"
>
<div className="flex flex-col min-h-full">
{/* your page content */}
<div className="p-3">{<Outlet />}</div>
{/* sticky-but-scrollable footer */}
<div className="mt-auto z-30">
<Footer />
</div>
</div>
</main>
</div>
</main>
</div>
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
disableKeyboardNavigation={["esc"]}
// disableInteraction={true}
scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
disableKeyboardNavigation={["esc"]}
// disableInteraction={true}
scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
</>
)}
</>
) : (
<ModalUi showHeader={false} isOpen={true} showClose={false}>
<div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5">
<p className="text-xl font-medium">{t("session-expired")}</p>
<button onClick={handleLoginBtn} className="op-btn op-btn-neutral">
{t("login")}
</button>
</div>
</ModalUi>
)}
</div>
) : (
<SessionExpiredModal />
);
};
+1 -1
View File
@@ -253,7 +253,7 @@ const AddAdmin = () => {
/[A-Z]/.test(newPassword) &&
/\d/.test(newPassword)
);
setSpecialCharValid(/[!@#$%^&*()\-_=+{};:,<.>]/.test(newPassword));
setSpecialCharValid(/[!@#$%^&*()\-_=+{};:,<.>?]/.test(newPassword));
};
const subscribeNewsletter = async () => {
try {
+26 -1
View File
@@ -4,6 +4,18 @@ import { Navigate } from "react-router";
import { useTranslation } from "react-i18next";
function ChangePassword() {
const [lengthValid, setLengthValid] = useState(false);
const [caseDigitValid, setCaseDigitValid] = useState(false);
const [specialCharValid, setSpecialCharValid] = useState(false);
const handlePasswordChange = (e) => {
const value = e.target.value;
setnewpassword(value);
setLengthValid(value.length >= 8);
setCaseDigitValid(
/[a-z]/.test(value) && /[A-Z]/.test(value) && /\d/.test(value)
);
setSpecialCharValid(/[!@#$%^&*()\-_=+{};:,<.>?]/.test(value));
};
const { t } = useTranslation();
const [currentpassword, setCurrentPassword] = useState("");
const [newpassword, setnewpassword] = useState("");
@@ -84,13 +96,26 @@ function ChangePassword() {
type="password"
name="newpassword"
value={newpassword}
onChange={(e) => setnewpassword(e.target.value)}
onChange={handlePasswordChange}
className="op-input op-input-bordered op-input-sm text-xs w-full"
placeholder={t("new-password")}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
{newpassword.length > 0 && (
<div className="mt-1 text-[11px]">
<p className={lengthValid ? "text-green-600" : "text-red-600"}>
{lengthValid ? "✓" : "✗"} Password length 8
</p>
<p className={caseDigitValid ? "text-green-600" : "text-red-600"}>
{caseDigitValid ? "✓" : "✗"} Uppercase, lowercase, and number
</p>
<p className={specialCharValid ? "text-green-600" : "text-red-600"}>
{specialCharValid ? "✓" : "✗"} Special character
</p>
</div>
)}
</div>
<div>
<label htmlFor="newpassword" className="text-xs block ml-1">
+1 -1
View File
@@ -27,7 +27,7 @@ function processDimensions(x, y, width, height) {
};
}
const DebugPdf = () => {
const { t } = useTranslation();
const {t} = useTranslation()
const { width } = useWindowSize();
const [pdf, setPdf] = useState("");
const [isModal, setIsModal] = useState(true);
@@ -13,6 +13,18 @@ import { useTranslation } from "react-i18next";
import Loader from "../primitives/Loader";
function ForgotPassword() {
const [lengthValid, setLengthValid] = useState(false);
const [caseDigitValid, setCaseDigitValid] = useState(false);
const [specialCharValid, setSpecialCharValid] = useState(false);
const handlePasswordChange = (event) => {
let value = event.target.value;
setState({ ...state, password: value });
setLengthValid(value.length >= 8);
setCaseDigitValid(
/[a-z]/.test(value) && /[A-Z]/.test(value) && /\d/.test(value)
);
setSpecialCharValid(/[!@#$%^&*()\-_=+{};:,<.>?]/.test(value));
};
const { t } = useTranslation();
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -124,6 +136,28 @@ function ForgotPassword() {
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-center text-xs font-bold">
<input
type="password"
name="password"
className="op-input op-input-bordered op-input-sm w-full mt-2"
value={state.password}
onChange={handlePasswordChange}
placeholder="New password"
required
/>
{state.password.length > 0 && (
<div className="mt-1 text-[11px]">
<p className={lengthValid ? "text-green-600" : "text-red-600"}>
{lengthValid ? "✓" : "✗"} Password length 8
</p>
<p className={caseDigitValid ? "text-green-600" : "text-red-600"}>
{caseDigitValid ? "✓" : "✗"} Uppercase, lowercase, and number
</p>
<p className={specialCharValid ? "text-green-600" : "text-red-600"}>
{specialCharValid ? "✓" : "✗"} Special character
</p>
</div>
)}
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
+9 -26
View File
@@ -30,8 +30,6 @@ 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() {
@@ -52,7 +50,6 @@ 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({
@@ -101,8 +98,6 @@ const Forms = (props) => {
extUserData?.IsTourEnabled === false
? "false"
: "true";
const fileSize =
maxFileSize;
useEffect(() => {
handleReset();
return () => abortController.abort();
@@ -141,10 +136,11 @@ const Forms = (props) => {
return;
}
// setFormData((prev) => ({ ...prev, file: files[0] }));
const totalBytes = Math.round(files.reduce((sum, f) => sum + f.size, 0)); // in bytes
const fileSizeBytes = fileSize * 1024 * 1024;
if (totalBytes > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
const totalMb = Math.round(
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
);
if (totalMb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
setFileUpload("");
setSelectedFiles([]);
removeFile(e);
@@ -280,11 +276,7 @@ const Forms = (props) => {
console.log("err in docx to pdf ", err);
const error =
t("docx-error");
if (err?.code === 209) {
dispatch(sessionStatus(false));
} else {
alert(error);
}
alert(error);
return;
}
}
@@ -342,13 +334,8 @@ const Forms = (props) => {
setSelectedFiles([]);
}
} catch (error) {
if (error?.code === 209) {
dispatch(sessionStatus(false));
} else {
alert(error.message);
}
alert(error.message);
setSelectedFiles([]);
removeFile(e);
}
};
// `isValidURL` is used to check valid webhook url
@@ -483,9 +470,7 @@ const Forms = (props) => {
}
} catch (err) {
console.log("err ", err);
if (err?.code === 209) {
dispatch(sessionStatus(false));
} else if (err.message === "only 15 reminder allowed") {
if (err.message === "only 15 reminder allowed") {
setIsAlert({
type: "danger",
message: t("only-15-reminder-allowed")
@@ -617,9 +602,7 @@ const Forms = (props) => {
}
} catch (err) {
removeFile();
if (err?.code === 209) {
dispatch(sessionStatus(false));
} else if (err?.response?.status === 401) {
if (err?.response?.status === 401) {
setIsPassword(true);
setIsCorrectPass(false);
} else {
+26 -1
View File
@@ -23,6 +23,18 @@ import { useTranslation } from "react-i18next";
import SelectLanguage from "../components/pdf/SelectLanguage";
function Login() {
const [lengthValid, setLengthValid] = useState(false);
const [caseDigitValid, setCaseDigitValid] = useState(false);
const [specialCharValid, setSpecialCharValid] = useState(false);
const handlePasswordChange = (event) => {
let value = event.target.value;
setState({ ...state, password: value });
setLengthValid(value.length >= 8);
setCaseDigitValid(
/[a-z]/.test(value) && /[A-Z]/.test(value) && /\d/.test(value)
);
setSpecialCharValid(/[!@#$%^&*()\-_=+{};:,<.>?]/.test(value));
};
const appName =
"OpenSign™";
const { t, i18n } = useTranslation();
@@ -474,7 +486,7 @@ function Login() {
name="password"
value={state.password}
autoComplete="current-password"
onChange={handleChange}
onChange={handlePasswordChange}
onInvalid={(e) =>
e.target.setCustomValidity(
t("input-required")
@@ -495,6 +507,19 @@ function Login() {
</span>
</div>
<div className="relative mt-1">
{state.password.length > 0 && (
<div className="mt-1 text-[11px]">
<p className={lengthValid ? "text-green-600" : "text-red-600"}>
{lengthValid ? "✓" : "✗"} Password length 8
</p>
<p className={caseDigitValid ? "text-green-600" : "text-red-600"}>
{caseDigitValid ? "✓" : "✗"} Uppercase, lowercase, and number
</p>
<p className={specialCharValid ? "text-green-600" : "text-red-600"}>
{specialCharValid ? "✓" : "✗"} Special character
</p>
</div>
)}
<NavLink
to="/forgetpassword"
className="text-[13px] op-link op-link-primary underline-offset-1 focus:outline-none ml-1"
@@ -1512,7 +1512,6 @@ function PdfRequestFiles(
dragTypeValue,
owner,
filterSignerPos?.placeHolder,
null,
widgetValue
),
Width: widgetWidth / (containerScale * scale),
@@ -1549,7 +1548,6 @@ function PdfRequestFiles(
dragTypeValue,
owner,
filterSignerPos?.placeHolder,
null,
widgetValue
),
Width: widgetWidth / (containerScale * scale),
+3 -6
View File
@@ -553,8 +553,7 @@ function PlaceHolderSign() {
options: addWidgetOptions(
dragTypeValue,
owner,
filterSignerPos?.placeHolder,
roleName
filterSignerPos?.placeHolder
),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
@@ -589,8 +588,7 @@ function PlaceHolderSign() {
options: addWidgetOptions(
dragTypeValue,
owner,
filterSignerPos?.placeHolder,
roleName
filterSignerPos?.placeHolder
),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
@@ -2047,12 +2045,12 @@ function PlaceHolderSign() {
setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber}
pdfBase64Url={pdfBase64Url}
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url}
setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer}
isMergePdfBtn={true}
pdfDetails={pdfDetails}
/>
{/* pdf render view */}
<div className="w-full md:w-[57%] flex mr-4">
@@ -2072,7 +2070,6 @@ function PlaceHolderSign() {
setAllPages={setAllPages}
setPageNumber={setPageNumber}
setIsTour={setPlaceholderTour}
pdfDetails={pdfDetails}
/>
<div className=" w-full md:w-[95%] ">
{/* this modal is used show alert set placeholder for all signers before send mail */}
-1
View File
@@ -37,7 +37,6 @@ const Report = () => {
// Number of documents to fetch per API call
const docLimit = 20;
// below useEffect is call when id param change
useEffect(() => {
setReportName("");
+1 -2
View File
@@ -1250,12 +1250,12 @@ function SignYourSelf() {
pageNumber={pageNumber}
containerWH={containerWH}
pdfBase64Url={pdfBase64Url}
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url}
setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer}
isMergePdfBtn={!pdfDetails?.[0]?.IsCompleted}
pdfDetails={pdfDetails}
/>
<div className="w-full md:w-[57%] flex mr-4">
<PdfTools
@@ -1273,7 +1273,6 @@ function SignYourSelf() {
setAllPages={setAllPages}
setPageNumber={setPageNumber}
isDisableEditTools={isCompleted}
pdfDetails={pdfDetails}
/>
<div className="w-full md:w-[95%]">
<ModalUi
@@ -348,8 +348,8 @@ const TemplatePlaceholder = () => {
setPrefillSigner([utils?.prefillObj()]);
}
setSignersData(updatedSigners);
setUniqueId(updatedSigners[0]?.Id);
setBlockColor(updatedSigners[0]?.blockColor);
setUniqueId(updatedSigners[0].Id);
setBlockColor(updatedSigners[0].blockColor);
setIsSelectId(0);
} else {
const updatedSigners = documentData[0].Signers.map((x, index) => ({
@@ -358,8 +358,8 @@ const TemplatePlaceholder = () => {
Role: "Role " + (index + 1)
}));
setSignersData(updatedSigners);
setUniqueId(updatedSigners[0]?.Id);
setBlockColor(updatedSigners[0]?.blockColor);
setUniqueId(updatedSigners[0].Id);
setBlockColor(updatedSigners[0].blockColor);
setPrefillSigner([utils?.prefillObj()]);
}
} else {
@@ -437,6 +437,7 @@ const TemplatePlaceholder = () => {
const addPositionOfSignature = (item, monitor) => {
getSignerPos(item, monitor);
};
// `getSignerPos` is used to get placeholder position when user place it and save it in array
const getSignerPos = (item, monitor) => {
if (uniqueId) {
@@ -482,8 +483,7 @@ const TemplatePlaceholder = () => {
options: addWidgetOptions(
dragTypeValue,
owner,
filterSignerPos?.placeHolder,
roleName
filterSignerPos?.placeHolder
),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
@@ -517,8 +517,7 @@ const TemplatePlaceholder = () => {
options: addWidgetOptions(
dragTypeValue,
owner,
filterSignerPos?.placeHolder,
roleName
filterSignerPos?.placeHolder
),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
@@ -615,6 +614,7 @@ const TemplatePlaceholder = () => {
setIsAddRole(true);
}
};
const tourAddRole = [
{
selector: '[data-tut="reactourAddbtn"]',
@@ -1774,12 +1774,12 @@ const TemplatePlaceholder = () => {
setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber}
pdfBase64Url={pdfBase64Url}
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url}
setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer}
isMergePdfBtn={true}
pdfDetails={pdfDetails}
/>
{/* pdf render view */}
@@ -1799,7 +1799,6 @@ const TemplatePlaceholder = () => {
allPages={allPages}
setAllPages={setAllPages}
setPageNumber={setPageNumber}
pdfDetails={pdfDetails}
/>
<div className="w-full md:w-[95%]">
<ModalUi
+7 -51
View File
@@ -12,7 +12,6 @@ import {
} from "react-i18next";
import DeleteUserModal from "../primitives/DeleteUserModal";
import axios from "axios";
import PasswordResetModal from "../primitives/PasswordResetModal";
const actions = [
{
@@ -24,14 +23,6 @@ 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 = () => {
@@ -217,9 +208,9 @@ const UserList = () => {
};
// `showAlert` handle show/hide alert
const showAlert = (type, msg, timer = 1500) => {
const showAlert = (type, msg) => {
setIsAlert({ type, msg });
setTimeout(() => setIsAlert({ type: "success", msg: "" }), timer);
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 1500);
};
const handleDeleteAccount = async (item) => {
@@ -256,15 +247,13 @@ const UserList = () => {
};
const handleActionBtn = async (act, item) => {
setIsActModal({ [`${act.action}_${item.objectId}`]: true });
if (act.action === "delete") {
setIsActModal({ [`delete_${item.objectId}`]: true });
}
};
const handleBtnVisibility = (act, item) => {
if (act.restrictAdmin) {
if (item?.UserRole === "contracts_Admin") {
return false;
} else {
return item?.objectId !== extClass?.[0]?.objectId;
}
return item?.objectId !== extClass?.[0]?.objectId;
} else if (
act.restrictBtn === true &&
item?.objectId === extClass?.[0]?.objectId
@@ -274,28 +263,6 @@ const UserList = () => {
return true;
}
};
const handleActiveToggleVisibility = (item) => {
if (item?.UserRole === "contracts_Admin") {
return false;
} else {
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 (
<div className="relative">
{isLoader && (
@@ -373,7 +340,7 @@ const UserList = () => {
<td className="px-4 py-2">
{formatRow(item.TeamIds)}
</td>
{handleActiveToggleVisibility(item) ? (
{item.UserRole !== "contracts_Admin" ? (
<td className="px-4 py-2 font-semibold">
<label className="cursor-pointer relative block items-center mb-0">
<input
@@ -469,17 +436,6 @@ const UserList = () => {
deleteRes={deleteUserRes}
handleClose={handleCloseModal}
/>
<PasswordResetModal
isOpen={
isActModal[
"resetpassword_" + item.objectId
]
}
userId={item?.UserId?.objectId}
onClose={handleCloseModal}
onSubmit={submitPassword}
showAlert={showAlert}
/>
</React.Fragment>
))}
</div>
+2 -4
View File
@@ -9,12 +9,10 @@ const ModalUi = ({
showHeader = true,
showClose = true,
reduceWidth,
position,
crossColor
position
}) => {
const width = reduceWidth;
const isBottom = position === "bottom" ? "items-end pb-2 !bg-black/10" : "";
const crossBtnColor = crossColor ?? "text-base-content";
return (
<>
{isOpen && (
@@ -40,7 +38,7 @@ const ModalUi = ({
)}
{showClose && (
<button
className={`${crossBtnColor} op-btn op-btn-sm op-btn-circle op-btn-ghost absolute right-2 top-2 z-40`}
className="op-btn op-btn-sm op-btn-circle op-btn-ghost text-base-content absolute right-2 top-2"
onClick={() => handleClose && handleClose()}
>
@@ -1,205 +0,0 @@
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> | 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 (
<>
<ModalUi
isOpen={isOpen}
title={t("reset-password")}
handleClose={() => {
setPassword("");
onClose?.();
}}
>
<form onSubmit={handleSubmit} className="space-y-3 pt-[15px] p-[20px]">
<p className="text-sm text-base-content/60">
{t("enter-strong-password")}
</p>
<label className="op-form-control w-full">
<div className="op-label">
<span className="op-label-text">{t("new-password")}</span>
</div>
<div className="flex gap-2">
<input
type="text"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder="Enter password or click Autogenerate"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoFocus
/>
<button
type="button"
className="op-btn op-btn-square op-btn-sm"
onClick={handleCopy}
title={copied ? "Copied" : "Copy"}
aria-label="Copy password"
disabled={!password}
>
<i
className={`fa-regular fa-copy ${copied ? "opacity-70" : ""}`}
></i>
</button>
</div>
</label>
{/* When everything is valid, show just a right tick */}
<ul className="mt-1 ml-2">
{conditions.map((c) => (
<li
key={c.key}
className={`${c.ok ? "text-success" : "text-error"} text-[12px] leading-snug`}
>
{c.ok ? "✓" : "✗"} {t(c.text)}
</li>
))}
</ul>
<div className="pt-2 flex flex-row gap-x-2">
<button
type="submit"
className="op-btn op-btn-primary"
disabled={!allValid || submitting}
>
{t("submit")}
</button>
<button type="button" className="op-btn" onClick={handleAutogen}>
{t("autogenerate")}
</button>
<button
type="button"
className="op-btn op-btn-ghost"
onClick={() => {
setPassword("");
onClose?.();
}}
>
{t("cancel")}
</button>
</div>
</form>
</ModalUi>
</>
);
}
+31 -5
View File
@@ -1,9 +1,12 @@
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import Parse from "parse";
import { Outlet } from "react-router";
import SessionExpiredModal from "./SessionExpiredModal";
import { Outlet, useNavigate, useLocation } from "react-router";
import ModalUi from "./ModalUi";
import { useTranslation } from "react-i18next";
const Validate = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const location = useLocation();
const [isUserValid, setIsUserValid] = useState(true);
useEffect(() => {
(async () => {
@@ -34,7 +37,30 @@ const Validate = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return isUserValid ? <Outlet /> : <SessionExpiredModal />;
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 ? (
<div>
<Outlet />
</div>
) : (
<ModalUi showHeader={false} isOpen={true} showClose={false}>
<div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5">
<p className="text-xl font-medium">{t("session-expired")}</p>
<button onClick={handleLoginBtn} className="op-btn op-btn-neutral">
{t("login")}
</button>
</div>
</ModalUi>
);
};
export default Validate;
@@ -1,29 +1,26 @@
import React from "react";
import { useTranslation } from "react-i18next";
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";
import { useNavigate } from "react-router";
import Parse from "parse";
const SessionExpiredModal = () => {
const ValidateSession = ({ children }) => {
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");
dispatch(sessionStatus(true));
navigate("/", { replace: true, state: { from: location } });
navigate("/", { replace: true });
}
};
return (
return localStorage.getItem("accesstoken") ? (
children
) : (
<ModalUi showHeader={false} isOpen={true} showClose={false}>
<div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5">
<p className="text-xl font-medium">{t("session-expired")}</p>
@@ -35,4 +32,4 @@ const SessionExpiredModal = () => {
);
};
export default SessionExpiredModal;
export default ValidateSession;
@@ -1,15 +0,0 @@
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;
+1 -3
View File
@@ -4,7 +4,6 @@ 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: {
@@ -12,7 +11,6 @@ export const store = configureStore({
TourSteps: TourStepsReducer,
ShowTenant,
widget: widgetReducer,
sidebar: sidebarReducer,
user: userReducer
sidebar: sidebarReducer
}
});
@@ -106,15 +106,6 @@ 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 = [];
@@ -339,8 +330,6 @@ const DocumentsReport = (props) => {
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
}
};
// Get current list
const indexOfLastDoc = currentPage * props.docPerPage;
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
@@ -803,7 +792,6 @@ const DocumentsReport = (props) => {
};
const handleCloseModal = () => {
setError("");
setIsModal({});
};
const handleSaveAsTemplate = async (doc) => {
@@ -1183,7 +1171,6 @@ const DocumentsReport = (props) => {
<React.Fragment key={index}>
{handleBtnVisibility(act, item) && (
<div
data-dropdown-root="1"
role="button"
data-tut={act?.selector}
onClick={() => handleActionBtn(act, item)}
@@ -30,7 +30,7 @@ import "../../styles/quill.css";
import BulkSendUi from "../../components/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import { Trans, useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import { useElSize } from "../../hook/useElSize";
import LottieWithLoader from "../../primitives/DotLottieReact";
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
@@ -67,7 +67,7 @@ const TemplatesReport = (props) => {
const [userDetails, setUserDetails] = useState({});
const [isNextStep, setIsNextStep] = useState({});
const [isBulkSend, setIsBulkSend] = useState({});
const [templateDetails, setTemplateDetails] = useState({});
const [templateDeatils, setTemplateDetails] = useState({});
const [placeholders, setPlaceholders] = useState([]);
const [isLoader, setIsLoader] = useState({});
const [isShareWith, setIsShareWith] = useState({});
@@ -165,15 +165,6 @@ 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 {
@@ -248,8 +239,9 @@ const TemplatesReport = (props) => {
);
if (checkIsSignatureExist) {
setActLoader({ [`${item.objectId}_${act.btnId}`]: true });
const template = await fetchTemplate(item.objectId);
const templateData = template.data && template.data.result;
const templateDeatils = await fetchTemplate(item.objectId);
const templateData =
templateDeatils.data && templateDeatils.data.result;
if (!templateData.error) {
setXyPosition(templateData?.Placeholders);
const signer = utils.handleSignersList(templateData);
@@ -292,7 +284,7 @@ const TemplatesReport = (props) => {
}
}
);
if (axiosRes?.data) {
if (axiosRes) {
return axiosRes;
}
} catch (e) {
@@ -305,10 +297,10 @@ const TemplatesReport = (props) => {
const navigatePageToDoc = async (templateRes, placeholder, signer) => {
setIsPrefillModal({});
const res = await createDocument(
[templateRes || templateDetails],
[templateRes || templateDeatils],
placeholder || xyPosition,
signer || signerList,
templateRes?.URL || templateDetails?.URL
templateRes?.URL || templateDeatils?.URL
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -887,8 +879,8 @@ const TemplatesReport = (props) => {
signerList,
setIsPrefillModal,
scale,
templateDetails?.URL,
[templateDetails],
templateDeatils?.URL,
[templateDeatils],
prefillImg,
extClass?.[0]?.UserId?.objectId,
);
@@ -1161,7 +1153,6 @@ 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={
@@ -1341,7 +1332,7 @@ const TemplatesReport = (props) => {
) : (
<BulkSendUi
Placeholders={placeholders}
item={templateDetails}
item={templateDeatils}
handleClose={handleQuickSendClose}
signatureType={signatureType}
/>
-1
View File
@@ -3,7 +3,6 @@ import { format, toZonedTime } from 'date-fns-tz';
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
import crypto from 'node:crypto';
import { PDFDocument, rgb } from 'pdf-lib';
import { parseUploadFile } from './utils/fileUtils.js';
dotenv.config({ quiet: true });
@@ -13,8 +13,8 @@ export const app = express();
dotenv.config({ quiet: true });
app.use(cors());
app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ limit: '100mb', extended: true }));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.post('/file_upload', uploadFile);
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
@@ -32,7 +32,7 @@ const resetPasswordAndDeleteSession = async userId => {
await Parse.Object.destroyAll(sessions, { useMasterKey: true });
}
};
export async function deleteUser(userId, adminId, adminTenantId, isOrgAdmin, orgPtr) {
export async function deleteUser(userId, adminId) {
const userPointer = { __type: 'Pointer', className: '_User', objectId: userId };
let userDetails = {
UserRole: 'not found',
@@ -47,18 +47,13 @@ export async function deleteUser(userId, adminId, adminTenantId, isOrgAdmin, org
const Users = Parse.Object.extend('contracts_Users');
const userQuery = new Parse.Query(Users);
userQuery.equalTo('UserId', userPointer);
if (adminTenantId) {
userQuery.equalTo('TenantId', adminTenantId);
if (isOrgAdmin && orgPtr) {
userQuery.equalTo('OrganizationId', orgPtr);
}
} else if (adminId) {
if (adminId) {
userQuery.equalTo('CreatedBy', { __type: 'Pointer', className: '_User', objectId: adminId });
}
const userResult = await userQuery.first({ useMasterKey: true });
userDetails = { ...userDetails, UserId: userId };
if (!userResult) {
const errorMessage = isOrgAdmin ? 'Unauthorized.' : 'User not found.';
const errorMessage = 'User not found.';
return { code: 400, message: errorMessage };
}
const contractsUserId = userResult.id;
@@ -66,10 +61,6 @@ export async function deleteUser(userId, adminId, adminTenantId, isOrgAdmin, org
const teamIds = userResult.get('TeamIds') || [];
const organizationId = userResult.get('OrganizationId')?.id;
const isAdmin = userResult?.get('UserRole') === 'contracts_Admin' ? true : false;
if (isOrgAdmin && isAdmin) {
const errorMessage = 'Unauthorized.';
return { code: 400, message: errorMessage };
}
userDetails = {
...userDetails,
UserRole: userResult?.get('UserRole'),
@@ -276,21 +267,6 @@ export const deleteUserPost = async (req, res) => {
return res.send(errorMessage);
}
if (extUser?.get('UserRole') !== 'contracts_Admin') {
const errorMessage =
'This action is not permitted. Kindly contact your administrator to request account deletion.';
return res.send(errorMessage);
}
const extUsers = new Parse.Query('contracts_Users');
extUsers.equalTo('TenantId', extUser?.get('TenantId'));
extUsers.notEqualTo('UserRole', 'contracts_Admin');
const isTeamUsers = await extUsers.first({ useMasterKey: true });
if (isTeamUsers) {
const errorMessage = `To delete this account, start by removing all team users associated with it. Once all users are removed, you'll be able to permanently delete the account.`;
return res.send(errorMessage);
}
// Get stored OTP info
const savedOtp = extUser.get('DeleteOTP') || '';
const expiry = extUser.get('DeleteOTPExpiry');
@@ -376,30 +352,7 @@ export const deleteUserByAdmin = async (req, res) => {
const errorMessage = 'User not found.';
return res.status(400).json({ message: errorMessage });
}
if (adminId === userId) {
return res.status(400).json({ message: 'You cannot delete your own account.' });
}
// 2. ext user details
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: adminId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) {
const errorMessage = 'User not found.';
return res.status(400).json({ message: errorMessage });
}
const isAdmin =
extUser?.get('UserRole') === 'contracts_Admin' ||
extUser?.get('UserRole') === 'contracts_OrgAdmin'
? true
: false;
const isOrgAdmin = extUser?.get('UserRole') === 'contracts_OrgAdmin';
const tenantId = extUser?.get('TenantId');
const orgPtr = isOrgAdmin && extUser?.get('OrganizationId');
if (!isAdmin) {
return res.status(400).json({ message: 'Unauthorized.' });
}
const response = await deleteUser(userId, adminId, tenantId, isOrgAdmin, orgPtr);
const response = await deleteUser(userId, adminId);
const code = response?.code || 400;
const message = response?.message || 'An error occurred while deleting your account.';
return res.status(code).json({ message: message });
-2
View File
@@ -56,7 +56,6 @@ 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);
@@ -123,4 +122,3 @@ 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);
@@ -7,7 +7,6 @@ import fs from 'node:fs';
import dotenv from 'dotenv';
import GenerateCertificate from './pdf/GenerateCertificate.js';
import { getSecureUrl } from '../../Utils.js';
import { parseUploadFile } from '../../utils/fileUtils.js';
dotenv.config({ quiet: true });
const eSignName = 'OpenSign';
const eSigncontact = 'hello@opensignlabs.com';
@@ -17,14 +16,10 @@ async function uploadFile(pdfName, filepath) {
try {
const filedata = fs.readFileSync(filepath);
let fileUrl;
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
// await file.save({ useMasterKey: true });
// const fileRes = getSecureUrl(file.url());
// fileUrl = fileRes.url;
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url;
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
await file.save({ useMasterKey: true });
const fileRes = getSecureUrl(file.url());
fileUrl = fileRes.url;
return { imageUrl: fileUrl };
} catch (err) {
console.log('Err ', err);
@@ -57,9 +52,7 @@ export default async function generateCertificatebydocId(req) {
const certificatePath = `./exports/certificate_${docId}.pdf`;
try {
const getDocument = new Parse.Query('contracts_Document');
getDocument.include(
'ExtUserPtr,Signers,AuditTrail.UserPtr,Placeholders,ExtUserPtr.TenantId,ExtUserPtr.UserId'
);
getDocument.include('ExtUserPtr,Signers,AuditTrail.UserPtr,Placeholders,ExtUserPtr.TenantId');
const docRes = await getDocument.get(docId, { useMasterKey: true });
if (docRes && docRes?.get('IsCompleted') && !docRes?.get('CertificateUrl')) {
@@ -20,23 +20,22 @@ async function getTenantByUserId(userId, contactId) {
}
} else {
const query = new Parse.Query('contracts_Users');
query.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
query.equalTo('UserId', {
__type: 'Pointer',
className: '_User',
objectId: userId,
});
const extuser = await query.first({ useMasterKey: true });
if (extuser) {
const tenantId = extuser?.get('TenantId')?.id || '';
const user = extuser?.get('CreatedBy')?.id || userId;
const tenantQuery = new Parse.Query('partners_Tenant');
if (tenantId) {
tenantQuery.equalTo('objectId', tenantId);
} else {
tenantQuery.equalTo('UserId', {
__type: 'Pointer',
className: '_User',
objectId: user,
});
}
tenantQuery.exclude('FileAdapters,PfxFile');
const res = await tenantQuery.first({ useMasterKey: true });
const tenantCreditsQuery = new Parse.Query('partners_Tenant');
tenantCreditsQuery.equalTo('UserId', {
__type: 'Pointer',
className: '_User',
objectId: user,
});
tenantCreditsQuery.exclude('FileAdapters,PfxFile');
const res = await tenantCreditsQuery.first({ useMasterKey: true });
return res;
} else {
return {};
@@ -14,7 +14,7 @@ import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
import { Placeholder } from './Placeholder.js';
import { SignPdf } from '@signpdf/signpdf';
import { P12Signer } from '@signpdf/signer-p12';
import { buildDownloadFilename, parseUploadFile } from '../../../utils/fileUtils.js';
import { buildDownloadFilename } from '../../../utils/fileUtils.js';
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
const APPID = serverAppId;
@@ -43,14 +43,10 @@ async function uploadFile(pdfName, filepath) {
try {
const filedata = fs.readFileSync(filepath);
let fileUrl;
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
// await file.save({ useMasterKey: true });
// const fileRes = getSecureUrl(file.url());
// fileUrl = fileRes.url;
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url;
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
await file.save({ useMasterKey: true });
const fileRes = getSecureUrl(file.url());
fileUrl = fileRes.url;
return { imageUrl: fileUrl };
} catch (err) {
@@ -354,7 +350,7 @@ async function PDF(req) {
const publicUrl = req.headers.public_url;
// below bode is used to get info of docId
const docQuery = new Parse.Query('contracts_Document');
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,CreatedBy');
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc');
docQuery.equalTo('objectId', docId);
const resDoc = await docQuery.first({ useMasterKey: true });
if (!resDoc) {
@@ -1,62 +0,0 @@
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;
}
}
@@ -1,6 +1,4 @@
import { flattenPdf, getSecureUrl } from '../../Utils.js';
import { parseUploadFile } from '../../utils/fileUtils.js';
export default async function saveFile(request) {
if (!request.params.fileBase64) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide file.');
@@ -12,7 +10,6 @@ export default async function saveFile(request) {
const extCls = new Parse.Query('contracts_Users');
extCls.equalTo('UserId', request.user);
extCls.include('TenantId');
extCls.include('UserId');
const resExt = await extCls.first({ useMasterKey: true });
if (resExt) {
const _resExt = JSON.parse(JSON.stringify(resExt));
@@ -23,26 +20,17 @@ export default async function saveFile(request) {
if (ext === 'pdf') {
mimeType = 'application/pdf';
const flatPdf = await flattenPdf(fileBase64);
// file = [...flatPdf];
file = flatPdf;
file = [...flatPdf];
} else if (ext === 'png' || ext === 'jpeg' || ext === 'jpg') {
mimeType = `image/${ext}`;
// file = { base64: fileBase64 };
file = Buffer.from(fileBase64, 'base64');
}
// const pdfFile = new Parse.File(fileName, file, mimeType);
// // Save the Parse File if needed
// const pdfData = await pdfFile.save({ useMasterKey: true });
// const presignedUrl = pdfData.url();
// const fileRes = getSecureUrl(presignedUrl);
// return { url: fileRes.url };
try {
const fileRes = await parseUploadFile(fileName, file, mimeType);
fileUrl = getSecureUrl(fileRes?.url)?.url;
return { url: fileUrl };
} catch (err) {
throw new Parse.Error(400, err?.message);
file = { base64: fileBase64 };
}
const pdfFile = new Parse.File(fileName, file, mimeType);
// Save the Parse File if needed
const pdfData = await pdfFile.save({ useMasterKey: true });
const presignedUrl = pdfData.url();
const fileRes = getSecureUrl(presignedUrl);
return { url: fileRes.url };
} else {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.');
}
+3 -3
View File
@@ -100,7 +100,7 @@ export const config = {
appId: serverAppId,
logLevel: ['error'],
maxLimit: 500,
maxUploadSize: '100mb',
maxUploadSize: '30mb',
masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret!
masterKeyIps: ['0.0.0.0/0', '::/0'], // '::1'
serverURL: cloudServerUrl, // Don't forget to change to https if needed
@@ -156,8 +156,8 @@ export const config = {
export const app = express();
app.use(cors());
app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ limit: '100mb', extended: true }));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.use(function (req, res, next) {
req.headers['x-real-ip'] = getUserIP(req);
const publicUrl = 'https://' + req?.get('host');
+933 -949
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -18,8 +18,8 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.896.0",
"@aws-sdk/s3-request-presigner": "^3.896.0",
"@aws-sdk/client-s3": "^3.888.0",
"@aws-sdk/s3-request-presigner": "^3.888.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
@@ -37,8 +37,8 @@
"generate-api-key": "^1.0.2",
"googleapis": "^159.0.0",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.1.0",
"mongodb": "^6.20.0",
"mailgun.js": "^12.0.3",
"mongodb": "^6.19.0",
"multer": "^2.0.2",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.1",
@@ -49,19 +49,19 @@
"parse-server": "^8.2.4",
"parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.9.1",
"posthog-node": "^5.8.4",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.4.0",
"sharp": "^0.34.4",
"rate-limiter-flexible": "^7.3.1",
"sharp": "^0.34.3",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
},
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.4",
"eslint": "^9.36.0",
"jasmine": "^5.11.0",
"mongodb-runner": "^5.10.0",
"eslint": "^9.35.0",
"jasmine": "^5.10.0",
"mongodb-runner": "^5.9.3",
"nodemon": "^3.1.10",
"nyc": "^17.1.0",
"prettier": "^3.6.2"
-23
View File
@@ -1,6 +1,3 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../Utils.js';
function formatFixedDate(date = new Date()) {
const dd = String(date.getDate()).padStart(2, '0');
const months = [
@@ -78,23 +75,3 @@ export function buildDownloadFilename(formatId, ctx) {
const safeExt = ext.replace(/\.+/g, '').toLowerCase() || 'pdf';
return `${stem}.${safeExt}`;
}
export async function parseUploadFile(fileName, fileData, mimeType) {
try {
const res = await axios.post(`${cloudServerUrl}/files/${fileName}`, fileData, {
headers: {
'X-Parse-Application-Id': serverAppId,
'X-Parse-Master-Key': process.env.MASTER_KEY,
'Content-Type': mimeType,
},
});
// console.log('File uploaded:', res.data);
return res.data;
} catch (err) {
const errorMessage = err?.response?.data?.error || 'Unknown error';
const statusCode = err?.response?.status || 500;
console.log('Err in parseUploadFile', errorMessage);
throw { message: errorMessage, code: statusCode };
}
}