Compare commits

...
Author SHA1 Message Date
Andrew 8833018027 fix: password validation issue 2025-09-26 17:57:57 +00:00
prafull-opensignlabsandGitHub d1c405912b Merge pull request #1945 from OpenSignLabs/updates-17798985943
refactor: remove unnecessary code
2025-09-17 18:51:19 +05:30
prafull-opensignlabs 745aca1754 Merge pull request #1298 from nxglabs/sync-to-public_repo-17798472199
Merge pull request #1297 from nxglabs/staging
2025-09-17 13:19:46 +00:00
prafull-opensignlabsandGitHub 672eeea851 Merge pull request #1943 from OpenSignLabs/updates-17788011031
build(deps): build dependencies and dark mode ui
2025-09-17 11:21:02 +05:30
prafull-opensignlabs 98efff65c9 Merge pull request #1295 from nxglabs/sync-to-public_repo-17787708673
fix: show signer name in typed signature instead in selfsign flow
2025-09-17 05:48:01 +00:00
prafull-opensignlabsandGitHub fe21978538 Merge pull request #1941 from OpenSignLabs/updates-17737216200
v2.28.0
2025-09-15 20:23:17 +05:30
prafull-opensignlabs ca4ccfd889 Merge pull request #1292 from nxglabs/sync-to-public_repo-17730712175
Merge pull request #1291 from nxglabs/staging
2025-09-15 14:51:16 +00:00
prafull-opensignlabsandGitHub 6cb2d69566 Merge pull request #1931 from OpenSignLabs/updates-17548337132
fix: getting error 'user not found' while deleting user
2025-09-08 16:22:46 +05:30
prafull-opensignlabs 98b9ebb228 fix: getting error 'user not found' while deleting user 2025-09-08 10:51:52 +00:00
51 changed files with 3216 additions and 2939 deletions
+7 -7
View File
@@ -3,13 +3,13 @@
"version": "2.26.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.8.4",
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.15.2",
"@lottiefiles/dotlottie-react": "^0.16.2",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.8.2",
"axios": "^1.11.0",
"axios": "^1.12.2",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^25.4.0",
@@ -42,7 +42,7 @@
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.8.2",
"react-router": "^7.9.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
@@ -51,7 +51,7 @@
"reactour": "^1.19.4",
"redux": "^5.0.1",
"regex-parser": "^2.3.1",
"serve": "^14.2.4",
"serve": "^14.2.5",
"styled-components": "^5.3.11",
"web-vitals": "^5.1.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
@@ -101,7 +101,7 @@
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.1.12",
"@types/react": "^19.1.13",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.21",
@@ -113,7 +113,7 @@
"eslint": "^9.34.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^26.1.0",
"jsdom": "^27.0.0",
"lint-staged": "^16.1.6",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
@@ -8,7 +8,7 @@
"dark-mode": "Dunkelmodus",
"name": "Name",
"phone": "Telefon",
"phone-optional": "optional",
"phone-optional": "Optional",
"email": "E-Mail",
"company": "Unternehmen",
"job-title": "Berufsbezeichnung",
@@ -1210,5 +1210,12 @@
"otp-email": "Wir haben einen Bestätigungscode gesendet",
"save-as-temp-warn": "Hinweis: Dieses Dokument enthält vorausgefüllte Widgets, die automatisch entfernt werden, da sie bereits im Basisdokument integriert sind.",
"edit-document": "Dokument bearbeiten",
"otp-email": "Wir haben einen Bestätigungscode gesendet"
"modify": "Ändern",
"merge-certificate-to-pdf": "Zertifikat mit PDF zusammenführen",
"merge-cc-to-pdf-help": {
"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."
},
"enter-name": "Name eingeben",
"enter-email": "E-Mail eingeben"
}
@@ -8,7 +8,7 @@
"dark-mode": "Dark mode",
"name": "Name",
"phone": "Phone",
"phone-optional": "optional",
"phone-optional": "Optional",
"email": "Email",
"company": "Company",
"job-title": "Job title",
@@ -1211,5 +1211,12 @@
"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",
"otp-email":"We've sent a verification code to"
"modify": "Modify",
"merge-certificate-to-pdf": "Merge Certificate to PDF",
"merge-cc-to-pdf-help": {
"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."
},
"enter-name": "Enter name",
"enter-email": "Enter email"
}
@@ -8,7 +8,7 @@
"dark-mode": "Modo oscuro",
"name": "Nombre",
"phone": "Teléfono",
"phone-optional": "opcional",
"phone-optional": "Opcional",
"email": "Correo",
"company": "Compañía",
"job-title": "Título del puesto",
@@ -1211,5 +1211,12 @@
"otp-email": "Hemos enviado un código de verificación",
"save-as-temp-warn": "Nota: Este documento incluye widgets prellenados, que se eliminarán automáticamente ya que ya están incorporados en el documento base.",
"edit-document": "Editar documento",
"otp-email": "Hemos enviado un código de verificación"
"modify": "Modificar",
"merge-certificate-to-pdf": "Combinar certificado con PDF",
"merge-cc-to-pdf-help": {
"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."
},
"enter-name": "Ingrese nombre",
"enter-email": "Ingrese correo electrónico"
}
@@ -8,7 +8,7 @@
"dark-mode": "Mode sombre",
"name": "Nom et Prénom",
"phone": "Téléphone",
"phone-optional": "(facultatif)",
"phone-optional": "Optionnel",
"email": "E-mail",
"company": "Organisation",
"job-title": "Votre Fonction",
@@ -1210,5 +1210,12 @@
"otp-email": "Nous avons envoyé un code de vérification",
"save-as-temp-warn": "Remarque : Ce document contient des widgets préremplis, qui seront automatiquement supprimés car ils sont déjà intégrés dans le document de base.",
"edit-document": "Modifier le document",
"otp-email": "Nous avons envoyé un code de vérification"
"modify": "Modifier",
"merge-certificate-to-pdf": "Fusionner le certificat avec le PDF",
"merge-cc-to-pdf-help": {
"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é."
},
"enter-name": "Saisir le nom",
"enter-email": "Saisir l'e-mail"
}
@@ -1210,5 +1210,13 @@
"otp-email": "हमने एक सत्यापन कोड भेजा है",
"save-as-temp-warn": "नोट: इस दस्तावेज़ में पहले से भरे हुए विजेट शामिल हैं, जिन्हें स्वचालित रूप से हटा दिया जाएगा क्योंकि वे पहले से ही मूल दस्तावेज़ में सम्मिलित हैं।",
"edit-document": "दस्तावेज़ संपादित करें",
"otp-email": "हमने एक सत्यापन कोड भेजा है"
"modify": "संशोधित करें",
"merge-certificate-to-pdf": "प्रमाणपत्र को PDF में मिलाएँ",
"merge-cc-to-pdf-help": {
"p1": "यह सुनिश्चित करेगा कि पूर्णता प्रमाणपत्र अंतिम PDF दस्तावेज़ में शामिल हो। हालाँकि, कृपया ध्यान दें कि एक बार मिलाने के बाद, प्रमाणपत्र को मुख्य दस्तावेज़ से अलग नहीं किया जा सकता।",
"p2": "यदि आप मिलाना नहीं चुनते हैं, तो पूर्णता प्रमाणपत्र हस्ताक्षरित दस्तावेज़ के साथ एक अलग PDF फ़ाइल के रूप में प्रदान किया जाएगा।"
},
"enter-name": "नाम दर्ज करें",
"enter-email": "ईमेल दर्ज करें"
}
@@ -8,7 +8,7 @@
"dark-mode": "Modalità scura",
"name": "Nome",
"phone": "Telefono",
"phone-optional": "facoltativo",
"phone-optional": "Opzionale",
"email": "Email",
"company": "Azienda",
"job-title": "Titolo professionale",
@@ -1210,5 +1210,12 @@
"otp-email": "Abbiamo inviato un codice di verifica",
"save-as-temp-warn": "Nota: Questo documento include widget precompilati, che verranno automaticamente rimossi poiché sono già incorporati nel documento di base.",
"edit-document": "Modifica documento",
"otp-email": "Abbiamo inviato un codice di verifica"
"modify": "Modifica",
"merge-certificate-to-pdf": "Unisci certificato al PDF",
"merge-cc-to-pdf-help": {
"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."
},
"enter-name": "Inserisci nome",
"enter-email": "Inserisci e-mail"
}
@@ -244,6 +244,7 @@ const AddSigner = (props) => {
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/>
</div>
<div className="mb-3">
@@ -263,6 +264,7 @@ const AddSigner = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/>
</div>
<div className="mb-3">
+2
View File
@@ -179,6 +179,7 @@ const AddUser = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/>
</div>
<div className="mb-3">
@@ -200,6 +201,7 @@ const AddUser = (props) => {
}
onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/>
</div>
<div className="mb-3">
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import Parse from "parse";
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
@@ -168,25 +168,21 @@ const DashboardCard = (props) => {
}
}
setresponse(arr.length);
setLoading(false);
});
} else {
await axios.get(url, { headers: headers }).then((res) => {
if (res.data.results.length > 0) {
setLoading(false);
if (props.Data.key !== "count") {
setresponse(res.data.results[0][props.Data.key]);
} else {
setresponse(res.data[props.Data.key]);
}
if (res?.data?.[props.Data.key]) {
setresponse(parseInt(res.data[props.Data.key]));
} else if (res?.data?.results?.length > 0) {
setresponse(res.data.results.length);
} else {
setresponse(0);
setLoading(false);
}
});
}
} catch (e) {
console.error("Problem", e);
} finally {
setLoading(false);
}
}
@@ -52,7 +52,7 @@ const EditTemplate = ({
Bcc: template?.Bcc,
RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
});
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
@@ -244,6 +244,7 @@ const EditTemplate = ({
setShowConfirm(false);
handleClose();
};
return (
<ModalUi
isOpen
+67 -56
View File
@@ -167,7 +167,9 @@ function Header(props) {
);
};
const handleDownloadBtn = async () => {
if (props?.isCompleted) {
if (
props?.isCompleted
) {
props?.setIsDownloadModal(true);
} else {
await handleDownloadDoc();
@@ -222,25 +224,27 @@ function Header(props) {
{t("download")}
</div>
</DropdownMenu.Item>
{props?.isCompleted && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
>
<div className="border-none bg-[#fff]">
<i
className="fa-light fa-award mr-[3px]"
aria-hidden="true"
></i>
{t("certificate")}
</div>
</DropdownMenu.Item>
)}
{
props?.isCompleted && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
>
<div className="border-none bg-[#fff]">
<i
className="fa-light fa-award mr-[3px]"
aria-hidden="true"
></i>
{t("certificate")}
</div>
</DropdownMenu.Item>
)
}
{props?.isSignYourself && (
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -518,24 +522,28 @@ function Header(props) {
></i>
<span className="hidden lg:block">{t("print")}</span>
</button>
{props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
>
<i
className="fa-light fa-award py-[3px]"
aria-hidden="true"
></i>
<span className="hidden lg:block">{t("certificate")}</span>
</button>
)}
{
props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
>
<i
className="fa-light fa-award py-[3px]"
aria-hidden="true"
></i>
<span className="hidden lg:block">
{t("certificate")}
</span>
</button>
)
}
<button
type="button"
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
@@ -592,23 +600,25 @@ function Header(props) {
)
) : props?.isCompleted ? (
<div className="flex flex-row">
{props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
>
<i className="fa-light fa-award" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">
{t("certificate")}
</span>
</button>
)}
{
props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
>
<i className="fa-light fa-award" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">
{t("certificate")}
</span>
</button>
)
}
<button
onClick={(e) =>
handleToPrint(e, setIsDownloading, props?.pdfDetails)
@@ -622,7 +632,8 @@ function Header(props) {
<button
type="button"
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
onClick={() => props?.setIsDownloadModal(true)}
// onClick={() => props?.setIsDownloadModal(true)}
onClick={() => handleDownloadBtn()}
>
<i className="fa-light fa-download" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">{t("download")}</span>
@@ -441,14 +441,12 @@ function PrefillWidgetModal(props) {
handleCheckboxValue(e.target.checked, ind, position);
}}
/>
{!position.options?.isHideLabel && (
<label
htmlFor={`checkbox-${position.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{data}
</label>
)}
<label
htmlFor={`checkbox-${position.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
@@ -560,14 +558,12 @@ function PrefillWidgetModal(props) {
handleWidgetDetails(position, data);
}}
/>
{!position.options?.isHideLabel && (
<label
htmlFor={`radio-${position.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
</label>
)}
<label
htmlFor={`radio-${position.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
@@ -591,8 +587,8 @@ function PrefillWidgetModal(props) {
}
};
const handleEmbedPrefill = (item) => {
props.handleCreateDocument();
const handleEmbedPrefill = async (item) => {
await props.handleCreateDocument();
};
//`loadOptions` function to use show all list of signer in dropdown
const loadOptions = async (inputValue) => {
@@ -654,7 +650,7 @@ function PrefillWidgetModal(props) {
handleClose={props.handleClosePrefillModal}
>
<div className="relative">
{loading && (
{(props?.isSubmit || loading) && (
<div className="absolute inset-0 flex items-center justify-center bg-white/70 z-[9999]">
<Loader />
</div>
@@ -733,7 +729,7 @@ function PrefillWidgetModal(props) {
</div>
<div className="flex gap-2 mx-4 mb-3">
<button
disabled={isAnyLoaderActive}
disabled={isAnyLoaderActive || props?.isSubmit}
className="op-btn op-btn-primary op-btn-sm w-[80px]"
onClick={() => handleEmbedPrefill(props?.item)}
>
@@ -36,7 +36,8 @@ import {
setLastIndex,
setScrollTriggerId,
setPrefillImg,
setPrefillImgLoad
setPrefillImgLoad,
setTypedSignFont
} from "../../redux/reducers/widgetSlice";
import { useDispatch, useSelector } from "react-redux";
import Loader from "../../primitives/Loader";
@@ -79,6 +80,7 @@ function WidgetsValueModal(props) {
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
const myInitial = useSelector((state) => state.widget.myInitial);
const lastWidget = useSelector((state) => state.widget.lastIndex);
const typedSignFont = useSelector((state) => state.widget.typedSignFont);
const { t } = useTranslation();
const canvasRef = useRef(null);
const imageRef = useRef(null);
@@ -278,10 +280,7 @@ function WidgetsValueModal(props) {
setIsLoader(true);
try {
dispatch(
setPrefillImg({
id: currWidgetsDetails?.key,
base64: image?.src
})
setPrefillImg({ id: currWidgetsDetails?.key, base64: image?.src })
);
const imageName = generatePdfName(16);
const imageUrl = await convertBase64ToFile(
@@ -630,11 +629,12 @@ function WidgetsValueModal(props) {
useEffect(() => {
const loadFont = async () => {
try {
await document.fonts.load(`20px ${fontSelect}`);
const selectFontSTyle = fontOptions.find(
(font) => font.value === fontSelect
const selectedFont = typedSignFont || fontSelect;
await document.fonts.load(`20px ${selectedFont}`);
const selectFontStyle = fontOptions.find(
(font) => font.value === selectedFont
);
setFontSelect(selectFontSTyle?.value || fontOptions[0].value);
setFontSelect(selectFontStyle?.value || fontOptions[0].value);
} catch (error) {
console.error("Error loading font:", error);
}
@@ -671,18 +671,21 @@ function WidgetsValueModal(props) {
}
}
if (isTab === "type") {
const signerName = localStorage.getItem("signer")
? JSON.parse(localStorage.getItem("signer"))?.Name
: currentUserName;
//trim user name or typed name value to show in initial signature
const trimmedName = typedSignature
? typedSignature?.trim()
: props?.journey === "kiosk-signing" && kiosk_signer
? kiosk_signer[0]?.Name?.trim()
: currentUserName?.trim();
: signerName?.trim();
//get full name of user
const fullUserName =
typedSignature ||
(props?.journey === "kiosk-signing" && kiosk_signer
? kiosk_signer[0]?.Name
: currentUserName);
: signerName);
const firstCharacter = trimmedName?.charAt(0);
const userName =
currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName;
@@ -697,12 +700,13 @@ function WidgetsValueModal(props) {
const maxWidth = currWidgetsDetails?.Width;
const maxHeight = currWidgetsDetails?.Height;
const widgetDims = { Width: maxWidth, Height: maxHeight };
const fontFamily = fontStyle || fontSelect || "Fasthand";
const fontFamily = fontStyle || typedSignFont || fontSelect || "Fasthand";
const fillColor = color || penColor;
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
setSignature(dataUrl);
setTextWidth(maxWidth);
setTextHeight(maxHeight);
dispatch(setTypedSignFont(fontFamily));
};
const PenColorComponent = () => {
return (
@@ -1200,8 +1204,8 @@ function WidgetsValueModal(props) {
)
) : isTab === "type" ? (
<>
<div className="flex justify-between items-center tabWidth">
<span className="mr-[5px] text-[12px]">
<div className="flex justify-between items-center tabWidth rounded-[4px]">
<span className="ml-[5px] text-[12px] text-base-content">
{currWidgetsDetails?.type === "initials"
? t("initial-teb")
: t("signature-tab")}
@@ -1228,7 +1232,7 @@ function WidgetsValueModal(props) {
}}
/>
</div>
<div className="border-[1px] border-[#d6d3d3] mt-[10px] ml-[5px]">
<div className="border-[1px] border-[#d6d3d3] mt-[10px] rounded-[4px]">
{fontOptions.map((font, ind) => {
return (
<div
@@ -1251,7 +1255,7 @@ function WidgetsValueModal(props) {
>
{typedSignature
? typedSignature
: "Your signature"}
: t("Your-Signature")}
</div>
</div>
);
@@ -1655,7 +1659,7 @@ function WidgetsValueModal(props) {
}
}, []);
return (
<span className="text-center text-[12px]">
<span className="text-center text-[12px] text-base-content">
{t("required-mssg", { leftRequiredWidget, totalWidget })}
</span>
);
@@ -1887,15 +1891,15 @@ function WidgetsValueModal(props) {
{isFinish ? (
<>
<div className="p-1 mt-3">
<span className="text-base">{t("finish-mssg")}</span>
<span className="text-base text-base-content">
{t("finish-mssg")}
</span>
</div>
<div className="flex gap-3 items-center mt-4">
<button
type="button"
className="op-btn op-btn-primary op-btn-sm px-4"
onClick={() => {
handleFinish();
}}
onClick={() => handleFinish()}
>
{t("finish")}
</button>
@@ -1955,9 +1959,7 @@ function WidgetsValueModal(props) {
<button
type="button"
className="op-btn op-btn-primary op-btn-sm"
onClick={() => {
handleClickOnNext();
}}
onClick={() => handleClickOnNext()}
disabled={handleDisable()}
>
{t("save")}
@@ -1967,10 +1969,7 @@ function WidgetsValueModal(props) {
type="button"
className="op-btn op-btn-primary op-btn-sm"
disabled={handleDisable()}
onClick={() => {
const isFinishDoc = true;
handleClickOnNext(isFinishDoc);
}}
onClick={() => handleClickOnNext(true)} // isFinishDoc
>
{t("done")}
</button>
@@ -1978,9 +1977,7 @@ function WidgetsValueModal(props) {
<button
type="button"
className="op-btn op-btn-primary op-btn-sm text-xs md:text-sm"
onClick={() => {
handleClickOnNext();
}}
onClick={() => handleClickOnNext()}
disabled={handleDisable()}
>
{t("next-field")}
+5 -2
View File
@@ -823,6 +823,8 @@ export const createDocument = async (
) => {
if (template && template.length > 0) {
const Doc = template[0];
const date = new Date();
const isoDate = date.toISOString();
let extUserId = Doc.ExtUserPtr.objectId;
let creatorId = Doc.CreatedBy.objectId;
const Extand_Class = localStorage.getItem("Extand_Class");
@@ -898,6 +900,7 @@ export const createDocument = async (
IsTourEnabled: Doc?.IsTourEnabled || false,
AllowModifications: Doc?.AllowModifications || false,
TimeToCompleteDays: parseInt(Doc?.TimeToCompleteDays) || 15,
DocSentAt: { __type: "Date", iso: isoDate },
...SignatureType,
...NotifyOnSignatures,
...Bcc,
@@ -3429,10 +3432,10 @@ export const updateDateWidgetsRes = (
journey,
isRemovePrefill
) => {
const extUser = localStorage.getItem("Extand_Class");
const contactUser = documentData?.Signers.find(
const contactUser = documentData?.Signers?.find(
(data) => data.objectId === signerId
);
const extUser = JSON.stringify([contactUser]);
let placeHolders = documentData?.Placeholders;
if (isRemovePrefill) {
placeHolders = placeHolders?.filter((x) => x.Role !== "prefill");
+2 -1
View File
@@ -2,10 +2,11 @@ import { useEffect, useMemo } from "react";
export function useManifestUrl(appName, logo) {
const url = useMemo(() => {
const start_url = window.location.origin || ".";
const manifest = {
short_name: appName,
name: appName,
start_url: ".",
start_url: start_url,
display: "standalone",
theme_color: "#000000",
background_color: "#ffffff",
+1 -39
View File
@@ -11,7 +11,7 @@ export const extraCols = [
export default function reportJson(id) {
const head = ["Title", "Note", "Folder", "File", "Owner", "Signers"];
const declineHead = ["Title", "Reason", "Folder", "File", "Owner", "Signers"];
const iphead = ["Title", "Note", "Folder", "File", "Signers"];
const iphead = ["Title", "Note", "Folder", "File", "Signers", "Sent Date"];
const contactbook = ["Name", "Email", "Phone", "Company", "JobTitle"];
const dashboardReportHead = ["Title", "File", "Owner", "Signers"];
const templateReport = ["Title", "File", "Owner", "Signers"];
@@ -38,25 +38,6 @@ export default function reportJson(id) {
redirectUrl: "",
action: "delete"
}
// {
// btnId: "22534",
// hoverLabel: "option",
// btnColor: "",
// restrictBtn: true,
// textColor: "black",
// btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
// action: "option",
// subaction: [
// {
// btnId: "1630",
// btnLabel: "Save as template",
// hoverLabel: "Save as template",
// btnIcon: "fa-light fa-envelope",
// redirectUrl: "",
// action: "saveastemplate"
// }
// ]
// }
],
helpMsg:
"These are documents you have started but have not finalized for sending."
@@ -429,25 +410,6 @@ export default function reportJson(id) {
redirectUrl: "",
action: "delete"
}
// {
// btnId: "55534",
// hoverLabel: "option",
// btnColor: "",
// restrictBtn: true,
// textColor: "black",
// btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
// action: "option",
// subaction: [
// {
// btnId: "6630",
// btnLabel: "Save as template",
// hoverLabel: "Save as template",
// btnIcon: "fa-light fa-envelope",
// redirectUrl: "",
// action: "saveastemplate"
// }
// ]
// }
]
};
// contactbook report
+1 -1
View File
@@ -36,7 +36,7 @@ const dashboardJson = [
queryType: "",
class: "contracts_Document",
query:
'where={"Type":null,"Signers":{"#*exists":true},"Placeholders":{"#*exists":true},"SignedUrl":{"#*exists":true},"IsCompleted":false,"IsDeclined":false,"IsArchive":null,"CreatedBy":{"__type":"Pointer","className":"_User","objectId":"#UserId.objectId#"},"ExpiryDate":{"#*gt":{"__type":"#Date#","iso":"#today#"}}}&keys=Name,ExpiryDate,SignedUrl,Signers&count=1',
'where={"Type":{"#*ne":"Folder"},"Signers":{"#*exists":true},"Placeholders":{"#*exists":true},"SignedUrl":{"#*exists":true},"IsCompleted":{"#*ne":true},"IsDeclined":{"#*ne":true},"IsArchive":{"#*ne":true},"CreatedBy":{"__type":"Pointer","className":"_User","objectId":"#UserId.objectId#"},"ExpiryDate":{"#*gt":{"__type":"#Date#","iso":"#today#"}}}&count=1&limit=0',
key: "count",
Redirect_type: "Report",
Redirect_id: "1MwEuxLEkF",
+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">
+24 -17
View File
@@ -41,7 +41,7 @@ const DocSuccessPage = () => {
objectId: docId,
SignedUrl: docUrl,
CertificateUrl: certificate,
IsCompleted: completed
IsCompleted: completed,
};
setPdfDetails([details]);
const base64Pdf = await getBase64FromUrl(docUrl);
@@ -52,7 +52,9 @@ const DocSuccessPage = () => {
};
const handleDownload = () => {
if (pdfDetails?.[0]?.IsCompleted) {
if (
pdfDetails?.[0]?.IsCompleted
) {
setIsDownloadModal(true);
} else {
handleDownloadPdf(pdfDetails, setIsDownloading, pdfBase64Url);
@@ -99,21 +101,26 @@ const DocSuccessPage = () => {
<span>{t("download")}</span>
</button>
{pdfDetails?.[0]?.IsCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(pdfDetails, setIsDownloading)
}
className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-secondary"
>
<i
className="fa-light fa-award mx-[3px] md:mx-0"
aria-hidden="true"
></i>
<span>{t("certificate")}</span>
</button>
)}
{
pdfDetails?.[0]?.IsCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
pdfDetails,
setIsDownloading
)
}
className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-secondary"
>
<i
className="fa-light fa-award mx-[3px] md:mx-0"
aria-hidden="true"
></i>
<span>{t("certificate")}</span>
</button>
)
}
<button
onClick={(e) =>
handleToPrint(e, setIsDownloading, pdfDetails)
@@ -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>
+8 -6
View File
@@ -67,7 +67,7 @@ const Forms = (props) => {
NotifyOnSignatures: "",
Bcc: [],
RedirectUrl: "",
AllowModifications: false
AllowModifications: false,
});
const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]);
@@ -461,7 +461,7 @@ const Forms = (props) => {
IsEnableOTP: "false",
IsTourEnabled: istourenabled,
RedirectUrl: "",
AllowModifications: false
AllowModifications: false,
});
setFileUpload("");
setSelectedFiles([]);
@@ -541,7 +541,7 @@ const Forms = (props) => {
IsTourEnabled: istourenabled,
NotifyOnSignatures: notifySign,
RedirectUrl: "",
AllowModifications: false
AllowModifications: false,
};
setFormData(obj);
removeFile();
@@ -631,10 +631,12 @@ const Forms = (props) => {
};
return (
<div className="shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content">
<div
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
>
{isAlert?.message && <Alert type={isAlert.type}>{isAlert.message}</Alert>}
{isSubmit ? (
<div className="h-[300px] flex justify-center items-center">
<div className="flex flex-col justify-center items-center h-[100vh]">
<Loader />
</div>
) : (
@@ -1047,7 +1049,7 @@ const Forms = (props) => {
></div>
)}
{isAdvanceOpt && (
<div className="card bg-base-100 rounded-box p-3 flex-grow ">
<div className="card bg-base-100 rounded-box p-3 flex-grow ">
{formData?.autoreminder === true &&
props.title !== "New Template" && (
<div className="text-xs mt-2">
+2
View File
@@ -378,6 +378,7 @@ function GuestLogin() {
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
placeholder={t("enter-name")}
required
/>
</div>
@@ -395,6 +396,7 @@ function GuestLogin() {
value={contact.email}
onChange={handleInputChange}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
required
disabled
/>
+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"
+29 -28
View File
@@ -14,8 +14,9 @@ import {
setMyInitial,
setDefaultSignImg,
resetWidgetState,
setPrefillImg
} from "../redux/reducers/widgetSlice.js";
setPrefillImg,
setTypedSignFont
} from "../redux/reducers/widgetSlice";
import {
contractDocument,
embedWidgetsToDoc,
@@ -736,6 +737,7 @@ function PdfRequestFiles(
widgets
);
if (resSign && resSign.status === "success") {
dispatch(setTypedSignFont("Fasthand"));
setPdfUrl(resSign.data);
setIsSigned(true);
setSignedSigners([]);
@@ -884,7 +886,8 @@ function PdfRequestFiles(
?.IsCompleted
? `&completed=true`
: "";
const params = `docid=${updatedDoc.updatedPdfDetails[0].objectId}&docurl=${encodeURIComponent(url)}${isCompleted}${fileAdapter}`;
const params =
`docid=${updatedDoc.updatedPdfDetails[0].objectId}&docurl=${encodeURIComponent(url)}${isCompleted}${fileAdapter}`;
window.location.href = `/success?${params}`;
}
} else {
@@ -1714,6 +1717,10 @@ function PdfRequestFiles(
}
return email;
};
const handleDownload = async () => {
setIsCompleted((prev) => ({ ...prev, isModal: false }));
setIsDownloadModal(true);
};
return (
<>
{isLoading.isLoad ? (
@@ -1925,34 +1932,28 @@ function PdfRequestFiles(
{t("print")}
</span>
</button>
<button
type="button"
onClick={() =>
handleDownloadCertificate(
pdfDetails,
setIsDownloading
)
}
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-secondary"
>
<i
className="fa-light fa-award mx-[3px] lg:mx-0"
aria-hidden="true"
></i>
<span className="hidden lg:block">
{t("certificate")}
</span>
</button>
<button
type="button"
onClick={() =>
handleDownloadCertificate(
pdfDetails,
setIsDownloading
)
}
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-secondary"
>
<i
className="fa-light fa-award mx-[3px] lg:mx-0"
aria-hidden="true"
></i>
<span className="hidden lg:block">
{t("certificate")}
</span>
</button>
<button
type="button"
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-primary"
onClick={() => {
setIsCompleted((prev) => ({
...prev,
isModal: false
}));
setIsDownloadModal(true);
}}
onClick={handleDownload}
>
<i
className="fa-light fa-download"
+4 -3
View File
@@ -61,6 +61,7 @@ const Preferences = () => {
completionMail: false
});
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
useEffect(() => {
fetchSignType();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -194,7 +195,7 @@ const Preferences = () => {
DateFormat: dateFormat,
Is12HourTime: is12HourTime,
IsLTVEnabled: isLTVEnabled,
DownloadFilenameFormat: fileNameFormat
DownloadFilenameFormat: fileNameFormat,
};
const updateRes = await Parse.Cloud.run("updatepreferences", params);
if (updateRes) {
@@ -804,7 +805,7 @@ const Preferences = () => {
onClick={() => handleModifyMail("request")}
className="op-btn op-btn-primary shadow-lg"
>
Modify
{t("modify")}
</button>
</div>
)
@@ -880,7 +881,7 @@ const Preferences = () => {
onClick={() => handleModifyMail("completion")}
className="op-btn op-btn-primary shadow-lg"
>
Modify
{t("modify")}
</button>
</div>
)
+6 -3
View File
@@ -32,7 +32,10 @@ const Report = () => {
const [defaultColumns, setDefaultColumns] = useState([]);
const [isColumnModal, setIsColumnModal] = useState(false);
const debounceTimer = useRef(null);
const docPerPage = 20;
// Number of documents to display per page (should always be half of docLimit for proper pagination)
const docPerPage = 10;
// Number of documents to fetch per API call
const docLimit = 20;
// below useEffect is call when id param change
useEffect(() => {
@@ -48,7 +51,7 @@ const Report = () => {
setVisibleColumns([]);
setColumnLabels({});
}
getReportData(0, docPerPage, "");
getReportData(0, docLimit, "");
// Function returned from useEffect is called on unmount
return () => {
@@ -64,7 +67,7 @@ const Report = () => {
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(list.length, 20, searchTerm);
getReportData(list?.length, docLimit, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
+4 -2
View File
@@ -63,8 +63,9 @@ import {
setMyInitial,
setDefaultSignImg,
setIsShowModal,
resetWidgetState
} from "../redux/reducers/widgetSlice.js";
resetWidgetState,
setTypedSignFont
} from "../redux/reducers/widgetSlice";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal";
@@ -745,6 +746,7 @@ function SignYourSelf() {
if (resSignPdf) {
const signedpdf = JSON.parse(JSON.stringify(resSignPdf));
setPdfUrl(signedpdf);
dispatch(setTypedSignFont("Fasthand"));
getDocumentDetails(false);
}
};
@@ -165,6 +165,7 @@ const TemplatePlaceholder = () => {
const [currUserId, setCurrUserId] = useState(false);
const [documentDetails, setDocumentDetails] = useState();
const [copied, setCopied] = useState(false);
const [isUseTemplate, setIsUseTemplate] = useState(false);
const currentUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
@@ -931,6 +932,7 @@ const TemplatePlaceholder = () => {
}
};
const handleCreateDocument = async () => {
setIsUseTemplate(true);
try {
setIsUiLoading(true);
const res = await utils?.handleCheckPrefillCreateDoc(
@@ -974,6 +976,7 @@ const TemplatePlaceholder = () => {
} catch (e) {
console.log("error in create document function", e);
}
setIsUseTemplate(false);
};
const navigatePageToDoc = async () => {
try {
@@ -2138,6 +2141,7 @@ const TemplatePlaceholder = () => {
isNewContact={isNewContact}
documentFlow={"template"}
docId={pdfDetails[0]?.objectId}
isSubmit={isUseTemplate}
/>
)}
{isShowModal[currWidgetsDetails?.key] && (
+1 -1
View File
@@ -487,7 +487,7 @@ function UserProfile() {
) : (
<>
{deleteUserRes ? (
<div className="h-[100px] p-[20px] flex justify-center items-center text-sm md:text-base">
<div className="h-[100px] p-[20px] flex justify-center items-center text-base-content text-sm md:text-base">
{deleteUserRes}
</div>
) : (
@@ -174,6 +174,7 @@ const AddContact = (props) => {
required
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/>
</div>
<div className="mb-[0.75rem]">
@@ -193,6 +194,7 @@ const AddContact = (props) => {
required
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase"
placeholder={t("enter-email")}
/>
</div>
{isOptionalDetails && (
@@ -96,8 +96,8 @@ function DownloadPdfZip(props) {
handleClose={() => props.setIsDownloadModal(false)}
>
<div className="p-[20px] h-full text-base-content">
{downloadType.map((data, ind) => {
return (
{downloadType
.map((data, ind) => (
<label
key={ind}
className="flex items-center gap-1 mb-2 cursor-pointer"
@@ -111,8 +111,7 @@ function DownloadPdfZip(props) {
/>
{data.label}
</label>
);
})}
))}
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
onClick={() => handleDownload()}
@@ -63,7 +63,7 @@ function CustomModal(props) {
className="op-btn op-btn-secondary ml-[10px] mb-3 mt-1"
onClick={() => props.handleDownloadBtn()}
>
Download
{t("download")}
</button>
)}
</div>
@@ -138,6 +138,12 @@ export const RenderReportCell = ({
{rowData?.ExpiryDate ? formatDate(rowData?.ExpiryDate?.iso) : "-"}
</td>
);
case "Sent Date":
return (
<td key={col} className="p-2 text-center">
{rowData?.DocSentAt ? formatDate(rowData?.DocSentAt?.iso) : "-"}
</td>
);
case "Signers":
return (
<SignerCell
@@ -13,7 +13,8 @@ const initialState = {
lastIndex: "",
scrollTriggerId: "",
prefillImg: [],
prefillImgLoad: {}
prefillImgLoad: {},
typedSignFont: "Fasthand"
};
const widgetSlice = createSlice({
@@ -41,6 +42,9 @@ const widgetSlice = createSlice({
setScrollTriggerId: (state, action) => {
state.scrollTriggerId = action.payload;
},
setTypedSignFont: (state, action) => {
state.typedSignFont = action.payload;
},
setPrefillImg: (state, action) => {
const existingIndex = state.prefillImg.findIndex(
(img) => img.id === action.payload.id
@@ -73,7 +77,8 @@ export const {
setLastIndex,
setScrollTriggerId,
setPrefillImg,
setPrefillImgLoad
setPrefillImgLoad,
setTypedSignFont
} = widgetSlice.actions;
export default widgetSlice.reducer;
@@ -88,6 +88,7 @@ const EditContactForm = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
required
/>
</div>
@@ -105,6 +106,7 @@ const EditContactForm = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
required
/>
</div>
@@ -90,6 +90,7 @@ const DocumentsReport = (props) => {
const [documentId, setDocumentId] = useState("");
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const [error, setError] = useState("");
const [isMailModal, setIsMailModal] = useState(false);
const [customizeMail, setCustomizeMail] = useState({ body: "", subject: "" });
@@ -495,7 +496,9 @@ const DocumentsReport = (props) => {
const docId = props?.ReportName !== "Templates" && item.objectId;
if (url) {
try {
if (isCompleted) {
if (
isCompleted
) {
setIsDownloadModal({ [item.objectId]: true });
} else {
const signedUrl = await getSignedUrl(
@@ -920,6 +923,7 @@ const DocumentsReport = (props) => {
};
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
const handlePrefillWidgetCreateDoc = async () => {
setIsSubmit(true);
const scale = 1;
const key = Object.keys(isPrefillModal)[0];
setActLoader({ [key]: true });
@@ -957,6 +961,7 @@ const DocumentsReport = (props) => {
alert(t("user-not-exist"));
}
}
setIsSubmit(false);
setActLoader({});
};
const handleRecipientSign = (documentId, currentId) => {
@@ -1027,10 +1032,10 @@ const DocumentsReport = (props) => {
if (isPrefill) {
return (
<span className="flex text-sm mt-3 text-red-500">
{t("save-as-temp-warn")}
{t("save-as-temp-warn")}
</span>
);
}
}
};
return (
<div className="relative">
@@ -1367,6 +1372,7 @@ const DocumentsReport = (props) => {
setIsNewContact={setIsNewContact}
isNewContact={isNewContact}
docId={item.objectId}
isSubmit={isSubmit}
/>
)}
{isModal["extendexpiry_" + item.objectId] && (
@@ -85,6 +85,7 @@ const TemplatesReport = (props) => {
const [documentId, setDocumentId] = useState("");
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
const [isMailModal, setIsMailModal] = useState(false);
@@ -871,6 +872,7 @@ const TemplatesReport = (props) => {
};
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
const handlePrefillWidgetCreateDoc = async () => {
setIsSubmit(true);
const scale = 1;
const res = await utils?.handleCheckPrefillCreateDoc(
xyPosition,
@@ -940,6 +942,7 @@ const TemplatesReport = (props) => {
alert(t("user-not-exist"));
}
}
setIsSubmit(false);
};
//function show signer list and share link to share signUrl
const handleShareList = () => {
@@ -1257,6 +1260,7 @@ const TemplatesReport = (props) => {
setIsNewContact={setIsNewContact}
isNewContact={isNewContact}
docId={item.objectId}
isSubmit={isSubmit}
/>
)}
{isShareWith[item.objectId] && (
@@ -2,9 +2,12 @@ import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import uploadFile from './uploadFile.js';
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
import { deleteUserByAdmin, deleteUserGet, deleteUserPost } from './deleteUser.js';
import { deleteUserByAdmin, deleteUserPost } from './deleteAccount/deleteUser.js';
import { deleteUserGet } from './deleteAccount/deleteUserGet.js';
import { deleteUserOtp } from './deleteAccount/deleteUserOtp.js';
export const app = express();
@@ -17,5 +20,6 @@ app.post('/file_upload', uploadFile);
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
app.get('/delete-account/:userId', deleteUserGet);
app.post('/delete-account/:userId/otp', deleteUserOtp);
app.post('/delete-account/:userId', deleteUserPost);
app.post('/deleteuser/:userId', deleteUserByAdmin);
@@ -1,7 +1,7 @@
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
import fs from 'node:fs/promises';
import pLimit from 'p-limit';
import { serverAppId } from '../../Utils.js';
import { serverAppId } from '../../../Utils.js';
// === Configuration ===
const serverHost = new URL(process.env.SERVER_URL).hostname;
@@ -1,7 +1,7 @@
import axios from 'axios';
import { cloudServerUrl, generateId, serverAppId } from '../../Utils.js';
import sendmailtoSupport from './sendMailToSupport.js';
import { cloudServerUrl, generateId, serverAppId } from '../../../Utils.js';
import { deleteContactsInBatch, deleteDataFiles, deleteInBatches } from './deleteFileUrl.js';
import { MAX_ATTEMPTS } from './deleteUtils.js';
const serverUrl = cloudServerUrl;
const appId = serverAppId;
@@ -54,7 +54,6 @@ export async function deleteUser(userId, adminId) {
userDetails = { ...userDetails, UserId: userId };
if (!userResult) {
const errorMessage = 'User not found.';
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
const contractsUserId = userResult.id;
@@ -73,7 +72,6 @@ export async function deleteUser(userId, adminId) {
};
if (adminId && isAdmin) {
const errorMessage = 'An error occurred while deleting your account.';
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -85,7 +83,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed during contracts_Template cleanup:', err);
const errorMessage = 'Failed during contracts_Template cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -95,7 +92,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed during contactbook cleanup:', err);
const errorMessage = 'Failed during contactbook cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -116,7 +112,6 @@ export async function deleteUser(userId, adminId) {
console.error('Failed during contactbook current user cleanup:', err);
const errorMessage =
'Failed during contactbook current user cleanup: ' + (err?.message || err);
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -130,7 +125,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed to delete appToken entries:', err);
const errorMessage = 'Failed to delete appToken entries:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -140,7 +134,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed during partners_DataFiles cleanup:', err);
const errorMessage = 'Failed during partners_DataFiles cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -156,7 +149,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed to delete contracts_Organizations entry:', err);
const errorMessage = 'Failed to delete contracts_Organizations entry:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
// STEP 7: Delete each entry in contracts_Teams by objectId from teamIds
@@ -170,7 +162,6 @@ export async function deleteUser(userId, adminId) {
} catch (teamErr) {
console.error(`Failed to delete team with ID ${team.id}:`, teamErr);
const errorMessage = `Failed to delete team with ID ${team.id}` + teamErr?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
}
@@ -178,7 +169,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed during contracts_Teams deletion loop:', err);
const errorMessage = 'Failed during contracts_Teams deletion loop:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -194,7 +184,6 @@ export async function deleteUser(userId, adminId) {
const msg = `Failed during partners_Tenant ${'cleanup:'} `;
console.error(msg, err);
const errorMessage = msg + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -214,7 +203,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed during partners_TenantCredits cleanup:', err);
const errorMessage = 'Failed during partners_TenantCredits cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
}
@@ -228,7 +216,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed during contracts_Signature cleanup:', err);
const errorMessage = 'Failed during contracts_Signature cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
@@ -238,120 +225,20 @@ export async function deleteUser(userId, adminId) {
} catch (err) {
console.error('Failed to delete contracts_Users entry:', err);
const errorMessage = 'Failed to delete contracts_Users entry:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
return { code: 200, message: 'User and all associated data deleted successfully.' };
} catch (error) {
console.error('User deletion process failed:', error);
const errorMessage = `User deletion failed: ${error.message || error}`;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage };
}
}
// 1. HTML Password Prompt Page
export const deleteUserGet = async (req, res) => {
const { userId } = req.params;
const extUserQuery = new Parse.Query('Contracts_Users');
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) {
const errorMessage = 'User not found.';
return res.send(errorMessage);
}
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
const htmlForm = `
<html>
<head>
<title>Delete Account</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #ffffff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
text-align: center;
}
h2 {
color: #dc3545;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 10px;
font-weight: 600;
}
input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
button {
background-color: #d9534f;
color: #ffffff;
border: none;
padding: 12px 20px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #c9302c;
}
.warning {
color: #6c757d;
font-size: 14px;
margin-top: -10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2>Confirm Account Deletion</h2>
<p class="warning">This action is irreversible. Please confirm by entering your password.</p>
<form method="POST" action="${routePath}/delete-account/${userId}">
<label for="password">Password</label>
<input type="password" name="password" id="password" placeholder="Please provide your password" required />
<button type="submit">Delete My Account</button>
</form>
</div>
</body>
</html>
`;
res.send(htmlForm);
};
// 2. Handle Password Verification and Deletion
export const deleteUserPost = async (req, res) => {
const { userId } = req.params;
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
const { password } = req.body;
const { otp } = req.body;
let userDetails = {
UserRole: 'not found',
Name: 'not found',
@@ -369,33 +256,53 @@ export const deleteUserPost = async (req, res) => {
const user = await userQuery.first({ useMasterKey: true });
if (!user) {
const errorMessage = 'User not found.';
// sendmailtoSupport(userDetails, errorMessage);
// return res.status(404).send(errorMessage);
return res.send(errorMessage);
}
const extUserQuery = new Parse.Query('Contracts_Users');
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) {
const errorMessage = 'User not found.';
return res.send(errorMessage);
}
// 2. Attempt login to verify password
const username = user.get('username'); // assuming 'username' is used for login
// Get stored OTP info
const savedOtp = extUser.get('DeleteOTP') || '';
const expiry = extUser.get('DeleteOTPExpiry');
const tries = Number(extUser.get('DeleteOTPTries') || 0);
if (tries >= MAX_ATTEMPTS) {
return res.status(429).send('Too many invalid attempts. Please resend OTP and try again.');
}
if (!otp || typeof otp !== 'string') {
// Count attempt
extUser.set('DeleteOTPTries', tries + 1);
await extUser.save(null, { useMasterKey: true });
return res.status(400).send('OTP is required.');
}
if (!savedOtp) {
return res.status(400).send('No OTP found. Please request a new OTP.');
}
if (expiry && Date.now() > expiry.getTime()) {
return res.status(400).send('OTP has expired. Please request a new OTP.');
}
if (otp !== savedOtp) {
// Increment tries on mismatch
extUser.set('DeleteOTPTries', tries + 1);
await extUser.save(null, { useMasterKey: true });
return res.status(400).send('Invalid OTP.');
}
// 2. Remove OTP related data
try {
// await Parse.User.logIn(username, password); // Will throw if password invalid
// Use REST login to avoid mutating the global Parse current user
// Will throw if password invalid
const res = await axios.get(serverUrl + '/login', {
params: { username, password },
headers: { 'X-Parse-Application-Id': appId },
});
console.log('Res ', res?.data);
extUser.unset('DeleteOTP');
extUser.unset('DeleteOTPExpiry');
extUser.unset('DeleteOTPSentAt');
extUser.unset('DeleteOTPTries');
await extUser.save(null, { useMasterKey: true });
} catch (err) {
console.log('err while validating password: ', err?.response?.data || err);
const errorMessage = `Invalid password. <a href="${routePath}/delete-account/${userId}">Try again</a>`;
sendmailtoSupport(userDetails, errorMessage);
return res.status(401).send(errorMessage);
}
const response = await deleteUser(userId);
@@ -405,7 +312,6 @@ export const deleteUserPost = async (req, res) => {
} catch (error) {
console.error('Account deletion error:', error);
const errorMessage = error?.message || 'An error occurred while deleting your account.';
sendmailtoSupport(userDetails, errorMessage);
return res.status(500).send(errorMessage);
}
};
@@ -444,7 +350,6 @@ export const deleteUserByAdmin = async (req, res) => {
const user = await userQuery.first({ useMasterKey: true });
if (!user) {
const errorMessage = 'User not found.';
sendmailtoSupport(userDetails, errorMessage);
return res.status(400).json({ message: errorMessage });
}
const response = await deleteUser(userId, adminId);
@@ -458,7 +363,6 @@ export const deleteUserByAdmin = async (req, res) => {
error?.message ||
'An error occurred while deleting your account.';
console.error(`Account deletion error:`, errorMessage);
sendmailtoSupport(userDetails, errorMessage);
return res.status(code).json({ message: errorMessage });
}
};
@@ -0,0 +1,143 @@
import { OTP_LENGTH, RESEND_COOLDOWN_SEC } from './deleteUtils.js';
// 1. HTML Password Prompt Page
export const deleteUserGet = async (req, res) => {
const { userId } = req.params;
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) return res.status(404).send('User not found.');
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
const htmlForm = `
<html>
<head>
<title>Delete Account</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }
.container { background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); width: 100%; max-width: 420px; text-align: center; }
h2 { color: #dc3545; margin-bottom: 14px; }
p.warning { color: #6c757d; font-size: 14px; margin-top: 0; margin-bottom: 18px; }
label { display: block; margin-bottom: 10px; font-weight: 600; text-align: left; }
input[type="text"] { width: 100%; padding: 12px; margin-bottom: 16px; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; letter-spacing: 0.2em; }
button { background-color: #d9534f; color: #ffffff; border: none; padding: 12px 16px; font-size: 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; }
button:hover { background-color: #c9302c; }
.secondary { background-color: #6c757d; }
.secondary:hover { background-color: #5a6268; }
.btn-block { width: 100%; }
.link-btn { background: transparent; border: none; color: #0d6efd; text-decoration: underline; padding: 0; font-size: 14px; cursor: pointer; }
.link-btn[disabled] { color: #6c757d; text-decoration: none; cursor: not-allowed; }
.muted { color: #6c757d; font-size: 13px; margin-top: 8px; min-height: 18px; }
.error { color: #c9302c; font-size: 13px; margin-top: 8px; min-height: 18px; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h2>Confirm Account Deletion</h2>
<!-- Initial view: only Send OTP -->
<div id="preOtp">
<p class="warning">This action is irreversible. A verification mail will be sent to your account to your registered email.</p>
<button type="button" id="sendOtpBtn" class="secondary btn-block">Send OTP</button>
<div id="preMsg" class="muted"></div>
<div id="preErr" class="error"></div>
</div>
<!-- Shown after OTP is sent -->
<form id="otpForm" class="hidden" method="POST" action="${routePath}/delete-account/${userId}">
<p class="warning">This action is irreversible. Verify with an OTP sent to your registered email.</p>
<label for="otp">One-Time Password (OTP)</label>
<input type="text" name="otp" id="otp" placeholder="Enter ${OTP_LENGTH}-digit OTP" required maxlength="${OTP_LENGTH}" inputmode="numeric" />
<div style="text-align:center; margin-top: 10px;">
<button type="submit" id="deleteBtn">Delete My Account</button>
</div>
<div style="text-align:center; margin-top: 10px;">
<button type="button" id="resendOtpLink" class="link-btn">Resend OTP</button>
</div>
<div id="timer" class="muted"></div>
<div id="msg" class="muted"></div>
<div id="err" class="error"></div>
</form>
</div>
<script>
(function(){
const preOtp = document.getElementById('preOtp');
const sendBtn = document.getElementById('sendOtpBtn');
const preMsg = document.getElementById('preMsg');
const preErr = document.getElementById('preErr');
const form = document.getElementById('otpForm');
const otpInput = document.getElementById('otp');
const resendLink = document.getElementById('resendOtpLink');
const timerEl = document.getElementById('timer');
const msgEl = document.getElementById('msg');
const errEl = document.getElementById('err');
const RESEND_WAIT = ${RESEND_COOLDOWN_SEC};
let countdown = 0;
let iv = null;
function setText(el, t){ el.textContent = t || ''; }
function updateResendState(){
resendLink.disabled = countdown>0;
if(countdown>0){
resendLink.setAttribute('disabled','true');
resendLink.style.pointerEvents='none';
}else{
resendLink.removeAttribute('disabled');
resendLink.style.pointerEvents='auto';
}
}
function tick(){
if(countdown<=0){ clearInterval(iv); iv=null; setText(timerEl,'You can resend the OTP now.'); updateResendState(); return; }
setText(timerEl,'Resend available in '+countdown+'s'); countdown--; updateResendState();
}
function startTimer(sec){ countdown=sec||RESEND_WAIT; if(iv) clearInterval(iv); tick(); iv=setInterval(tick,1000); }
async function sendOtp(showForm){
setText(preErr,''); setText(preMsg,'Sending OTP...');
try {
const resp = await fetch('${routePath}/delete-account/${userId}/otp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
const data = await resp.json().catch(()=>({}));
if(!resp.ok){
if(resp.status===429 && data?.retryAfterSec){
startTimer(data.retryAfterSec); setText(preErr,'Please wait '+data.retryAfterSec+'s before resending.'); setText(preMsg,'');
return;
}
throw new Error(data?.error || 'Failed to send OTP.');
}
startTimer(data?.cooldownSec || RESEND_WAIT);
// Success: hide pre, show form
preOtp.classList.add('hidden');
form.classList.remove('hidden');
setText(msgEl,'OTP sent to your registered email.');
otpInput.focus();
} catch(e){
setText(preErr,e.message||'Error sending OTP.');
setText(preMsg,'');
}
}
sendBtn.addEventListener('click',()=>sendOtp(true));
resendLink.addEventListener('click',()=>{ if(countdown<=0) sendOtp(false); });
})();
</script>
</body>
</html>
`;
res.send(htmlForm);
};
@@ -0,0 +1,43 @@
import {
generateOtp,
msUntil,
sendDeleteOtpEmail,
OTP_EXPIRES_MIN,
RESEND_COOLDOWN_SEC,
} from './deleteUtils.js';
export const deleteUserOtp = async (req, res) => {
const { userId } = req.params;
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) return res.status(404).json({ error: 'User not found' });
const now = Date.now();
const lastSentAt = extUser.get('DeleteOTPSentAt')?.getTime?.() || 0;
const cooldownEndsAt = lastSentAt + RESEND_COOLDOWN_SEC * 1000;
const remainingMs = msUntil(now, cooldownEndsAt);
if (remainingMs > 0) {
return res
.status(429)
.json({ error: 'Cooldown not finished', retryAfterSec: Math.ceil(remainingMs / 1000) });
}
const otp = generateOtp();
const expiresAt = new Date(now + OTP_EXPIRES_MIN * 60 * 1000);
try {
const resp = await sendDeleteOtpEmail(extUser, otp);
extUser.set('DeleteOTP', otp);
extUser.set('DeleteOTPExpiry', expiresAt);
extUser.set('DeleteOTPSentAt', new Date(now));
extUser.set('DeleteOTPTries', 0); // reset tries on resend
await extUser.save(null, { useMasterKey: true });
return res.json({ ok: true, cooldownSec: RESEND_COOLDOWN_SEC, expiresInMin: OTP_EXPIRES_MIN });
} catch (err) {
console.log('Error sending delete OTP (POST /otp):', err?.response?.data || err);
return res.status(500).json({ error: 'Failed to send OTP' });
}
};
@@ -0,0 +1,72 @@
import axios from 'axios';
import { appName, cloudServerUrl, serverAppId } from '../../../Utils.js';
const serverUrl = cloudServerUrl;
const appId = serverAppId;
const masterKey = process.env.MASTER_KEY;
// Constants (adjust to your preference)
export const OTP_LENGTH = 6;
export const OTP_EXPIRES_MIN = 10; // OTP validity in minutes
export const RESEND_COOLDOWN_SEC = 30; // Cooldown between OTP sends
export const MAX_ATTEMPTS = 5; // Max allowed wrong attempts
export function generateOtp(len = OTP_LENGTH) {
// 6-digit numeric OTP (000000999999, padded)
const n = Math.floor(Math.random() * Math.pow(10, len));
return String(n).padStart(len, '0');
}
export async function sendDeleteOtpEmail(extUser, otp) {
const params = {
extUserId: extUser.id,
from: appName,
recipient: extUser?.get('Email'),
subject: 'OTP for Deletion account request',
html: `
<html lang="en">
<body style="margin:0;padding:0;background:#f6f7fb;font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border:1px solid #e9ecf1;border-radius:8px;padding:20px;">
<tr>
<td align="left" style="font-size:16px;color:#0f172a;">
<div style="font-weight:bold;margin-bottom:8px;">${appName}</div>
<div style="font-size:18px;margin:0 0 12px 0;">Your verification code</div>
<div style="display:inline-block;border:1px solid #e9ecf1;border-radius:6px;background:#f8fafc;padding:10px 14px;margin-bottom:10px;">
<span style="font-family:Consolas,'Courier New',monospace;font-size:24px;letter-spacing:6px;color:#0f172a;">${otp}</span>
</div>
<p style="margin:8px 0 0 0;font-size:13px;color:#475569;">
This code expires in <strong>${OTP_EXPIRES_MIN}</strong> minutes.
</p>
<hr style="border:none;border-top:1px solid #e9ecf1;margin:18px 0;">
<p style="margin:0;font-size:12px;color:#64748b;">
If you didnt request this code, you can ignore this email.
</p>
</td>
</tr>
</table>
<div style="font-size:11px;color:#94a3b8;margin-top:12px;">
&copy; ${new Date().getFullYear()} ${appName}. All rights reserved.
</div>
</td>
</tr>
</table>
</body>
</html>
`,
};
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
};
return axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
}
export function msUntil(nowMs, futureMs) {
return Math.max(0, (futureMs || 0) - nowMs);
}
@@ -1,83 +0,0 @@
import { appName, smtpenable } from '../../Utils.js';
export default async function sendmailtoSupport(userDetails, errorMessage) {
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
// Render a simple HTML form. In production, consider using a templating engine.
try {
await Parse.Cloud.sendEmail({
sender: appName + ' <' + mailsender + '>',
recipient: 'support@opensignlabs.com',
subject: `Error while deleting account ${appName}`,
text: `Error while deleting account ${appName}`,
html: `<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 40px;
color: #333;
}
.container {
max-width: 600px;
margin: auto;
background: #ffffff;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
padding: 30px;
}
h1 {
color: #dc3545;
font-size: 24px;
margin-bottom: 20px;
text-align: center;
}
h2 {
font-size: 20px;
color: #495057;
margin-top: 30px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
}
.details p {
margin: 10px 0;
line-height: 1.6;
}
.label {
font-weight: bold;
color: #212529;
}
.value {
color: #495057;
}
</style>
</head>
<body>
<div class="container">
<h1>Error: ${errorMessage}</h1>
<h2>User Details</h2>
<div class="details">
<p><span class="label">UserRole:</span> <span class="value">${userDetails?.UserRole}</span></p>
<p><span class="label">Name:</span> <span class="value">${userDetails?.Name}</span></p>
<p><span class="label">Email:</span> <span class="value">${userDetails?.Email}</span></p>
<p><span class="label">UserId:</span> <span class="value">${userDetails?.UserId}</span></p>
<p><span class="label">ExtUserId:</span> <span class="value">${userDetails?.objectId}</span></p>
<p><span class="label">TenantId:</span> <span class="value">${userDetails?.TenantId}</span></p>
</div>
</div>
</body>
</html>`,
});
} catch (err) {
console.log('err while sending mail to support', err);
}
}
@@ -46,7 +46,7 @@ export default async function getDrive(request) {
return { error: 'Please provide required parameter!' };
}
} catch (err) {
console.log('err', err);
console.log('err', err?.response?.data || err);
if (err.code == 209) {
return { error: 'Invalid session token' };
} else {
@@ -21,6 +21,12 @@ const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY;
const eSignName = 'OpenSign';
const eSigncontact = 'hello@opensignlabs.com';
const docUrl = `${serverUrl}/classes/contracts_Document`;
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
};
async function unlinkFile(path) {
if (fs.existsSync(path)) {
@@ -90,13 +96,7 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
isCompleted = true;
}
const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted };
const signedRes = await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
const signedRes = await axios.put(`${docUrl}/${docId}`, body, { headers });
return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail };
} catch (err) {
console.log('update doc err ', err);
@@ -141,13 +141,7 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
html: body,
mailProvider: mailProvider,
};
await axios.post(serverUrl + '/functions/sendmailv3', params, {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
}
} catch (err) {
console.log('err in sendnotifymail', err);
@@ -255,13 +249,7 @@ async function sendCompletedMail(obj) {
filename: docName,
};
try {
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
// console.log('res', res.data.result);
if (res.data?.result?.status !== 'success') {
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
@@ -298,13 +286,7 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
fs.writeFileSync(certificatePath, signedCertificate);
const file = await uploadFile('certificate.pdf', certificatePath);
const body = { CertificateUrl: file.imageUrl };
await axios.put(serverUrl + '/classes/contracts_Document/' + doc.objectId, body, {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
await axios.put(`${docUrl}/${doc.objectId}`, body, { headers });
// used in API only
if (doc.IsSendMail === false) {
console.log("don't send mail");
@@ -313,6 +295,41 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
}
saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId);
unlinkFile(pfx.name);
return file.imageUrl;
}
/**
* Process a PDF for signing:
* - updates audit trail, generates certificate.
* - Optionally inserts a signature placeholder (Placeholder()).
* - Otherwise (no merge + no placeholder), it flattens forms for finalization.
*
* @param {Object} _resDoc - Document details (expects AuditTrail, etc.)
* @param {Buffer|Uint8Array} pdfBytes - Original PDF bytes
* @param {string} [options.reason] - Reason text used in placeholder
* @param {string} [options.UserPtr] - user pointer (for audit trail)
* @param {string} [options.ipAddress] - IP (for audit trail)
* @param {string} [options.Signature] - Signature (for audit trail)
* @returns {Promise<Buffer>} merged PDF Buffer
*/
async function processPdf(_resDoc, PdfBuffer, reason) {
// No CC merge; operate directly on the original PDF
const pdfDoc = await PDFDocument.load(PdfBuffer);
const form = pdfDoc.getForm();
// Updates the field appearances to ensure visual changes are reflected.
form.updateFieldAppearances();
// Flattens the form, converting all form fields into non-editable, static content
form.flatten();
Placeholder({
pdfDoc: pdfDoc,
reason: `Digitally signed by ${eSignName} for ${reason}`,
location: 'n/a',
name: eSignName,
contactInfo: eSigncontact,
signatureLength: 16000,
});
const pdfWithPlaceholderBytes = await pdfDoc.save();
return Buffer.from(pdfWithPlaceholderBytes);
}
/**
*
@@ -389,7 +406,11 @@ async function PDF(req) {
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
let isCompleted = false;
if (_resDoc.Signers && _resDoc.Signers.length > 0) {
if (auditTrail.length === _resDoc.Signers.length) {
const removePrefill =
_resDoc?.Placeholders?.length > 0 &&
_resDoc?.Placeholders?.filter(x => x?.Role !== 'prefill');
if (auditTrail.length === removePrefill?.length) {
// if (auditTrail.length === _resDoc.Signers.length) {
isCompleted = true;
}
} else {
@@ -408,24 +429,9 @@ async function PDF(req) {
signersName && signersName.length > 0
? signersName?.join(', ')
: username + ' <' + userEmail + '>';
const pdfDoc = await PDFDocument.load(PdfBuffer);
const form = pdfDoc.getForm();
// Updates the field appearances to ensure visual changes are reflected.
form.updateFieldAppearances();
// Flattens the form, converting all form fields into non-editable, static content
form.flatten();
const p12Cert = new P12Signer(P12Buffer, { passphrase: passphrase || null });
signedFilePath = `./exports/signed_${name}`;
Placeholder({
pdfDoc: pdfDoc,
reason: `Digitally signed by ${eSignName} for ${reason}`,
location: 'n/a',
name: eSignName,
contactInfo: eSigncontact,
signatureLength: 16000,
});
const pdfWithPlaceholderBytes = await pdfDoc.save();
PdfBuffer = Buffer.from(pdfWithPlaceholderBytes);
PdfBuffer = await processPdf(_resDoc, PdfBuffer, reason, UserPtr, userIP, sign);
//`new signPDF` create new instance of pdfBuffer and p12Buffer
const OBJ = new SignPdf();
// `signedDocs` is used to signpdf digitally
@@ -484,9 +490,7 @@ async function PDF(req) {
console.log('Err in signpdf', err);
const body = { DebugginLog: err?.message };
try {
await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, {
headers: { 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY },
});
await axios.put(`${docUrl}/${docId}`, body, { headers });
} catch (err) {
console.log('err in saving debugginglog', err);
}
@@ -25,6 +25,7 @@ export default function reportJson(id, currentUserId) {
'RequestSubject',
'ExtUserPtr.TenantId.RequestBody',
'ExtUserPtr.TenantId.RequestSubject',
'DocSentAt',
];
const filterKeys = [
'TimeToCompleteDays',
+2452 -2407
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.879.0",
"@aws-sdk/s3-request-presigner": "^3.879.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",
@@ -27,16 +27,16 @@
"@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0",
"axios": "^1.11.0",
"axios": "^1.12.2",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"date-fns-tz": "^3.2.0",
"dotenv": "^17.2.1",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"form-data": "^4.0.4",
"generate-api-key": "^1.0.2",
"googleapis": "^159.0.0",
"libreoffice-convert": "^1.6.1",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.0.3",
"mongodb": "^6.19.0",
"multer": "^2.0.2",
@@ -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.8.1",
"posthog-node": "^5.8.4",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.2.0",
"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.0",
"eslint": "^9.34.0",
"@babel/eslint-parser": "^7.28.4",
"eslint": "^9.35.0",
"jasmine": "^5.10.0",
"mongodb-runner": "^5.9.2",
"mongodb-runner": "^5.9.3",
"nodemon": "^3.1.10",
"nyc": "^17.1.0",
"prettier": "^3.6.2"