orbitx-core-models 11.175.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/avatars.d.ts +15 -0
- package/dist/helpers/avatars.d.ts.map +1 -0
- package/dist/helpers/avatars.js +122 -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/invoiceDocument.d.ts +63 -0
- package/dist/helpers/invoiceDocument.d.ts.map +1 -0
- package/dist/helpers/invoiceDocument.js +148 -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 +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Runs an async route handler and hands any error to Express's error handler. */
|
|
2
|
+
export declare const catchAsync: (fn: (req: any, res: any, next: any) => Promise<any>) => (req: any, res: any, next: any) => void;
|
|
3
|
+
export declare const HTTP: {
|
|
4
|
+
readonly OK: 200;
|
|
5
|
+
readonly CREATED: 201;
|
|
6
|
+
readonly BAD_REQUEST: 400;
|
|
7
|
+
readonly FORBIDDEN: 403;
|
|
8
|
+
readonly NOT_FOUND: 404;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=_http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_http.d.ts","sourceRoot":"","sources":["../../src/helpers/_http.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,eAAO,MAAM,UAAU,GACpB,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,MACnD,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,SAE7B,CAAC;AAEJ,eAAO,MAAM,IAAI;;;;;;CAMP,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Internal to the helpers (not exported from the package): Express plumbing the shared controllers use.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.HTTP = exports.catchAsync = void 0;
|
|
5
|
+
/** Runs an async route handler and hands any error to Express's error handler. */
|
|
6
|
+
const catchAsync = (fn) => (req, res, next) => {
|
|
7
|
+
fn(req, res, next).catch(next);
|
|
8
|
+
};
|
|
9
|
+
exports.catchAsync = catchAsync;
|
|
10
|
+
exports.HTTP = {
|
|
11
|
+
OK: 200,
|
|
12
|
+
CREATED: 201,
|
|
13
|
+
BAD_REQUEST: 400,
|
|
14
|
+
FORBIDDEN: 403,
|
|
15
|
+
NOT_FOUND: 404,
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function createAvatarController({ companyIdOf, upload, remove }: {
|
|
2
|
+
companyIdOf: (req: any) => any;
|
|
3
|
+
upload: (file: any, folder: string) => Promise<{
|
|
4
|
+
publicUrl: string;
|
|
5
|
+
s3Key: string;
|
|
6
|
+
}>;
|
|
7
|
+
remove: (s3Key: string) => Promise<void>;
|
|
8
|
+
}): {
|
|
9
|
+
getAvatars: (req: any, res: any, next: any) => void;
|
|
10
|
+
createAvatar: (req: any, res: any, next: any) => void;
|
|
11
|
+
updateAvatar: (req: any, res: any, next: any) => void;
|
|
12
|
+
deleteAvatar: (req: any, res: any, next: any) => void;
|
|
13
|
+
setDefaultAvatar: (req: any, res: any, next: any) => void;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=avatars.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avatars.d.ts","sourceRoot":"","sources":["../../src/helpers/avatars.ts"],"names":[],"mappings":"AAcA,wBAAgB,sBAAsB,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;IACtE,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrF,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;;;;;;EAiHA"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAvatarController = createAvatarController;
|
|
4
|
+
/**
|
|
5
|
+
* The chat avatars a company keeps (Company.avatars): list, add, rename/replace, delete, set default. Shared by the
|
|
6
|
+
* Ops backend (any company, from the URL) and the Client backend (the signed-in company):
|
|
7
|
+
* module.exports = createAvatarController({
|
|
8
|
+
* companyIdOf: (req) => req.user.owner, // or req.params.companyId
|
|
9
|
+
* upload: (file, folder) => <store it; resolves { publicUrl, s3Key }>,
|
|
10
|
+
* remove: (s3Key) => <delete it>,
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
const mongoose_1 = require("mongoose");
|
|
14
|
+
const company_model_1 = require("../models/company.model");
|
|
15
|
+
const errors_1 = require("./errors");
|
|
16
|
+
const _http_1 = require("./_http");
|
|
17
|
+
function createAvatarController({ companyIdOf, upload, remove }) {
|
|
18
|
+
const CompanyModel = company_model_1.Company;
|
|
19
|
+
const getAvatars = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
20
|
+
var _a, _b, _c;
|
|
21
|
+
const company = await CompanyModel.findById(companyIdOf(req)).select("avatars").lean();
|
|
22
|
+
if (!company)
|
|
23
|
+
return next(new errors_1.AppError("Company not found", _http_1.HTTP.NOT_FOUND));
|
|
24
|
+
res.status(_http_1.HTTP.OK).json({
|
|
25
|
+
success: true,
|
|
26
|
+
results: (_b = (_a = company.avatars) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
|
|
27
|
+
data: { avatars: (_c = company.avatars) !== null && _c !== void 0 ? _c : [] },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
// Body fields: name (text), avatar (file)
|
|
31
|
+
const createAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
32
|
+
var _a;
|
|
33
|
+
const { name } = req.body;
|
|
34
|
+
const file = req.file;
|
|
35
|
+
if (!name || !name.trim())
|
|
36
|
+
return next(new errors_1.AppError("Please provide a name for the avatar", _http_1.HTTP.BAD_REQUEST));
|
|
37
|
+
if (!file)
|
|
38
|
+
return next(new errors_1.AppError("Please upload an image file", _http_1.HTTP.BAD_REQUEST));
|
|
39
|
+
const company = await CompanyModel.findById(companyIdOf(req));
|
|
40
|
+
if (!company)
|
|
41
|
+
return next(new errors_1.AppError("Company not found", _http_1.HTTP.NOT_FOUND));
|
|
42
|
+
const { publicUrl, s3Key } = await upload(file, `avatars/${company._id}`);
|
|
43
|
+
// Never the default on creation.
|
|
44
|
+
company.avatars = (_a = company.avatars) !== null && _a !== void 0 ? _a : [];
|
|
45
|
+
company.avatars.push({ name: name.trim(), url: publicUrl, s3Key, isDefault: false });
|
|
46
|
+
await company.save();
|
|
47
|
+
res.status(_http_1.HTTP.CREATED).json({
|
|
48
|
+
success: true,
|
|
49
|
+
message: "Avatar uploaded successfully",
|
|
50
|
+
data: { avatar: company.avatars[company.avatars.length - 1] },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
// Loads the company and one of its avatars, or answers the error.
|
|
54
|
+
async function findAvatar(req, next) {
|
|
55
|
+
var _a;
|
|
56
|
+
const { avatarId } = req.params;
|
|
57
|
+
if (!mongoose_1.Types.ObjectId.isValid(avatarId)) {
|
|
58
|
+
next(new errors_1.AppError("Invalid avatar ID", _http_1.HTTP.BAD_REQUEST));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const company = await CompanyModel.findById(companyIdOf(req));
|
|
62
|
+
if (!company) {
|
|
63
|
+
next(new errors_1.AppError("Company not found", _http_1.HTTP.NOT_FOUND));
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const avatar = ((_a = company.avatars) !== null && _a !== void 0 ? _a : []).id(avatarId);
|
|
67
|
+
if (!avatar) {
|
|
68
|
+
next(new errors_1.AppError("Avatar not found", _http_1.HTTP.NOT_FOUND));
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { company, avatar, avatarId };
|
|
72
|
+
}
|
|
73
|
+
// Body fields (all optional): name (text), avatar (file)
|
|
74
|
+
const updateAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
75
|
+
const found = await findAvatar(req, next);
|
|
76
|
+
if (!found)
|
|
77
|
+
return;
|
|
78
|
+
const { company, avatar } = found;
|
|
79
|
+
const { name } = req.body;
|
|
80
|
+
const file = req.file;
|
|
81
|
+
if (!name && !file)
|
|
82
|
+
return next(new errors_1.AppError("Please provide a name or a new image to update", _http_1.HTTP.BAD_REQUEST));
|
|
83
|
+
if (name && name.trim())
|
|
84
|
+
avatar.name = name.trim();
|
|
85
|
+
if (file) {
|
|
86
|
+
const oldS3Key = avatar.s3Key;
|
|
87
|
+
const { publicUrl, s3Key } = await upload(file, `avatars/${company._id}`);
|
|
88
|
+
avatar.url = publicUrl;
|
|
89
|
+
avatar.s3Key = s3Key;
|
|
90
|
+
// The old file goes only after the new one is stored.
|
|
91
|
+
await remove(oldS3Key);
|
|
92
|
+
}
|
|
93
|
+
await company.save();
|
|
94
|
+
res.status(_http_1.HTTP.OK).json({ success: true, message: "Avatar updated successfully", data: { avatar } });
|
|
95
|
+
});
|
|
96
|
+
const deleteAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
97
|
+
const found = await findAvatar(req, next);
|
|
98
|
+
if (!found)
|
|
99
|
+
return;
|
|
100
|
+
const { company, avatar } = found;
|
|
101
|
+
const s3KeyToDelete = avatar.s3Key;
|
|
102
|
+
avatar.deleteOne();
|
|
103
|
+
await company.save();
|
|
104
|
+
// A file that fails to delete is logged, never an error for the user.
|
|
105
|
+
remove(s3KeyToDelete).catch((err) => console.warn(`Failed to delete S3 object '${s3KeyToDelete}': ${err.message}`));
|
|
106
|
+
res.status(_http_1.HTTP.OK).json({ success: true, message: "Avatar deleted successfully" });
|
|
107
|
+
});
|
|
108
|
+
// Only one avatar can be the default.
|
|
109
|
+
const setDefaultAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
110
|
+
var _a;
|
|
111
|
+
const found = await findAvatar(req, next);
|
|
112
|
+
if (!found)
|
|
113
|
+
return;
|
|
114
|
+
const { company, avatar, avatarId } = found;
|
|
115
|
+
((_a = company.avatars) !== null && _a !== void 0 ? _a : []).forEach((a) => {
|
|
116
|
+
a.isDefault = a._id.toString() === avatarId;
|
|
117
|
+
});
|
|
118
|
+
await company.save();
|
|
119
|
+
res.status(_http_1.HTTP.OK).json({ success: true, message: "Default avatar set successfully", data: { avatar } });
|
|
120
|
+
});
|
|
121
|
+
return { getAvatars, createAvatar, updateAvatar, deleteAvatar, setDefaultAvatar };
|
|
122
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns encrypted lead contact details (email/phone tokens) back into real values. Used by the Ops and Client
|
|
3
|
+
* backends; each one only says how it reaches chat-service's decryption:
|
|
4
|
+
* const { decryptLeadContactInfo, decryptTokensInTexts } = createContactDecryptor({
|
|
5
|
+
* requestDecrypt: async (field, value) => <the JSON reply of the decrypt route for { [field]: value }>,
|
|
6
|
+
* });
|
|
7
|
+
* Best-effort: a value that can't be decrypted shows as "Encrypted" (never ciphertext) and never breaks the caller.
|
|
8
|
+
*/
|
|
9
|
+
export declare const unwrapPlainEmail: (value: any) => any;
|
|
10
|
+
export declare const looksEncrypted: (value: any) => boolean;
|
|
11
|
+
export type ContactField = "email" | "phone";
|
|
12
|
+
export declare function createContactDecryptor({ requestDecrypt }: {
|
|
13
|
+
requestDecrypt: (field: ContactField, value: string) => Promise<any>;
|
|
14
|
+
}): {
|
|
15
|
+
decryptLeadContactInfo: (raw: {
|
|
16
|
+
email?: string;
|
|
17
|
+
phone?: string;
|
|
18
|
+
} | null | undefined) => Promise<{
|
|
19
|
+
email?: string;
|
|
20
|
+
phone?: string;
|
|
21
|
+
}>;
|
|
22
|
+
decryptTokensInTexts: (texts: string[]) => Promise<string[]>;
|
|
23
|
+
looksEncrypted: (value: any) => boolean;
|
|
24
|
+
unwrapPlainEmail: (value: any) => any;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=contactDecrypt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contactDecrypt.d.ts","sourceRoot":"","sources":["../../src/helpers/contactDecrypt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,eAAO,MAAM,gBAAgB,GAAI,OAAO,GAAG,KAAG,GAG7C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,KAAG,OACmC,CAAC;AAehF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;AAE7C,wBAAgB,sBAAsB,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;CAAE;kCActF;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS;gBACtE,MAAM;gBAAU,MAAM;;kCAmBL,MAAM,EAAE,KAAG,OAAO,CAAC,MAAM,EAAE,CAAC;4BApDnC,GAAG,KAAG,OAAO;8BALX,GAAG,KAAG,GAAG;EAmFhD"}
|
|
@@ -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;
|