Merge pull request #1292 from nxglabs/sync-to-public_repo-17730712175

Merge pull request #1291 from nxglabs/staging
This commit is contained in:
prafull-opensignlabs
2025-09-15 14:51:16 +00:00
parent 6cb2d69566
commit ca4ccfd889
45 changed files with 2541 additions and 2240 deletions
+1 -1
View File
@@ -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"
@@ -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);
}
@@ -697,12 +697,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 (
+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",
+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)
+4 -4
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();
@@ -1047,7 +1047,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
/>
+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] && (
@@ -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,8 @@
import axios from 'axios';
import { cloudServerUrl, generateId, serverAppId } from '../../Utils.js';
import sendmailtoSupport from './sendMailToSupport.js';
import { cloudServerUrl, generateId, serverAppId } from '../../../Utils.js';
import sendmailtoSupport from '../sendMailToSupport.js';
import { deleteContactsInBatch, deleteDataFiles, deleteInBatches } from './deleteFileUrl.js';
import { MAX_ATTEMPTS } from './deleteUtils.js';
const serverUrl = cloudServerUrl;
const appId = serverAppId;
@@ -250,108 +251,10 @@ export async function deleteUser(userId, adminId) {
}
}
// 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',
@@ -375,27 +278,51 @@ export const deleteUserPost = async (req, res) => {
}
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);
// sendmailtoSupport(userDetails, errorMessage);
// return res.status(401).send(errorMessage);
}
const response = await deleteUser(userId);
@@ -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);
}
@@ -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',
+1894 -1849
View File
File diff suppressed because it is too large Load Diff
+7 -7
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.886.0",
"@aws-sdk/s3-request-presigner": "^3.886.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
@@ -31,7 +31,7 @@
"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",
@@ -49,17 +49,17 @@
"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.3",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.2.0",
"rate-limiter-flexible": "^7.3.0",
"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",
"nodemon": "^3.1.10",