mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-17 21:25:54 +02:00
821 lines
24 KiB
JavaScript
821 lines
24 KiB
JavaScript
import dotenv from 'dotenv';
|
||
import { format, toZonedTime } from 'date-fns-tz';
|
||
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
|
||
import crypto from 'node:crypto';
|
||
import {
|
||
PDFDocument,
|
||
PDFName,
|
||
rgb,
|
||
degrees,
|
||
// StandardFonts,
|
||
PDFArray,
|
||
PDFDict,
|
||
} from 'pdf-lib';
|
||
import { parseUploadFile } from './utils/fileUtils.js';
|
||
|
||
dotenv.config({ quiet: true });
|
||
|
||
export const cloudServerUrl = 'http://localhost:8080/app';
|
||
export const serverAppId = process.env.APP_ID || 'opensign';
|
||
export const appName = 'OpenSign™';
|
||
export const prefillDraftDocWidget = ['date', 'textbox', 'checkbox', 'radio button', 'image'];
|
||
export const prefillDraftTemWidget = [
|
||
'date',
|
||
'textbox',
|
||
'checkbox',
|
||
'radio button',
|
||
'image',
|
||
'dropdown',
|
||
];
|
||
export const MAX_NAME_LENGTH = 250;
|
||
export const MAX_NOTE_LENGTH = 200;
|
||
export const MAX_DESCRIPTION_LENGTH = 500;
|
||
export const color = [
|
||
'#93a3db',
|
||
'#e6c3db',
|
||
'#c0e3bc',
|
||
'#bce3db',
|
||
'#b8ccdb',
|
||
'#ceb8db',
|
||
'#ffccff',
|
||
'#99ffcc',
|
||
'#cc99ff',
|
||
'#ffcc99',
|
||
'#66ccff',
|
||
'#ffffcc',
|
||
];
|
||
|
||
export const prefillBlockColor = 'transparent';
|
||
export function replaceMailVaribles(subject, body, variables) {
|
||
let replacedSubject = subject;
|
||
let replacedBody = body;
|
||
|
||
for (const variable in variables) {
|
||
const regex = new RegExp(`{{${variable}}}`, 'g');
|
||
if (subject) {
|
||
replacedSubject = replacedSubject.replace(regex, variables[variable]);
|
||
}
|
||
if (body) {
|
||
replacedBody = replacedBody.replace(regex, variables[variable]);
|
||
}
|
||
}
|
||
const result = { subject: replacedSubject, body: replacedBody };
|
||
return result;
|
||
}
|
||
|
||
export const saveFileUsage = async (size, fileUrl, userId) => {
|
||
//checking server url and save file's size
|
||
try {
|
||
if (userId) {
|
||
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
|
||
const tenantQuery = new Parse.Query('partners_Tenant');
|
||
tenantQuery.equalTo('UserId', userPtr);
|
||
const tenant = await tenantQuery.first({ useMasterKey: true });
|
||
if (tenant) {
|
||
const tenantPtr = { __type: 'Pointer', className: 'partners_Tenant', objectId: tenant.id };
|
||
try {
|
||
const tenantCredits = new Parse.Query('partners_TenantCredits');
|
||
tenantCredits.equalTo('PartnersTenant', tenantPtr);
|
||
const res = await tenantCredits.first({ useMasterKey: true });
|
||
if (res) {
|
||
const response = JSON.parse(JSON.stringify(res));
|
||
const usedStorage = response?.usedStorage ? response.usedStorage + size : size;
|
||
const updateCredit = new Parse.Object('partners_TenantCredits');
|
||
updateCredit.id = res.id;
|
||
updateCredit.set('usedStorage', usedStorage);
|
||
await updateCredit.save(null, { useMasterKey: true });
|
||
} else {
|
||
const newCredit = new Parse.Object('partners_TenantCredits');
|
||
newCredit.set('usedStorage', size);
|
||
newCredit.set('PartnersTenant', tenantPtr);
|
||
await newCredit.save(null, { useMasterKey: true });
|
||
}
|
||
} catch (err) {
|
||
console.log('err in save usage', err);
|
||
}
|
||
saveDataFile(size, fileUrl, tenantPtr, userPtr);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.log('err in fetch tenant Id', err);
|
||
}
|
||
};
|
||
|
||
//function for save fileUrl and file size in particular client db class partners_DataFiles
|
||
const saveDataFile = async (size, fileUrl, tenantPtr, UserId) => {
|
||
try {
|
||
const newDataFiles = new Parse.Object('partners_DataFiles');
|
||
newDataFiles.set('FileUrl', fileUrl);
|
||
newDataFiles.set('FileSize', size);
|
||
newDataFiles.set('TenantPtr', tenantPtr);
|
||
newDataFiles.set('UserId', UserId);
|
||
await newDataFiles.save(null, { useMasterKey: true });
|
||
} catch (err) {
|
||
console.log('error in save usage ', err);
|
||
}
|
||
};
|
||
|
||
export const updateMailCount = async extUserId => {
|
||
// Update count in contracts_Users class
|
||
const query = new Parse.Query('contracts_Users');
|
||
query.equalTo('objectId', extUserId);
|
||
|
||
try {
|
||
const contractUser = await query.first({ useMasterKey: true });
|
||
if (contractUser) {
|
||
const _extRes = JSON.parse(JSON.stringify(contractUser));
|
||
contractUser.increment('EmailCount', 1);
|
||
await contractUser.save(null, { useMasterKey: true });
|
||
}
|
||
} catch (error) {
|
||
console.log('Error updating EmailCount in contracts_Users: ' + error.message);
|
||
}
|
||
};
|
||
|
||
export function sanitizeFileName(fileName) {
|
||
// Remove spaces and invalid characters
|
||
const file = fileName.replace(/[^a-zA-Z0-9._-]/g, '');
|
||
const removedot = file.replace(/\.(?=.*\.)/g, '');
|
||
return removedot.replace(/[^a-zA-Z0-9._-]/g, '');
|
||
}
|
||
|
||
export const useLocal = process.env.USE_LOCAL ? process.env.USE_LOCAL.toLowerCase() : 'false';
|
||
export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '465' ? false : true;
|
||
export const smtpenable =
|
||
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
|
||
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
|
||
// `generateId` is used to unique Id for fileAdapter
|
||
export function generateId(length) {
|
||
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||
let result = '';
|
||
const charactersLength = characters.length;
|
||
for (let i = 0; i < length; i++) {
|
||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* FlattenPdf renders field values as static content and removes the interactive
|
||
* form layer. Signatures are stripped entirely. Non-widget annotations (links,
|
||
* comments, stamps) are preserved.
|
||
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
|
||
* @returns {Promise<Uint8Array>} flatPdf - pdf file in Uint8Array
|
||
*/
|
||
export const flattenPdf = async pdfFile => {
|
||
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
|
||
|
||
try {
|
||
const acroFormEntry = pdfDoc.catalog.get(PDFName.of('AcroForm'));
|
||
const acroForm = pdfDoc.context.lookupMaybe
|
||
? pdfDoc.context.lookupMaybe(acroFormEntry)
|
||
: pdfDoc.context.lookup(acroFormEntry);
|
||
|
||
if (acroForm && typeof acroForm.set === 'function') {
|
||
// Avoid pdf-lib form APIs here; some malformed PDFs crash while
|
||
// iterating/removing fields. Clearing /Fields directly is safer.
|
||
acroForm.set(PDFName.of('Fields'), pdfDoc.context.obj([]));
|
||
acroForm.delete(PDFName.of('XFA'));
|
||
acroForm.delete(PDFName.of('SigFlags'));
|
||
}
|
||
} catch {
|
||
// If AcroForm is malformed, continue with page annotation cleanup.
|
||
}
|
||
|
||
for (const page of pdfDoc.getPages()) {
|
||
try {
|
||
const annotationsRef = page.node.get(PDFName.of('Annots'));
|
||
if (!annotationsRef) continue;
|
||
|
||
const annotations = pdfDoc.context.lookup(annotationsRef);
|
||
if (!annotations || !annotations.asArray) continue;
|
||
|
||
const filtered = annotations.asArray().filter(annotRef => {
|
||
try {
|
||
const annot = pdfDoc.context.lookup(annotRef);
|
||
const subtype = annot?.get(PDFName.of('Subtype'));
|
||
return subtype?.toString() !== '/Widget';
|
||
} catch {
|
||
return true;
|
||
}
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
page.node.delete(PDFName.of('Annots'));
|
||
} else {
|
||
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
|
||
}
|
||
} catch {
|
||
// best effort cleanup
|
||
}
|
||
}
|
||
|
||
try {
|
||
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
|
||
} catch {
|
||
// best effort cleanup
|
||
}
|
||
|
||
return await pdfDoc.save({ useObjectStreams: false });
|
||
};
|
||
|
||
/* ---- flattenPdf private helpers ---- */
|
||
|
||
function _safeGetWidgetsServer(field) {
|
||
try {
|
||
return field.acroField?.getWidgets?.() || [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function _getWidgetRectServer(widget, pdfDoc) {
|
||
try {
|
||
const r = widget.getRectangle?.();
|
||
if (r && isFinite(r.x) && isFinite(r.y) && isFinite(r.width) && isFinite(r.height)) {
|
||
return r;
|
||
}
|
||
} catch {
|
||
/* fall through to manual extraction */
|
||
}
|
||
|
||
try {
|
||
const rectArr = widget.dict?.lookup?.(PDFName.of('Rect'));
|
||
if (!rectArr || typeof rectArr.size !== 'function' || rectArr.size() !== 4) return null;
|
||
|
||
const x1 = _numberFromPdfObjectServer(rectArr.get(0));
|
||
const y1 = _numberFromPdfObjectServer(rectArr.get(1));
|
||
const x2 = _numberFromPdfObjectServer(rectArr.get(2));
|
||
const y2 = _numberFromPdfObjectServer(rectArr.get(3));
|
||
|
||
if ([x1, y1, x2, y2].some(v => !isFinite(v))) return null;
|
||
|
||
return {
|
||
x: Math.min(x1, x2),
|
||
y: Math.min(y1, y2),
|
||
width: Math.abs(x2 - x1),
|
||
height: Math.abs(y2 - y1),
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function _numberFromPdfObjectServer(obj) {
|
||
if (!obj) return NaN;
|
||
if (typeof obj.asNumber === 'function') return obj.asNumber();
|
||
if (typeof obj.numberValue === 'function') return obj.numberValue();
|
||
return Number(obj?.value ?? obj);
|
||
}
|
||
|
||
function _getWidgetPageServer(pdfDoc, pages, widget) {
|
||
try {
|
||
const pRef = widget.P?.();
|
||
if (pRef) {
|
||
for (const page of pages) {
|
||
if (page.ref === pRef) return page;
|
||
}
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
|
||
try {
|
||
const pRef = widget.dict?.get?.(PDFName.of('P'));
|
||
if (pRef) {
|
||
for (const page of pages) {
|
||
if (page.ref === pRef) return page;
|
||
}
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
|
||
// Fallback: search page annots
|
||
try {
|
||
for (const page of pages) {
|
||
const annots = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
|
||
if (!annots) continue;
|
||
|
||
for (let i = 0; i < annots.size(); i++) {
|
||
const ref = annots.get(i);
|
||
if (ref === widget.ref) return page;
|
||
}
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function _drawWidgetBoxServer(page, rect) {
|
||
try {
|
||
page.drawRectangle({
|
||
x: rect.x,
|
||
y: rect.y,
|
||
width: rect.width,
|
||
height: rect.height,
|
||
borderWidth: 0.6,
|
||
borderColor: rgb(0.65, 0.65, 0.65),
|
||
});
|
||
} catch {
|
||
/* best effort */
|
||
}
|
||
}
|
||
|
||
function _drawTextFieldServer(page, field, rect, font) {
|
||
let text = '';
|
||
try {
|
||
text = field.getText?.() ?? '';
|
||
} catch {
|
||
text = '';
|
||
}
|
||
text = String(text ?? '');
|
||
|
||
if (!text) return;
|
||
|
||
let multiline = false;
|
||
try {
|
||
multiline = field.isMultiline?.() ?? false;
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
let comb = false;
|
||
try {
|
||
comb = field.isCombed?.() ?? false;
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
if (comb) {
|
||
_drawCombTextServer(page, text, rect, font);
|
||
return;
|
||
}
|
||
|
||
if (multiline || text.includes('\n')) {
|
||
_drawMultilineTextServer(page, text, rect, font);
|
||
return;
|
||
}
|
||
|
||
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
|
||
const baselineY = rect.y + Math.max(2, (rect.height - fontSize) / 2);
|
||
|
||
page.drawText(text, {
|
||
x: rect.x + 2,
|
||
y: baselineY,
|
||
size: fontSize,
|
||
font,
|
||
color: rgb(0, 0, 0),
|
||
maxWidth: Math.max(1, rect.width - 4),
|
||
});
|
||
}
|
||
|
||
function _drawMultilineTextServer(page, text, rect, font) {
|
||
const lines = String(text).replace(/\r/g, '').split('\n');
|
||
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
|
||
const lineHeight = fontSize + 1.5;
|
||
|
||
let y = rect.y + rect.height - fontSize - 2;
|
||
|
||
for (const line of lines) {
|
||
if (y < rect.y + 1) break;
|
||
|
||
page.drawText(line, {
|
||
x: rect.x + 2,
|
||
y,
|
||
size: fontSize,
|
||
font,
|
||
color: rgb(0, 0, 0),
|
||
maxWidth: Math.max(1, rect.width - 4),
|
||
lineHeight,
|
||
});
|
||
|
||
y -= lineHeight;
|
||
}
|
||
}
|
||
|
||
function _drawCombTextServer(page, text, rect, font) {
|
||
const chars = String(text).split('');
|
||
const count = Math.max(chars.length, 1);
|
||
const cellWidth = rect.width / count;
|
||
const fontSize = Math.max(8, Math.min(12, rect.height - 4));
|
||
|
||
chars.forEach((ch, i) => {
|
||
const textWidth = font.widthOfTextAtSize(ch, fontSize);
|
||
const x = rect.x + i * cellWidth + (cellWidth - textWidth) / 2;
|
||
const y = rect.y + Math.max(2, (rect.height - fontSize) / 2);
|
||
|
||
page.drawText(ch, {
|
||
x,
|
||
y,
|
||
size: fontSize,
|
||
font,
|
||
color: rgb(0, 0, 0),
|
||
});
|
||
|
||
if (i < count - 1) {
|
||
try {
|
||
page.drawLine({
|
||
start: { x: rect.x + (i + 1) * cellWidth, y: rect.y },
|
||
end: { x: rect.x + (i + 1) * cellWidth, y: rect.y + rect.height },
|
||
thickness: 0.4,
|
||
color: rgb(0.75, 0.75, 0.75),
|
||
});
|
||
} catch {
|
||
/* best effort */
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function _fitSingleLineFontSizeServer(text, rect, font) {
|
||
let size = Math.min(12, rect.height - 4);
|
||
size = Math.max(size, 6);
|
||
|
||
while (size > 6) {
|
||
const width = font.widthOfTextAtSize(text, size);
|
||
if (width <= rect.width - 4) return size;
|
||
size -= 0.5;
|
||
}
|
||
|
||
return 6;
|
||
}
|
||
|
||
function _drawCheckBoxServer(page, field, rect, zapf) {
|
||
let checked = false;
|
||
try {
|
||
checked = field.isChecked();
|
||
} catch {
|
||
checked = false;
|
||
}
|
||
|
||
if (!checked) return;
|
||
|
||
const size = Math.max(8, Math.min(rect.width, rect.height) - 4);
|
||
|
||
page.drawText('\u2714', {
|
||
x: rect.x + Math.max(1, (rect.width - size * 0.7) / 2),
|
||
y: rect.y + Math.max(1, (rect.height - size) / 2),
|
||
size,
|
||
font: zapf,
|
||
color: rgb(0, 0, 0),
|
||
});
|
||
}
|
||
|
||
function _drawRadioGroupServer(page, field, widget, rect) {
|
||
let selected = null;
|
||
try {
|
||
selected = field.getSelected();
|
||
} catch {
|
||
selected = null;
|
||
}
|
||
|
||
if (!selected) return;
|
||
|
||
let widgetOnValue = null;
|
||
try {
|
||
widgetOnValue = widget.getOnValue?.();
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
if (!widgetOnValue) {
|
||
try {
|
||
const ap = widget.dict?.lookupMaybe?.(PDFName.of('AP'), PDFDict);
|
||
const n = ap?.lookupMaybe?.(PDFName.of('N'), PDFDict);
|
||
if (n) {
|
||
const keys = n.keys();
|
||
for (const k of keys) {
|
||
const name = k?.decodeText?.() ?? k?.encodedName ?? String(k);
|
||
if (name !== '/Off' && name !== 'Off') {
|
||
widgetOnValue = name.replace(/^\//, '');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
const selectedStr = String(selected).replace(/^\//, '');
|
||
const onStr = String(widgetOnValue ?? '').replace(/^\//, '');
|
||
|
||
if (!onStr || selectedStr !== onStr) return;
|
||
|
||
// Circle outline
|
||
try {
|
||
page.drawEllipse({
|
||
x: rect.x + rect.width / 2,
|
||
y: rect.y + rect.height / 2,
|
||
xScale: rect.width / 2 - 1,
|
||
yScale: rect.height / 2 - 1,
|
||
borderWidth: 0.8,
|
||
borderColor: rgb(0, 0, 0),
|
||
});
|
||
} catch {
|
||
/* best effort */
|
||
}
|
||
|
||
// Inner filled dot
|
||
try {
|
||
const r = Math.min(rect.width, rect.height) / 4;
|
||
page.drawEllipse({
|
||
x: rect.x + rect.width / 2,
|
||
y: rect.y + rect.height / 2,
|
||
xScale: r,
|
||
yScale: r,
|
||
color: rgb(0, 0, 0),
|
||
});
|
||
} catch {
|
||
/* best effort */
|
||
}
|
||
}
|
||
|
||
function _drawDropdownServer(page, field, rect, font) {
|
||
let text = '';
|
||
try {
|
||
const selected = field.getSelected?.();
|
||
if (Array.isArray(selected)) {
|
||
text = selected.join(', ');
|
||
} else {
|
||
text = selected ?? '';
|
||
}
|
||
} catch {
|
||
text = '';
|
||
}
|
||
|
||
text = String(text ?? '');
|
||
if (!text) return;
|
||
|
||
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
|
||
|
||
page.drawText(text, {
|
||
x: rect.x + 2,
|
||
y: rect.y + Math.max(2, (rect.height - fontSize) / 2),
|
||
size: fontSize,
|
||
font,
|
||
color: rgb(0, 0, 0),
|
||
maxWidth: Math.max(1, rect.width - 12),
|
||
});
|
||
}
|
||
|
||
function _drawOptionListServer(page, field, rect, font) {
|
||
let selected = [];
|
||
try {
|
||
selected = field.getSelected?.() || [];
|
||
} catch {
|
||
selected = [];
|
||
}
|
||
|
||
if (!Array.isArray(selected)) {
|
||
selected = [selected].filter(Boolean);
|
||
}
|
||
|
||
if (!selected.length) return;
|
||
|
||
const lines = selected.map(v => String(v));
|
||
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
|
||
const lineHeight = fontSize + 1.5;
|
||
|
||
let y = rect.y + rect.height - fontSize - 2;
|
||
|
||
for (const line of lines) {
|
||
if (y < rect.y + 1) break;
|
||
|
||
page.drawText(line, {
|
||
x: rect.x + 2,
|
||
y,
|
||
size: fontSize,
|
||
font,
|
||
color: rgb(0, 0, 0),
|
||
maxWidth: Math.max(1, rect.width - 4),
|
||
});
|
||
|
||
y -= lineHeight;
|
||
}
|
||
}
|
||
|
||
/** Remove only Widget annotations; preserve links, stamps, comments, etc. */
|
||
function _removeWidgetAnnotationsServer(pdfDoc) {
|
||
for (const page of pdfDoc.getPages()) {
|
||
try {
|
||
const annotationsRef = page.node.get(PDFName.of('Annots'));
|
||
if (!annotationsRef) continue;
|
||
|
||
const annotations = pdfDoc.context.lookup(annotationsRef);
|
||
if (!annotations || !annotations.asArray) continue;
|
||
|
||
const filtered = annotations.asArray().filter(annotRef => {
|
||
try {
|
||
const annot = pdfDoc.context.lookup(annotRef);
|
||
const subtype = annot?.get(PDFName.of('Subtype'));
|
||
return subtype?.toString() !== '/Widget';
|
||
} catch {
|
||
return true;
|
||
}
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
page.node.delete(PDFName.of('Annots'));
|
||
} else {
|
||
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
|
||
}
|
||
} catch {
|
||
/* best effort */
|
||
}
|
||
}
|
||
|
||
try {
|
||
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
|
||
} catch {
|
||
/* best effort */
|
||
}
|
||
}
|
||
|
||
// Format date and time for the selected timezone
|
||
export const formatTimeInTimezone = (date, timezone) => {
|
||
const nyDate = timezone && toZonedTime(date, timezone);
|
||
const generatedDate = timezone
|
||
? format(nyDate, 'EEE, dd MMM yyyy HH:mm:ss zzz', { timeZone: timezone })
|
||
: new Date(date).toUTCString();
|
||
return generatedDate;
|
||
};
|
||
|
||
// `getSecureUrl` is used to return local secure url if local files
|
||
export const getSecureUrl = url => {
|
||
const fileUrl = new URL(url)?.pathname?.includes('/files/');
|
||
if (fileUrl) {
|
||
try {
|
||
const file = getSignedLocalUrl(url);
|
||
if (file) {
|
||
return { url: file };
|
||
} else {
|
||
return { url: '' };
|
||
}
|
||
} catch (err) {
|
||
console.log('err while fileupload ', err);
|
||
return { url: '' };
|
||
}
|
||
} else {
|
||
return { url: url };
|
||
}
|
||
};
|
||
|
||
export const mailTemplate = param => {
|
||
const themeColor = '#47a3ad';
|
||
const subject = `${param.senderName} has requested you to sign "${param.title}"`;
|
||
const AppName = appName;
|
||
const logo = `<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' />`;
|
||
|
||
const body =
|
||
"<html><head><meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='background:white;padding-bottom:20px'><div style='padding:10px'>" +
|
||
logo +
|
||
`</div><div style='padding:2px;font-family:system-ui;background-color:${themeColor}'><p style='font-size:20px;font-weight:400;color:white;padding-left:20px'>Digital Signature Request</p></div><div><p style='padding:20px;font-size:14px;margin-bottom:10px'>` +
|
||
param.senderName +
|
||
' has requested you to review and sign <strong>' +
|
||
param.title +
|
||
"</strong>.</p><div style='padding: 5px 0px 5px 25px;display:flex;flex-direction:row;justify-content:space-around'><table><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Sender</td><td></td><td style='color:#626363;font-weight:bold'>" +
|
||
param.senderMail +
|
||
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Organization</td><td></td><td style='color:#626363;font-weight:bold'> " +
|
||
param.organization +
|
||
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expires on</td><td></td><td style='color:#626363;font-weight:bold'>" +
|
||
param.localExpireDate +
|
||
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Note</td><td></td><td style='color:#626363;font-weight:bold'>" +
|
||
param.note +
|
||
"</td></tr><tr><td></td><td></td></tr></table></div> <div style='margin-left:70px'><a target=_blank href=" +
|
||
param.signingUrl +
|
||
"><button style='padding:12px;background-color:#d46b0f;color:white;border:0px;font-weight:bold;margin-top:30px'>Sign here</button></a></div><div style='display:flex;justify-content:center;margin-top:10px'></div></div></div><div><p> This is an automated email from " +
|
||
AppName +
|
||
'. For any queries regarding this email, please contact the sender ' +
|
||
param.senderMail +
|
||
` directly.</p></div></div></body></html>`;
|
||
|
||
return { subject, body };
|
||
};
|
||
|
||
export const selectFormat = data => {
|
||
switch (data) {
|
||
case 'L':
|
||
return 'MM/dd/yyyy';
|
||
case 'MM/DD/YYYY':
|
||
return 'MM/dd/yyyy';
|
||
case 'DD-MM-YYYY':
|
||
return 'dd-MM-yyyy';
|
||
case 'DD/MM/YYYY':
|
||
return 'dd/MM/yyyy';
|
||
case 'LL':
|
||
return 'MMMM dd, yyyy';
|
||
case 'DD MMM, YYYY':
|
||
return 'dd MMM, yyyy';
|
||
case 'YYYY-MM-DD':
|
||
return 'yyyy-MM-dd';
|
||
case 'MM-DD-YYYY':
|
||
return 'MM-dd-yyyy';
|
||
case 'MM.DD.YYYY':
|
||
return 'MM.dd.yyyy';
|
||
case 'MMM DD, YYYY':
|
||
return 'MMM dd, yyyy';
|
||
case 'MMMM DD, YYYY':
|
||
return 'MMMM dd, yyyy';
|
||
case 'DD MMMM, YYYY':
|
||
return 'dd MMMM, yyyy';
|
||
case 'DD.MM.YYYY':
|
||
return 'dd.MM.yyyy';
|
||
case 'DD-MMM-YYYY':
|
||
return 'dd-MMM-yyyy';
|
||
default:
|
||
return 'MM/dd/yyyy';
|
||
}
|
||
};
|
||
|
||
export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
|
||
const zonedDate = toZonedTime(date, timeZone); // Convert date to the given timezone
|
||
const timeFormat = is12Hour ? 'hh:mm:ss a' : 'HH:mm:ss';
|
||
return dateFormat
|
||
? format(zonedDate, `${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`, { timeZone })
|
||
: formatTimeInTimezone(date, timeZone);
|
||
}
|
||
|
||
export const randomId = (digit = 8) => {
|
||
// 1. Grab a cryptographically-secure 32-bit random value
|
||
// Use crypto for stronger randomness
|
||
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
|
||
const raw = randomBytes[0]; // 0 … 4,294,967,295
|
||
|
||
// Calculate the min and max for the given digit length
|
||
const min = Math.pow(10, digit - 1); // e.g., digit=3 → 100
|
||
const max = Math.pow(10, digit) - 1; // e.g., digit=3 → 999
|
||
const range = max - min + 1;
|
||
|
||
// Collapse random value into the range and shift
|
||
return min + (raw % range);
|
||
};
|
||
export const handleValidImage = async Placeholder => {
|
||
const updatedPlaceholders = [];
|
||
|
||
for (const placeholder of Placeholder || []) {
|
||
//Clean and format signerPtr
|
||
let signerPtr = placeholder.signerPtr;
|
||
// Check if signerPtr exists and has an id
|
||
if (signerPtr?.id) {
|
||
// Case 1: If signerPtr is a Parse Object instance
|
||
if (signerPtr instanceof Parse.Object) {
|
||
// If signerPtr has no attributes, it’s a plain pointer already
|
||
if (!signerPtr.attributes || Object.keys(signerPtr.attributes).length === 0) {
|
||
// Convert to a clean pointer using Parse’s built-in method
|
||
signerPtr = signerPtr.toPointer();
|
||
} else {
|
||
// If it has attributes, manually construct the pointer object
|
||
signerPtr = {
|
||
__type: 'Pointer',
|
||
className: signerPtr.className,
|
||
objectId: signerPtr.id,
|
||
};
|
||
}
|
||
// Case 2: If signerPtr is already a plain JS object resembling a pointer
|
||
} else if (typeof signerPtr === 'object' && signerPtr.className && signerPtr.objectId) {
|
||
// Normalize it to a valid Parse pointer object
|
||
signerPtr = {
|
||
__type: 'Pointer',
|
||
className: signerPtr.className,
|
||
objectId: signerPtr.objectId,
|
||
};
|
||
}
|
||
}
|
||
|
||
//Process placeHolder if Role is 'prefill'
|
||
if (placeholder?.Role === 'prefill') {
|
||
const updatedRole = [];
|
||
for (const item of placeholder.placeHolder || []) {
|
||
const updatedPos = [];
|
||
for (const posItem of item.pos || []) {
|
||
if (
|
||
(posItem?.type === 'image' || posItem?.type === 'draw') &&
|
||
posItem?.options?.response
|
||
) {
|
||
const validUrl = await getPresignedUrl(posItem?.options?.response);
|
||
updatedPos.push({
|
||
...posItem,
|
||
...(item.SignUrl !== undefined && { SignUrl: validUrl }),
|
||
options: { ...posItem.options, response: validUrl },
|
||
});
|
||
} else {
|
||
updatedPos.push(posItem);
|
||
}
|
||
}
|
||
updatedRole.push({ ...item, pos: updatedPos });
|
||
}
|
||
|
||
updatedPlaceholders.push({ ...placeholder, signerPtr, placeHolder: updatedRole });
|
||
} else {
|
||
// Not prefill role, just push as-is
|
||
updatedPlaceholders.push({ ...placeholder, signerPtr });
|
||
}
|
||
}
|
||
return updatedPlaceholders;
|
||
};
|