orbitx-core-models 11.174.0 → 11.176.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helpers/_http.d.ts +10 -0
- package/dist/helpers/_http.d.ts.map +1 -0
- package/dist/helpers/_http.js +16 -0
- package/dist/helpers/addonState.d.ts +20 -0
- package/dist/helpers/addonState.d.ts.map +1 -0
- package/dist/helpers/addonState.js +77 -0
- package/dist/helpers/analyticsBuckets.d.ts +32 -0
- package/dist/helpers/analyticsBuckets.d.ts.map +1 -0
- package/dist/helpers/analyticsBuckets.js +143 -0
- package/dist/helpers/avatars.d.ts +15 -0
- package/dist/helpers/avatars.d.ts.map +1 -0
- package/dist/helpers/avatars.js +122 -0
- package/dist/helpers/chatAttachments.d.ts +33 -0
- package/dist/helpers/chatAttachments.d.ts.map +1 -0
- package/dist/helpers/chatAttachments.js +81 -0
- package/dist/helpers/contactDecrypt.d.ts +26 -0
- package/dist/helpers/contactDecrypt.d.ts.map +1 -0
- package/dist/helpers/contactDecrypt.js +95 -0
- package/dist/helpers/emailTemplates.d.ts +19 -0
- package/dist/helpers/emailTemplates.d.ts.map +1 -0
- package/dist/helpers/emailTemplates.js +276 -0
- package/dist/helpers/errors.d.ts +27 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +101 -0
- package/dist/helpers/invoiceDocument.d.ts +63 -0
- package/dist/helpers/invoiceDocument.d.ts.map +1 -0
- package/dist/helpers/invoiceDocument.js +148 -0
- package/dist/helpers/listQuery.d.ts +35 -0
- package/dist/helpers/listQuery.d.ts.map +1 -0
- package/dist/helpers/listQuery.js +180 -0
- package/dist/helpers/myUpdates.d.ts +16 -0
- package/dist/helpers/myUpdates.d.ts.map +1 -0
- package/dist/helpers/myUpdates.js +94 -0
- package/dist/helpers/ownActivity.d.ts +8 -0
- package/dist/helpers/ownActivity.d.ts.map +1 -0
- package/dist/helpers/ownActivity.js +114 -0
- package/dist/helpers/ownTickets.d.ts +21 -0
- package/dist/helpers/ownTickets.d.ts.map +1 -0
- package/dist/helpers/ownTickets.js +169 -0
- package/dist/helpers/s3Storage.d.ts +38 -0
- package/dist/helpers/s3Storage.d.ts.map +1 -0
- package/dist/helpers/s3Storage.js +73 -0
- package/dist/helpers/sessions.d.ts +22 -0
- package/dist/helpers/sessions.d.ts.map +1 -0
- package/dist/helpers/sessions.js +61 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -0
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turns encrypted lead contact details (email/phone tokens) back into real values. Used by the Ops and Client
|
|
4
|
+
* backends; each one only says how it reaches chat-service's decryption:
|
|
5
|
+
* const { decryptLeadContactInfo, decryptTokensInTexts } = createContactDecryptor({
|
|
6
|
+
* requestDecrypt: async (field, value) => <the JSON reply of the decrypt route for { [field]: value }>,
|
|
7
|
+
* });
|
|
8
|
+
* Best-effort: a value that can't be decrypted shows as "Encrypted" (never ciphertext) and never breaks the caller.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.looksEncrypted = exports.unwrapPlainEmail = void 0;
|
|
12
|
+
exports.createContactDecryptor = createContactDecryptor;
|
|
13
|
+
const ENCRYPTED_TOKEN_RE = /^\{.*\}$|__enc|^[a-f0-9]{16}$/i;
|
|
14
|
+
// A lead can hold a plain address wrapped in braces ("{name@example.com}"), which is not a token.
|
|
15
|
+
const PLAIN_BRACED_EMAIL = /^\{([^\s@{}]+@[^\s@{}]+\.[^\s@{}]+)\}$/;
|
|
16
|
+
const unwrapPlainEmail = (value) => {
|
|
17
|
+
const m = typeof value === "string" && value.match(PLAIN_BRACED_EMAIL);
|
|
18
|
+
return m ? m[1] : value;
|
|
19
|
+
};
|
|
20
|
+
exports.unwrapPlainEmail = unwrapPlainEmail;
|
|
21
|
+
const looksEncrypted = (value) => typeof value === "string" && ENCRYPTED_TOKEN_RE.test((0, exports.unwrapPlainEmail)(value));
|
|
22
|
+
exports.looksEncrypted = looksEncrypted;
|
|
23
|
+
// Leads saved before the Employee Dashboard fix lost their "__enc__" / "__encphone__" markers
|
|
24
|
+
// ("{local@<16-hex>}" and a bare 16-hex phone). chat-service only accepts the marked form, so restore the
|
|
25
|
+
// markers before asking it to decrypt.
|
|
26
|
+
const restoreEmailMarkers = (value) => {
|
|
27
|
+
const m = typeof value === "string" && value.match(/^\{([^\s@{}]+)@([a-f0-9]{16})\}$/i);
|
|
28
|
+
return m ? `{${m[1]}@__enc__${m[2]}}` : value;
|
|
29
|
+
};
|
|
30
|
+
const restorePhoneMarkers = (value) => typeof value === "string" && /^[a-f0-9]{16}$/i.test(value) ? `__encphone__{${value}}` : value;
|
|
31
|
+
const EMAIL_TOKENS = /\{[^\s@{}]+@__enc__[a-f0-9]{16}\}/gi;
|
|
32
|
+
const PHONE_TOKENS = /__encphone__\{[a-f0-9]{16}\}/gi;
|
|
33
|
+
function createContactDecryptor({ requestDecrypt }) {
|
|
34
|
+
// Decrypts one field. Email and phone are sent separately so a value that cannot be decrypted never stops the
|
|
35
|
+
// other one from being shown.
|
|
36
|
+
async function decryptField(field, value) {
|
|
37
|
+
var _a;
|
|
38
|
+
try {
|
|
39
|
+
const json = await requestDecrypt(field, value);
|
|
40
|
+
if ((json === null || json === void 0 ? void 0 : json.success) && ((_a = json.data) === null || _a === void 0 ? void 0 : _a[field]))
|
|
41
|
+
return json.data[field];
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error(`Failed to decrypt lead ${field}:`, err === null || err === void 0 ? void 0 : err.message);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** The lead's email/phone as they should be shown: real values, plain values as they are, else "Encrypted". */
|
|
49
|
+
async function decryptLeadContactInfo(raw) {
|
|
50
|
+
const result = {
|
|
51
|
+
email: (0, exports.looksEncrypted)(raw === null || raw === void 0 ? void 0 : raw.email) ? "Encrypted" : (0, exports.unwrapPlainEmail)(raw === null || raw === void 0 ? void 0 : raw.email),
|
|
52
|
+
phone: (0, exports.looksEncrypted)(raw === null || raw === void 0 ? void 0 : raw.phone) ? "Encrypted" : raw === null || raw === void 0 ? void 0 : raw.phone,
|
|
53
|
+
};
|
|
54
|
+
const [email, phone] = await Promise.all([
|
|
55
|
+
(0, exports.looksEncrypted)(raw === null || raw === void 0 ? void 0 : raw.email) ? decryptField("email", restoreEmailMarkers(raw.email)) : null,
|
|
56
|
+
(0, exports.looksEncrypted)(raw === null || raw === void 0 ? void 0 : raw.phone) ? decryptField("phone", restorePhoneMarkers(raw.phone)) : null,
|
|
57
|
+
]);
|
|
58
|
+
if (email)
|
|
59
|
+
result.email = email;
|
|
60
|
+
if (phone)
|
|
61
|
+
result.phone = phone;
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replaces the encrypted email/phone tokens inside each text with their real values (a transcript, for example).
|
|
66
|
+
* Each distinct token is looked up once; a token that cannot be decrypted is left as it is.
|
|
67
|
+
*/
|
|
68
|
+
async function decryptTokensInTexts(texts) {
|
|
69
|
+
const joined = texts.join("\n");
|
|
70
|
+
const emails = [...new Set(joined.match(EMAIL_TOKENS) || [])];
|
|
71
|
+
const phones = [...new Set(joined.match(PHONE_TOKENS) || [])];
|
|
72
|
+
if (emails.length === 0 && phones.length === 0)
|
|
73
|
+
return texts;
|
|
74
|
+
const plain = new Map();
|
|
75
|
+
await Promise.all([
|
|
76
|
+
...emails.map(async (token) => {
|
|
77
|
+
const value = await decryptField("email", token);
|
|
78
|
+
if (value)
|
|
79
|
+
plain.set(token, value);
|
|
80
|
+
}),
|
|
81
|
+
...phones.map(async (token) => {
|
|
82
|
+
const value = await decryptField("phone", token);
|
|
83
|
+
if (value)
|
|
84
|
+
plain.set(token, value);
|
|
85
|
+
}),
|
|
86
|
+
]);
|
|
87
|
+
return texts.map((text) => {
|
|
88
|
+
let out = text;
|
|
89
|
+
for (const [token, value] of plain)
|
|
90
|
+
out = out.split(token).join(value);
|
|
91
|
+
return out;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { decryptLeadContactInfo, decryptTokensInTexts, looksEncrypted: exports.looksEncrypted, unwrapPlainEmail: exports.unwrapPlainEmail };
|
|
95
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email bodies more than one backend sends, kept here so they look the same everywhere.
|
|
3
|
+
* leadForwardedEmail: Ops "Forward" and the Client dashboard "Forward" (was services/email/templates/lead-forwarded.js).
|
|
4
|
+
* resetPasswordEmail: the password-reset code (Employee and Client; was templates/reset-password.js).
|
|
5
|
+
*/
|
|
6
|
+
export type LeadForwardedEmailInput = {
|
|
7
|
+
forwardedBy: string;
|
|
8
|
+
companyName: string;
|
|
9
|
+
fromOrbitxpro?: boolean;
|
|
10
|
+
rows: {
|
|
11
|
+
label: string;
|
|
12
|
+
value: unknown;
|
|
13
|
+
}[];
|
|
14
|
+
note?: string;
|
|
15
|
+
leadUrl?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const leadForwardedEmail: ({ forwardedBy, companyName, fromOrbitxpro, rows, note, leadUrl }: LeadForwardedEmailInput) => string;
|
|
18
|
+
export declare const resetPasswordEmail: (code: string | number) => string;
|
|
19
|
+
//# sourceMappingURL=emailTemplates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emailTemplates.d.ts","sourceRoot":"","sources":["../../src/helpers/emailTemplates.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,kEAAkE,uBAAuB,KAAG,MA4H9H,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,GAAG,MAAM,KAAG,MA2I1D,CAAC"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Email bodies more than one backend sends, kept here so they look the same everywhere.
|
|
4
|
+
* leadForwardedEmail: Ops "Forward" and the Client dashboard "Forward" (was services/email/templates/lead-forwarded.js).
|
|
5
|
+
* resetPasswordEmail: the password-reset code (Employee and Client; was templates/reset-password.js).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.resetPasswordEmail = exports.leadForwardedEmail = void 0;
|
|
9
|
+
// A lead forwarded by hand to any address someone types in (Ops "Forward" or the Client
|
|
10
|
+
// Dashboard's "Forward"). Same visual system as ticket-update.js. Every value is escaped here,
|
|
11
|
+
// since lead names, notes and contact details are free text that must never render as markup.
|
|
12
|
+
const esc = (value) => value === null || value === undefined || value === ""
|
|
13
|
+
? "-"
|
|
14
|
+
: String(value).replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
15
|
+
const FONT = "font-family:'Century Gothic','Trebuchet MS',Arial,sans-serif;";
|
|
16
|
+
const leadForwardedEmail = ({ forwardedBy, companyName, fromOrbitxpro, rows, note, leadUrl }) => {
|
|
17
|
+
const who = `<strong>${esc(forwardedBy)}</strong>`;
|
|
18
|
+
const intro = fromOrbitxpro
|
|
19
|
+
? `${who} from ORBITXPRO forwarded you a lead for <strong>${esc(companyName)}</strong>.`
|
|
20
|
+
: `${who} at <strong>${esc(companyName)}</strong> forwarded you this lead.`;
|
|
21
|
+
const rowHtml = rows
|
|
22
|
+
.map(({ label, value }) => `
|
|
23
|
+
<tr>
|
|
24
|
+
<td style="padding:7px 0;font-size:13px;color:#888888;font-weight:700;width:130px;vertical-align:top;${FONT}">${esc(label)}</td>
|
|
25
|
+
<td style="padding:7px 0;font-size:14px;color:#1a1a1a;${FONT}">${esc(value)}</td>
|
|
26
|
+
</tr>`)
|
|
27
|
+
.join("");
|
|
28
|
+
const noteHtml = note
|
|
29
|
+
? `
|
|
30
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px 0;background-color:#fafafa;border:1px solid #e8e8e8;border-left:4px solid #FE5E17;border-radius:0 8px 8px 0;">
|
|
31
|
+
<tr>
|
|
32
|
+
<td style="padding:14px 20px;">
|
|
33
|
+
<div style="font-size:13px;color:#888888;font-weight:700;margin-bottom:4px;${FONT}">Note</div>
|
|
34
|
+
<div style="font-size:14px;color:#1a1a1a;line-height:1.6;${FONT}">${esc(note).replace(/\n/g, "<br>")}</div>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
</table>`
|
|
38
|
+
: "";
|
|
39
|
+
const buttonHtml = leadUrl
|
|
40
|
+
? `
|
|
41
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 8px 0;">
|
|
42
|
+
<tr>
|
|
43
|
+
<td align="center">
|
|
44
|
+
<a href="${esc(leadUrl)}" style="display:inline-block;background-color:#FE5E17;color:#ffffff;text-decoration:none;font-weight:700;font-size:14px;padding:12px 28px;border-radius:6px;${FONT}">View Lead</a>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</table>`
|
|
48
|
+
: "";
|
|
49
|
+
return `<!DOCTYPE html>
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
<title>Lead Forwarded - ORBITXPRO</title>
|
|
55
|
+
<style>
|
|
56
|
+
* { box-sizing: border-box; }
|
|
57
|
+
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
|
58
|
+
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
|
59
|
+
body { margin: 0; padding: 0; background-color: #f9f9f9; ${FONT} -webkit-font-smoothing: antialiased; }
|
|
60
|
+
@media only screen and (max-width: 620px) {
|
|
61
|
+
.wrapper-td { padding: 12px 12px 30px 12px !important; }
|
|
62
|
+
.logo-table, .main-card { width: 100% !important; }
|
|
63
|
+
.body-td { padding: 24px 20px !important; }
|
|
64
|
+
.card-h2 { font-size: 19px !important; }
|
|
65
|
+
.card-p { font-size: 14px !important; }
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
</head>
|
|
69
|
+
<body style="margin:0;padding:0;background-color:#f9f9f9;${FONT}">
|
|
70
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
71
|
+
<tr>
|
|
72
|
+
<td align="center" class="wrapper-td" style="background-color:#f9f9f9;padding:20px 16px 40px 16px;">
|
|
73
|
+
|
|
74
|
+
<table class="logo-table" width="600" cellpadding="0" cellspacing="0" border="0">
|
|
75
|
+
<tr>
|
|
76
|
+
<td align="center" style="padding:24px 0 20px 0;">
|
|
77
|
+
<span style="font-size:28px;font-weight:300;letter-spacing:12px;color:#FE5E17;text-transform:uppercase;${FONT}">ORBITXPRO</span>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
</table>
|
|
81
|
+
|
|
82
|
+
<table class="main-card" width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;border:1px solid #e0e0e0;border-radius:12px;overflow:hidden;">
|
|
83
|
+
<tr>
|
|
84
|
+
<td height="80" style="background-color:#FE5E17;background-image:radial-gradient(rgba(255,255,255,0.18) 1.5px, transparent 1.5px), linear-gradient(135deg, rgba(255,255,255,0.06) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.06) 75%, transparent 75%, transparent);background-size:30px 30px, 90px 90px;background-repeat:repeat, repeat;"> </td>
|
|
85
|
+
</tr>
|
|
86
|
+
<tr>
|
|
87
|
+
<td class="body-td" style="padding:32px 36px 28px 36px;">
|
|
88
|
+
<h2 class="card-h2" style="color:#1a1a1a;font-size:22px;margin-top:0;margin-bottom:14px;font-weight:700;${FONT}">A Lead Was Forwarded to You</h2>
|
|
89
|
+
<p class="card-p" style="color:#555555;line-height:1.7;font-size:15px;margin-top:0;${FONT}">${intro}</p>
|
|
90
|
+
|
|
91
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;background-color:#fcfcfc;border:1px solid #e8e8e8;border-radius:8px;">
|
|
92
|
+
<tr>
|
|
93
|
+
<td style="padding:12px 20px;border-bottom:1px solid #eeeeee;">
|
|
94
|
+
<span style="font-size:13px;font-weight:700;color:#888888;text-transform:uppercase;letter-spacing:1px;${FONT}">Lead Details</span>
|
|
95
|
+
</td>
|
|
96
|
+
</tr>
|
|
97
|
+
<tr>
|
|
98
|
+
<td style="padding:6px 20px 12px 20px;">
|
|
99
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">${rowHtml}
|
|
100
|
+
</table>
|
|
101
|
+
</td>
|
|
102
|
+
</tr>
|
|
103
|
+
</table>
|
|
104
|
+
${noteHtml}${buttonHtml}
|
|
105
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0 0 0;">
|
|
106
|
+
<tr><td style="border-top:1px solid #eeeeee;font-size:0;"> </td></tr>
|
|
107
|
+
</table>
|
|
108
|
+
</td>
|
|
109
|
+
</tr>
|
|
110
|
+
<tr>
|
|
111
|
+
<td style="padding:28px 30px 30px 30px;text-align:center;">
|
|
112
|
+
<p style="margin:0 0 14px 0;font-size:13px;${FONT}">
|
|
113
|
+
<a href="https://www.linkedin.com/company/orbitxpro" style="color:#999999;text-decoration:none;margin:0 8px;">LinkedIn</a>
|
|
114
|
+
<a href="https://x.com/orbitxpro" style="color:#999999;text-decoration:none;margin:0 8px;">X</a>
|
|
115
|
+
<a href="https://www.instagram.com/orbitxpro_" style="color:#999999;text-decoration:none;margin:0 8px;">Instagram</a>
|
|
116
|
+
<a href="https://www.facebook.com/people/Orbitxpro/61588888121323/" style="color:#999999;text-decoration:none;margin:0 8px;">Facebook</a>
|
|
117
|
+
</p>
|
|
118
|
+
<a href="https://www.orbitxpro.com" style="color:#FE5E17;text-decoration:none;font-weight:700;font-size:14px;display:block;margin-bottom:6px;${FONT}">www.orbitxpro.com</a>
|
|
119
|
+
<p style="font-size:12px;color:#999999;margin:0 0 20px 0;${FONT}">
|
|
120
|
+
Questions? Email <a href="mailto:support@orbitxpro.com" style="color:#999999;font-weight:700;text-decoration:none;">support@orbitxpro.com</a>
|
|
121
|
+
</p>
|
|
122
|
+
<p style="font-size:11px;color:#999999;line-height:1.6;border-top:1px solid #eeeeee;padding-top:20px;margin:0;${FONT}">
|
|
123
|
+
This lead was forwarded through ORBITXPRO. To respond, contact the person who forwarded it; replies to this email aren't monitored.<br>
|
|
124
|
+
© 2026 ORBITXPRO Inc. | All rights reserved.
|
|
125
|
+
</p>
|
|
126
|
+
</td>
|
|
127
|
+
</tr>
|
|
128
|
+
</table>
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
</table>
|
|
132
|
+
</body>
|
|
133
|
+
</html>`;
|
|
134
|
+
};
|
|
135
|
+
exports.leadForwardedEmail = leadForwardedEmail;
|
|
136
|
+
const resetPasswordEmail = (code) => {
|
|
137
|
+
return `<!DOCTYPE html>
|
|
138
|
+
<html lang="en">
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="UTF-8">
|
|
141
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
142
|
+
<title>Password Reset Code - ORBITXPRO</title>
|
|
143
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
|
144
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
145
|
+
<style>
|
|
146
|
+
body { margin: 0; padding: 0; background-color: #f9f9f9; font-family: 'Outfit', sans-serif; -webkit-font-smoothing: antialiased; }
|
|
147
|
+
.wrapper { width: 100%; background-color: #f9f9f9; padding-bottom: 40px; }
|
|
148
|
+
.main-container { width: 100%; max-width: 600px; margin: 0 auto; background-color: #ffffff; }
|
|
149
|
+
|
|
150
|
+
/* Logo Header - CONSTANT */
|
|
151
|
+
.logo-header { padding: 30px 0; text-align: center; background-color: #f9f9f9; }
|
|
152
|
+
.dune-logo { font-size: 28px; font-weight: 300; letter-spacing: 12px; color: #FE5E17; text-transform: uppercase; margin: 0; display: inline-block; }
|
|
153
|
+
|
|
154
|
+
/* Banner Style - CONSTANT */
|
|
155
|
+
.banner {
|
|
156
|
+
background-color: #FE5E17;
|
|
157
|
+
background-image: radial-gradient(rgba(255, 255, 255, 0.15) 1.5px, transparent 1.5px), linear-gradient(135deg, rgba(255, 255, 255, 0.05) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0.05) 75%, transparent 75%, transparent);
|
|
158
|
+
background-size: 40px 40px, 120px 120px;
|
|
159
|
+
height: 80px; border-top-left-radius: 12px; border-top-right-radius: 12px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.content-card { border: 1px solid #e0e0e0; border-radius: 12px; overflow: hidden; background-color: #ffffff; }
|
|
163
|
+
.inner-padding { padding: 30px; text-align: left; }
|
|
164
|
+
h2 { color: #333333; font-size: 22px; margin-top: 0; }
|
|
165
|
+
p { color: #666666; line-height: 1.6; font-size: 15px; }
|
|
166
|
+
|
|
167
|
+
/* Reset Code Box */
|
|
168
|
+
.code-container {
|
|
169
|
+
text-align: center;
|
|
170
|
+
margin: 30px 0;
|
|
171
|
+
padding: 20px;
|
|
172
|
+
background-color: #fcfcfc;
|
|
173
|
+
border: 1px dashed #FE5E17;
|
|
174
|
+
border-radius: 8px;
|
|
175
|
+
}
|
|
176
|
+
.reset-code {
|
|
177
|
+
font-size: 32px;
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
letter-spacing: 8px;
|
|
180
|
+
color: #333333;
|
|
181
|
+
display: block;
|
|
182
|
+
}
|
|
183
|
+
.expiry-text {
|
|
184
|
+
font-size: 12px;
|
|
185
|
+
color: #888888;
|
|
186
|
+
margin-top: 10px;
|
|
187
|
+
display: block;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Security Alert Box */
|
|
191
|
+
.security-note {
|
|
192
|
+
background-color: #fffafb;
|
|
193
|
+
border-left: 4px solid #c0392b;
|
|
194
|
+
padding: 15px;
|
|
195
|
+
margin: 25px 0;
|
|
196
|
+
font-size: 13px;
|
|
197
|
+
color: #666666;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Footer - CONSTANT with HOVERS */
|
|
201
|
+
.footer { padding: 30px; text-align: center; font-size: 12px; color: #999999; }
|
|
202
|
+
.social-icons { margin-bottom: 15px; }
|
|
203
|
+
.social-icons a {
|
|
204
|
+
color: #999999;
|
|
205
|
+
text-decoration: none;
|
|
206
|
+
font-size: 18px;
|
|
207
|
+
margin: 0 8px;
|
|
208
|
+
transition: color 0.3s ease;
|
|
209
|
+
}
|
|
210
|
+
.social-icons a:hover { color: #FE5E17 !important; }
|
|
211
|
+
.website-link { color: #FE5E17; text-decoration: none; font-weight: 600; font-size: 14px; display: block; margin-bottom: 5px; }
|
|
212
|
+
.support-container { font-size: 12px; margin-bottom: 20px; }
|
|
213
|
+
.support-link {
|
|
214
|
+
color: #999999;
|
|
215
|
+
text-decoration: none;
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
transition: color 0.3s ease;
|
|
218
|
+
}
|
|
219
|
+
.support-link:hover { color: #FE5E17 !important; text-decoration: underline; }
|
|
220
|
+
.compliance-text { line-height: 1.5; border-top: 1px solid #eeeeee; padding-top: 20px; }
|
|
221
|
+
</style>
|
|
222
|
+
</head>
|
|
223
|
+
<body>
|
|
224
|
+
<div class="wrapper">
|
|
225
|
+
<div class="logo-header">
|
|
226
|
+
<div class="dune-logo">ORBITXPRO</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div class="main-container">
|
|
230
|
+
<div class="content-card">
|
|
231
|
+
<div class="banner"></div>
|
|
232
|
+
|
|
233
|
+
<div class="inner-padding">
|
|
234
|
+
<h2>Password Reset Request</h2>
|
|
235
|
+
<p>Hello,</p>
|
|
236
|
+
<p>We received a request to reset the password for your ORBITXPRO account. Use the verification code below to proceed with the reset process:</p>
|
|
237
|
+
|
|
238
|
+
<div class="code-container">
|
|
239
|
+
<span class="reset-code">${code}</span>
|
|
240
|
+
<span class="expiry-text">This code will expire in 10 minutes.</span>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="security-note">
|
|
244
|
+
<strong>Security Reminder:</strong> If you did not request a password reset, please ignore this email or contact support if you suspect unauthorized access to your account. Your password will not change until you use this code to create a new one.
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<p>For security reasons, never share this code with anyone. ORBITXPRO employees will never ask you for your password or verification codes.</p>
|
|
248
|
+
|
|
249
|
+
<hr style="border:0; border-top:1px solid #eeeeee; margin: 25px 0;">
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div class="footer">
|
|
253
|
+
<div class="social-icons">
|
|
254
|
+
<a href="https://www.linkedin.com/company/orbitxpro"><i class="fab fa-linkedin"></i></a>
|
|
255
|
+
<a href="https://x.com/orbitxpro"><i class="fab fa-twitter"></i></a>
|
|
256
|
+
<a href="https://www.instagram.com/orbitxpro_"><i class="fab fa-instagram"></i></a>
|
|
257
|
+
<a href="https://www.facebook.com/people/Orbitxpro/61588888121323/"><i class="fab fa-facebook"></i></a>
|
|
258
|
+
</div>
|
|
259
|
+
<a href="https://www.orbitxpro.com" class="website-link">www.orbitxpro.com</a>
|
|
260
|
+
<div class="support-container">
|
|
261
|
+
Didn't request this?
|
|
262
|
+
<a href="mailto:support@orbitxpro.com" class="support-link">Contact Customer Support</a>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="compliance-text">
|
|
265
|
+
© 2026 ORBITXPRO Inc. | Security Notification <br>
|
|
266
|
+
<strong>Please do not reply directly to this email.</strong>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</body>
|
|
273
|
+
</html>
|
|
274
|
+
`;
|
|
275
|
+
};
|
|
276
|
+
exports.resetPasswordEmail = resetPasswordEmail;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One error type and one Express error handler for every backend (Employee, Ops, Client, Partner). A backend's
|
|
3
|
+
* utils/app-error.js re-exports AppError, and its controllers/error.controller.js is
|
|
4
|
+
* module.exports = createErrorHandler({ development: <this backend's env says development> });
|
|
5
|
+
*/
|
|
6
|
+
/** An expected error the caller should see: its message goes back as-is with its HTTP status. */
|
|
7
|
+
export declare class AppError extends Error {
|
|
8
|
+
statusCode: number;
|
|
9
|
+
success: boolean;
|
|
10
|
+
isOperational: boolean;
|
|
11
|
+
/** Optional machine-readable code and details, sent to the client when set. */
|
|
12
|
+
code?: string;
|
|
13
|
+
details?: unknown;
|
|
14
|
+
constructor(message: string, statusCode: number, extra?: {
|
|
15
|
+
code?: string;
|
|
16
|
+
details?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The global error handler. In development the full error goes back; anywhere else (production, staging, or an
|
|
21
|
+
* environment name the backend doesn't set) known errors get a clean message and anything unexpected a generic 500.
|
|
22
|
+
* Before this was shared, a backend whose environment was neither "development" nor "production" never answered.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createErrorHandler({ development }: {
|
|
25
|
+
development: boolean | (() => boolean);
|
|
26
|
+
}): (err: any, _req: any, res: any, _next: any) => void;
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/helpers/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,iGAAiG;AACjG,qBAAa,QAAS,SAAQ,KAAK;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,UAAS;IAChB,aAAa,UAAQ;IACrB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;gBAEN,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE;CAO9F;AAyDD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CAAE,IAIpF,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG,EAAE,OAAO,GAAG,UAelD"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One error type and one Express error handler for every backend (Employee, Ops, Client, Partner). A backend's
|
|
4
|
+
* utils/app-error.js re-exports AppError, and its controllers/error.controller.js is
|
|
5
|
+
* module.exports = createErrorHandler({ development: <this backend's env says development> });
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.AppError = void 0;
|
|
9
|
+
exports.createErrorHandler = createErrorHandler;
|
|
10
|
+
/** An expected error the caller should see: its message goes back as-is with its HTTP status. */
|
|
11
|
+
class AppError extends Error {
|
|
12
|
+
constructor(message, statusCode, extra) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.success = false;
|
|
15
|
+
this.isOperational = true;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
if (extra === null || extra === void 0 ? void 0 : extra.code)
|
|
18
|
+
this.code = extra.code;
|
|
19
|
+
if ((extra === null || extra === void 0 ? void 0 : extra.details) !== undefined)
|
|
20
|
+
this.details = extra.details;
|
|
21
|
+
Error.captureStackTrace(this, this.constructor);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.AppError = AppError;
|
|
25
|
+
const BAD_REQUEST = 400;
|
|
26
|
+
const UNAUTHORIZED = 401;
|
|
27
|
+
const CONFLICT = 409;
|
|
28
|
+
const INTERNAL = 500;
|
|
29
|
+
// Invalid field value (e.g. a malformed id)
|
|
30
|
+
const handleCastErrorDB = (err) => new AppError(`Invalid ${err.path} : ${err.value}`, BAD_REQUEST);
|
|
31
|
+
// Duplicate key
|
|
32
|
+
const handleDuplicateFieldsDB = (err) => {
|
|
33
|
+
const key = Object.keys(err.keyValue || {})[0];
|
|
34
|
+
return new AppError(`${key} already exists, try another value.`, CONFLICT);
|
|
35
|
+
};
|
|
36
|
+
// Schema validation
|
|
37
|
+
const handleValidationErrorDB = (err) => {
|
|
38
|
+
const errors = Object.values(err.errors || {}).map((el) => el.message);
|
|
39
|
+
return new AppError(`Invalid input data. ${errors.join(". ")}`, BAD_REQUEST);
|
|
40
|
+
};
|
|
41
|
+
const handleJWTError = () => new AppError("Invalid token. Login again", UNAUTHORIZED);
|
|
42
|
+
const handleJWTExpireError = () => new AppError("Your token has been expired! please log in again", UNAUTHORIZED);
|
|
43
|
+
// File uploads (multer)
|
|
44
|
+
const handleMulterError = (err) => {
|
|
45
|
+
if (err.code === "LIMIT_UNEXPECTED_FILE")
|
|
46
|
+
return new AppError("Upload limit reached or invalid field name.", BAD_REQUEST);
|
|
47
|
+
if (err.code === "LIMIT_FILE_SIZE") {
|
|
48
|
+
return new AppError("File size limit exceeded. Maximum allowed size is 10 MB per file.", BAD_REQUEST);
|
|
49
|
+
}
|
|
50
|
+
return err;
|
|
51
|
+
};
|
|
52
|
+
const devError = (err, res) => {
|
|
53
|
+
res.status(err.statusCode).json({
|
|
54
|
+
success: err.success,
|
|
55
|
+
error: err,
|
|
56
|
+
message: err.message,
|
|
57
|
+
stack: err.stack,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const prodError = (err, res) => {
|
|
61
|
+
if (err.isOperational) {
|
|
62
|
+
res.status(err.statusCode).json({
|
|
63
|
+
success: err.success,
|
|
64
|
+
message: err.message,
|
|
65
|
+
...(err.code && { error: { code: err.code, ...(err.details && { details: err.details }) } }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.error("ERROR", err);
|
|
70
|
+
res.status(INTERNAL).json({ success: false, message: "Something went wrong!" });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The global error handler. In development the full error goes back; anywhere else (production, staging, or an
|
|
75
|
+
* environment name the backend doesn't set) known errors get a clean message and anything unexpected a generic 500.
|
|
76
|
+
* Before this was shared, a backend whose environment was neither "development" nor "production" never answered.
|
|
77
|
+
*/
|
|
78
|
+
function createErrorHandler({ development }) {
|
|
79
|
+
const isDev = () => (typeof development === "function" ? development() : development);
|
|
80
|
+
// Express recognises an error handler by its four arguments.
|
|
81
|
+
return (err, _req, res, _next) => {
|
|
82
|
+
err.statusCode = err.statusCode || INTERNAL;
|
|
83
|
+
err.success = err.success || false;
|
|
84
|
+
if (isDev())
|
|
85
|
+
return devError(err, res);
|
|
86
|
+
let error = { ...err, message: err.message };
|
|
87
|
+
if (err.name === "CastError")
|
|
88
|
+
error = handleCastErrorDB(error);
|
|
89
|
+
if (err.code === 11000)
|
|
90
|
+
error = handleDuplicateFieldsDB(error);
|
|
91
|
+
if (err.name === "ValidationError")
|
|
92
|
+
error = handleValidationErrorDB(error);
|
|
93
|
+
if (err.name === "JsonWebTokenError")
|
|
94
|
+
error = handleJWTError();
|
|
95
|
+
if (err.name === "TokenExpiredError")
|
|
96
|
+
error = handleJWTExpireError();
|
|
97
|
+
if (err.name === "MulterError")
|
|
98
|
+
error = handleMulterError(error);
|
|
99
|
+
prodError(error, res);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Who the invoice is from (owner, 2026-09-29). */
|
|
2
|
+
export declare const INVOICE_ISSUER: {
|
|
3
|
+
name: string;
|
|
4
|
+
addressLines: string[];
|
|
5
|
+
email: string;
|
|
6
|
+
};
|
|
7
|
+
/** The statuses a client can see; drafts and invoices waiting for approval stay inside Ops. */
|
|
8
|
+
export declare const CLIENT_VISIBLE_INVOICE_STATUSES: string[];
|
|
9
|
+
/** Approved invoices are charged together on the 5th of the month. */
|
|
10
|
+
export declare function nextInvoiceChargeDate(from?: Date): Date;
|
|
11
|
+
export declare function createInvoiceDocuments({ stripe }: {
|
|
12
|
+
stripe: any;
|
|
13
|
+
}): {
|
|
14
|
+
ISSUER: {
|
|
15
|
+
name: string;
|
|
16
|
+
addressLines: string[];
|
|
17
|
+
email: string;
|
|
18
|
+
};
|
|
19
|
+
nextInvoiceNumber: (at?: Date) => Promise<string>;
|
|
20
|
+
ensureInvoiceNumber: (invoice: any) => Promise<string>;
|
|
21
|
+
nextChargeDate: typeof nextInvoiceChargeDate;
|
|
22
|
+
buildInvoiceDocument: (invoiceId: any, { companyId, clientView }?: {
|
|
23
|
+
companyId?: any;
|
|
24
|
+
clientView?: boolean;
|
|
25
|
+
}) => Promise<{
|
|
26
|
+
invoice: {
|
|
27
|
+
_id: any;
|
|
28
|
+
number: string;
|
|
29
|
+
status: any;
|
|
30
|
+
issueDate: any;
|
|
31
|
+
periodStart: any;
|
|
32
|
+
periodEnd: any;
|
|
33
|
+
subtotal: any;
|
|
34
|
+
adjustments: any;
|
|
35
|
+
totalDue: any;
|
|
36
|
+
approvedAt: any;
|
|
37
|
+
paidAt: any;
|
|
38
|
+
chargeDate: Date | null;
|
|
39
|
+
stripeInvoiceUrl: any;
|
|
40
|
+
};
|
|
41
|
+
from: {
|
|
42
|
+
name: string;
|
|
43
|
+
addressLines: string[];
|
|
44
|
+
email: string;
|
|
45
|
+
};
|
|
46
|
+
billTo: {
|
|
47
|
+
name: any;
|
|
48
|
+
companyId: any;
|
|
49
|
+
address: any;
|
|
50
|
+
city: any;
|
|
51
|
+
state: any;
|
|
52
|
+
zip: any;
|
|
53
|
+
country: any;
|
|
54
|
+
email: any;
|
|
55
|
+
};
|
|
56
|
+
items: any;
|
|
57
|
+
card: {
|
|
58
|
+
brand: any;
|
|
59
|
+
last4: any;
|
|
60
|
+
} | null;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=invoiceDocument.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoiceDocument.d.ts","sourceRoot":"","sources":["../../src/helpers/invoiceDocument.ts"],"names":[],"mappings":"AAaA,mDAAmD;AACnD,eAAO,MAAM,cAAc;;;;CAI1B,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,+BAA+B,UAA8D,CAAC;AAE3G,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,IAAI,OAAa,GAAG,IAAI,CAI7D;AAED,wBAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,GAAG,CAAA;CAAE;;;;;;sCAOb,OAAO,CAAC,MAAM,CAAC;mCAOtB,GAAG,KAAG,OAAO,CAAC,MAAM,CAAC;;sCAmClB,GAAG,8BAAqC;QAAE,SAAS,CAAC,EAAE,GAAG,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoEjI"}
|