@heliyos/heliyos-api-core 1.0.69 → 1.0.71
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/authentication.d.ts +2 -0
- package/dist/authentication.js +97 -6
- package/dist/authorization.d.ts +3 -1
- package/dist/authorization.js +9 -4
- package/dist/email/index.d.ts +1 -0
- package/dist/email/index.js +1 -0
- package/dist/email/integration-not-synced.html +138 -0
- package/dist/middleware.js +7 -13
- package/dist/static/authPolicyFile.d.ts +1 -0
- package/dist/static/authPolicyFile.js +2 -0
- package/dist/static/authPolicyFile.ts +7 -0
- package/package.json +1 -1
- package/src/email/index.ts +1 -0
- package/src/email/integration-not-synced.html +138 -0
package/dist/authentication.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface IAuthResponseApiKey {
|
|
|
15
15
|
policy: IAuthResponseApiKeyPolicy[] | undefined;
|
|
16
16
|
userId: string;
|
|
17
17
|
organizationId: string;
|
|
18
|
+
organizationIsActive?: boolean;
|
|
19
|
+
organizationBillingStatus?: string;
|
|
18
20
|
}
|
|
19
21
|
interface IAuthResponseApiKeyPolicy {
|
|
20
22
|
resource: string;
|
package/dist/authentication.js
CHANGED
|
@@ -23,6 +23,52 @@ exports.authentication = void 0;
|
|
|
23
23
|
const basic_auth_1 = __importDefault(require("basic-auth"));
|
|
24
24
|
const customError_1 = require("./@types/globals/customError");
|
|
25
25
|
const _1 = require(".");
|
|
26
|
+
const INACTIVE_ORG_ALLOWED_PATH_PREFIXES = [
|
|
27
|
+
"/v1/platform/billing/overview",
|
|
28
|
+
"/v1/platform/billing/products",
|
|
29
|
+
"/v1/platform/billing/payments",
|
|
30
|
+
"/v1/platform/billing/portal-link",
|
|
31
|
+
"/v1/platform/billing/portal",
|
|
32
|
+
"/v1/platform/billing/customer-portal",
|
|
33
|
+
"/v1/platform/billing/callback/",
|
|
34
|
+
"/v1/auth/session/logout",
|
|
35
|
+
];
|
|
36
|
+
const BASIC_AUTH_ALLOWED_PATH_PREFIXES = [
|
|
37
|
+
"/v1/platform/billing/internal/",
|
|
38
|
+
"/v1/platform/usage/internal/log",
|
|
39
|
+
];
|
|
40
|
+
const canAccessWithInactiveOrganization = (path) => {
|
|
41
|
+
if (!path) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return INACTIVE_ORG_ALLOWED_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
45
|
+
};
|
|
46
|
+
const canAccessWithBasicAuth = (path) => {
|
|
47
|
+
if (!path) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return BASIC_AUTH_ALLOWED_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
51
|
+
};
|
|
52
|
+
const getCandidateSessionCookieNames = () => {
|
|
53
|
+
const explicitNodeEnv = String(process.env.NODE_ENV || "").trim();
|
|
54
|
+
const explicitPublicEnv = String(process.env.NEXT_PUBLIC_ENV || "").trim();
|
|
55
|
+
const names = new Set(["user_session"]);
|
|
56
|
+
[explicitNodeEnv, explicitPublicEnv, "production", "development", "local"]
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.forEach((envName) => names.add(`${envName}_user_session`));
|
|
59
|
+
return Array.from(names);
|
|
60
|
+
};
|
|
61
|
+
const getUserSessionCookieFromRequest = (req) => {
|
|
62
|
+
const cookies = req.cookies || {};
|
|
63
|
+
const cookieNames = getCandidateSessionCookieNames();
|
|
64
|
+
for (const name of cookieNames) {
|
|
65
|
+
const value = cookies[name];
|
|
66
|
+
if (value) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
26
72
|
/**
|
|
27
73
|
* Function to check internal and external authentication
|
|
28
74
|
* @param req
|
|
@@ -36,7 +82,7 @@ const authentication = (req, res, next) => __awaiter(void 0, void 0, void 0, fun
|
|
|
36
82
|
const container = {
|
|
37
83
|
input: {
|
|
38
84
|
authentication_header: req.headers.authorization,
|
|
39
|
-
user_session_cookie: req
|
|
85
|
+
user_session_cookie: getUserSessionCookieFromRequest(req),
|
|
40
86
|
ip: getIp(req),
|
|
41
87
|
auth_type: undefined,
|
|
42
88
|
},
|
|
@@ -46,7 +92,10 @@ const authentication = (req, res, next) => __awaiter(void 0, void 0, void 0, fun
|
|
|
46
92
|
},
|
|
47
93
|
};
|
|
48
94
|
// Check for the type of authentication
|
|
49
|
-
checkAuthType(container, res);
|
|
95
|
+
const authTypeCheckResponse = checkAuthType(container, res);
|
|
96
|
+
if (authTypeCheckResponse) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
50
99
|
// Based on type, do further authentication
|
|
51
100
|
// Either of BASIC / COOKIE / BEARER
|
|
52
101
|
const authenticationResponse = yield authenticateRequest(container);
|
|
@@ -62,6 +111,34 @@ const authentication = (req, res, next) => __awaiter(void 0, void 0, void 0, fun
|
|
|
62
111
|
message: "User not authenticated, Invalid token",
|
|
63
112
|
});
|
|
64
113
|
}
|
|
114
|
+
if (container.output.isBasicAuth &&
|
|
115
|
+
!canAccessWithBasicAuth(req.path || req.originalUrl || "")) {
|
|
116
|
+
return res.status(403).json({
|
|
117
|
+
error: {
|
|
118
|
+
status: 403,
|
|
119
|
+
err_msg: "FORBIDDEN",
|
|
120
|
+
},
|
|
121
|
+
message: "Basic authentication is restricted to internal service endpoints.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const authUser = authenticationResponse;
|
|
125
|
+
const hasOrganizationContext = Boolean(authUser === null || authUser === void 0 ? void 0 : authUser.organizationId);
|
|
126
|
+
const organizationIsActive = authUser === null || authUser === void 0 ? void 0 : authUser.organizationIsActive;
|
|
127
|
+
const shouldBlockInactiveOrganization = hasOrganizationContext &&
|
|
128
|
+
organizationIsActive !== true &&
|
|
129
|
+
!canAccessWithInactiveOrganization(req.path || req.originalUrl || "");
|
|
130
|
+
if (shouldBlockInactiveOrganization) {
|
|
131
|
+
return res.status(403).json({
|
|
132
|
+
error: {
|
|
133
|
+
status: 403,
|
|
134
|
+
err_msg: "FORBIDDEN",
|
|
135
|
+
},
|
|
136
|
+
message: "Organization billing is inactive. Please renew your subscription to continue.",
|
|
137
|
+
data: {
|
|
138
|
+
organizationBillingStatus: authUser.organizationBillingStatus || "past_due",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
65
142
|
// Set logged in user data which can be used later on
|
|
66
143
|
setLoggedInUser(container, req);
|
|
67
144
|
// Move to next chain
|
|
@@ -137,7 +214,15 @@ const isJwtToken = (token) => {
|
|
|
137
214
|
try {
|
|
138
215
|
if (token.indexOf(".") > -1) {
|
|
139
216
|
const token_parts = token.split(".");
|
|
140
|
-
|
|
217
|
+
if (!token_parts[0]) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const normalizedHeader = token_parts[0]
|
|
221
|
+
.replace(/-/g, "+")
|
|
222
|
+
.replace(/_/g, "/");
|
|
223
|
+
const padding = (4 - (normalizedHeader.length % 4)) % 4;
|
|
224
|
+
const paddedHeader = normalizedHeader + "=".repeat(padding);
|
|
225
|
+
const token_detail_string = Buffer.from(paddedHeader, "base64");
|
|
141
226
|
const token_detail = JSON.parse(token_detail_string.toString());
|
|
142
227
|
if (token_detail.typ && token_detail.typ.toUpperCase() === "JWT") {
|
|
143
228
|
return true;
|
|
@@ -266,6 +351,8 @@ const callAuthApiServer = (token) => __awaiter(void 0, void 0, void 0, function*
|
|
|
266
351
|
organizationId: authResult.data.data.payload.organizationId,
|
|
267
352
|
role: authResult.data.data.payload.role,
|
|
268
353
|
userFullName: authResult.data.data.payload.userFullName,
|
|
354
|
+
organizationIsActive: authResult.data.data.payload.organizationIsActive,
|
|
355
|
+
organizationBillingStatus: authResult.data.data.payload.organizationBillingStatus,
|
|
269
356
|
};
|
|
270
357
|
}
|
|
271
358
|
else {
|
|
@@ -273,7 +360,9 @@ const callAuthApiServer = (token) => __awaiter(void 0, void 0, void 0, function*
|
|
|
273
360
|
}
|
|
274
361
|
}
|
|
275
362
|
catch (error) {
|
|
276
|
-
|
|
363
|
+
_1.logger.error("Error while verifying bearer token with auth service", {
|
|
364
|
+
error: (error === null || error === void 0 ? void 0 : error.message) || error,
|
|
365
|
+
});
|
|
277
366
|
throw error;
|
|
278
367
|
}
|
|
279
368
|
});
|
|
@@ -286,7 +375,7 @@ const verifyApiKey = (apiKey) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
286
375
|
var _a, _b, _c, _d;
|
|
287
376
|
try {
|
|
288
377
|
// Call api key service to verify api key
|
|
289
|
-
const apiRes = yield _1.axios.authServer.post(`/
|
|
378
|
+
const apiRes = yield _1.axios.authServer.post(`/v1/auth/api_key/verify`, {
|
|
290
379
|
apiKey,
|
|
291
380
|
});
|
|
292
381
|
if ((_b = (_a = apiRes.data) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.isValid) {
|
|
@@ -298,7 +387,9 @@ const verifyApiKey = (apiKey) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
298
387
|
}
|
|
299
388
|
}
|
|
300
389
|
catch (err) {
|
|
301
|
-
|
|
390
|
+
_1.logger.error("Error while verifying API key with auth service", {
|
|
391
|
+
error: (err === null || err === void 0 ? void 0 : err.message) || err,
|
|
392
|
+
});
|
|
302
393
|
// If verified api key not found then throw error
|
|
303
394
|
const error = new customError_1.HttpError("Invalid api key.");
|
|
304
395
|
error.status = "403";
|
package/dist/authorization.d.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* @param resourceAction
|
|
6
6
|
* @returns
|
|
7
7
|
*/
|
|
8
|
-
export declare const authorizeUser: <T = string, U = string>(organizationId: T, userId: U, resourceAction: string
|
|
8
|
+
export declare const authorizeUser: <T = string, U = string>(organizationId: T, userId: U, resourceAction: string, options?: {
|
|
9
|
+
allowInactive?: boolean;
|
|
10
|
+
}) => Promise<{
|
|
9
11
|
isAllowed: string;
|
|
10
12
|
userRole: string;
|
|
11
13
|
}>;
|
package/dist/authorization.js
CHANGED
|
@@ -20,17 +20,22 @@ const axios_1 = require("./axios");
|
|
|
20
20
|
* @returns
|
|
21
21
|
*/
|
|
22
22
|
// eslint-disable-next-line import/prefer-default-export, @typescript-eslint/naming-convention
|
|
23
|
-
const authorizeUser = (organizationId, userId, resourceAction) => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
+
const authorizeUser = (organizationId, userId, resourceAction, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
24
|
try {
|
|
25
|
-
const authenticationResponse = yield axios_1.coreAxios.authServer.post(
|
|
25
|
+
const authenticationResponse = yield axios_1.coreAxios.authServer.post(`/v1/auth/user/${userId}`, {
|
|
26
|
+
resourceAction,
|
|
27
|
+
organizationId,
|
|
28
|
+
allowInactive: Boolean(options === null || options === void 0 ? void 0 : options.allowInactive),
|
|
29
|
+
});
|
|
26
30
|
return authenticationResponse.data.data;
|
|
27
31
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
|
28
32
|
}
|
|
29
33
|
catch (err) {
|
|
30
|
-
|
|
34
|
+
const status = String((err === null || err === void 0 ? void 0 : err.status) || "");
|
|
35
|
+
if (status === "401") {
|
|
31
36
|
throw err;
|
|
32
37
|
}
|
|
33
|
-
if (
|
|
38
|
+
if (status === "403") {
|
|
34
39
|
throw err;
|
|
35
40
|
}
|
|
36
41
|
const error = new customError_1.HttpError("Something went wrong with Authentication");
|
package/dist/email/index.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export declare const emailTemplates: {
|
|
|
9
9
|
notificationImmediateGrouped: string;
|
|
10
10
|
notificationDailySummary: string;
|
|
11
11
|
organizationDailySummary: string;
|
|
12
|
+
integrationNotSynced: string;
|
|
12
13
|
};
|
|
13
14
|
export declare const getEmailTemplate: (template: string, data: object) => string;
|
package/dist/email/index.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.emailTemplates = {
|
|
|
41
41
|
notificationImmediateGrouped: "notification-immediate-grouped.html",
|
|
42
42
|
notificationDailySummary: "notification-daily-summary.html",
|
|
43
43
|
organizationDailySummary: "organization-daily-summary.html",
|
|
44
|
+
integrationNotSynced: "integration-not-synced.html",
|
|
44
45
|
};
|
|
45
46
|
/**
|
|
46
47
|
* Simple markdown to HTML converter for email templates
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Integration Not Synched</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: Arial, sans-serif;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
color: #0e0d0c;
|
|
13
|
+
background-color: #f8f8f6;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.container {
|
|
19
|
+
max-width: 600px;
|
|
20
|
+
margin: 20px auto;
|
|
21
|
+
background-color: #ffffff;
|
|
22
|
+
border: 2px solid #7c3aed;
|
|
23
|
+
border-radius: 12px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header {
|
|
28
|
+
background-color: #7c3aed;
|
|
29
|
+
padding: 30px 20px;
|
|
30
|
+
text-align: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.header img {
|
|
34
|
+
max-width: 180px;
|
|
35
|
+
height: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.content {
|
|
39
|
+
padding: 30px;
|
|
40
|
+
color: #2c2721;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.button {
|
|
44
|
+
display: inline-block;
|
|
45
|
+
padding: 12px 24px;
|
|
46
|
+
background-color: #7c3aed;
|
|
47
|
+
color: #ffffff !important;
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
font-weight: bold;
|
|
51
|
+
margin-top: 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.button:hover {
|
|
55
|
+
background-color: #6d28d9;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.footer {
|
|
59
|
+
background-color: #ebe7db;
|
|
60
|
+
padding: 20px;
|
|
61
|
+
text-align: center;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
color: #5c5545;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.warning-box {
|
|
67
|
+
background-color: #fff3cd;
|
|
68
|
+
border-left: 4px solid #ffc107;
|
|
69
|
+
padding: 16px;
|
|
70
|
+
margin: 20px 0;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.details-table {
|
|
75
|
+
width: 100%;
|
|
76
|
+
border: 1px solid #e5e7eb;
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
border-collapse: collapse;
|
|
79
|
+
margin: 20px 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.details-table td {
|
|
83
|
+
padding: 10px 12px;
|
|
84
|
+
border-bottom: 1px solid #f3f4f6;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.details-label {
|
|
89
|
+
color: #5c5545;
|
|
90
|
+
width: 160px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.details-value {
|
|
94
|
+
color: #0e0d0c;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
|
|
99
|
+
<body>
|
|
100
|
+
<div class="container">
|
|
101
|
+
<div class="header">
|
|
102
|
+
<img src="https://assets.heliyos.ai/heliyos-logo-white.png" alt="Heliyos AI">
|
|
103
|
+
</div>
|
|
104
|
+
<div class="content">
|
|
105
|
+
<h2 style="margin-top: 0; color: #0e0d0c;">Action Required: Integration Not Synched</h2>
|
|
106
|
+
<p>Hi {{first_name}},</p>
|
|
107
|
+
<p>We detected that your integration <strong>{{integration_name}}</strong> is currently not synched.</p>
|
|
108
|
+
<p>{{summary}}</p>
|
|
109
|
+
|
|
110
|
+
<div class="warning-box">
|
|
111
|
+
<p style="margin: 0;"><strong>Detected at:</strong> {{timestamp}}</p>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{{#if details}}
|
|
115
|
+
<table class="details-table">
|
|
116
|
+
{{#each details}}
|
|
117
|
+
<tr>
|
|
118
|
+
<td class="details-label">{{label}}</td>
|
|
119
|
+
<td class="details-value">{{value}}</td>
|
|
120
|
+
</tr>
|
|
121
|
+
{{/each}}
|
|
122
|
+
</table>
|
|
123
|
+
{{/if}}
|
|
124
|
+
|
|
125
|
+
<p style="text-align: center;">
|
|
126
|
+
<a href="{{reconnect_url}}" class="button">Reconnect Integration</a>
|
|
127
|
+
</p>
|
|
128
|
+
<p>If you're having trouble clicking the button, copy and paste the following URL into your web browser:</p>
|
|
129
|
+
<p style="word-break: break-all; font-size: 13px; color: #5c5545;">{{reconnect_url}}</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="footer">
|
|
132
|
+
<p>© {{year}} {{company_name}}. All rights reserved.</p>
|
|
133
|
+
<p>This is an automated message, please do not reply to this email.</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</body>
|
|
137
|
+
|
|
138
|
+
</html>
|
package/dist/middleware.js
CHANGED
|
@@ -19,6 +19,7 @@ const serve_static_1 = __importDefault(require("serve-static"));
|
|
|
19
19
|
const authentication_1 = require("./authentication");
|
|
20
20
|
const allowedOrigin_1 = require("./allowedOrigin");
|
|
21
21
|
const customError_1 = require("./@types/globals/customError");
|
|
22
|
+
const logger_1 = require("./logger");
|
|
22
23
|
const genericErrorMessage = "Something went wrong";
|
|
23
24
|
const defaultErrorStatusCode = 500;
|
|
24
25
|
const nonProductionEnvironments = new Set(["development", "local", "test"]);
|
|
@@ -189,19 +190,12 @@ const handle_errors = (error, _, res, __) => {
|
|
|
189
190
|
description: sanitizeErrorDescription(description),
|
|
190
191
|
};
|
|
191
192
|
// Log original error
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"\n" +
|
|
199
|
-
"Desc: " +
|
|
200
|
-
description +
|
|
201
|
-
"\n" +
|
|
202
|
-
"Stack: " +
|
|
203
|
-
stack +
|
|
204
|
-
"\n---\n=== End Error ===");
|
|
193
|
+
logger_1.logger.error("Unhandled middleware error", {
|
|
194
|
+
message,
|
|
195
|
+
status,
|
|
196
|
+
description,
|
|
197
|
+
stack,
|
|
198
|
+
});
|
|
205
199
|
// Provide stack track in env development and local
|
|
206
200
|
if (!isProductionEnvironment()) {
|
|
207
201
|
response.error = Object.assign({ stack }, error);
|
|
@@ -773,12 +773,19 @@ export const authPolicy: IAuthPolicy = {
|
|
|
773
773
|
},
|
|
774
774
|
};
|
|
775
775
|
|
|
776
|
+
const memberRolePermissions = Array.from(
|
|
777
|
+
new Set(authPolicy.ROLES_PERMISSIONS.TEAM_MEMBER)
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
authPolicy.ROLES_PERMISSIONS.MEMBER = memberRolePermissions;
|
|
781
|
+
|
|
776
782
|
interface IAuthPolicy {
|
|
777
783
|
RESOURCES_ACTIONS: ResourcePolicyActionsType;
|
|
778
784
|
ROLES_PERMISSIONS: RolesPermissionsType;
|
|
779
785
|
}
|
|
780
786
|
|
|
781
787
|
export type RolesPermissionsType = {
|
|
788
|
+
MEMBER?: string[];
|
|
782
789
|
TEAM_MEMBER: string[];
|
|
783
790
|
ADMIN: string[];
|
|
784
791
|
OWNER: string[];
|
package/package.json
CHANGED
package/src/email/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const emailTemplates = {
|
|
|
13
13
|
notificationImmediateGrouped: "notification-immediate-grouped.html",
|
|
14
14
|
notificationDailySummary: "notification-daily-summary.html",
|
|
15
15
|
organizationDailySummary: "organization-daily-summary.html",
|
|
16
|
+
integrationNotSynced: "integration-not-synced.html",
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Integration Not Synched</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: Arial, sans-serif;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
color: #0e0d0c;
|
|
13
|
+
background-color: #f8f8f6;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.container {
|
|
19
|
+
max-width: 600px;
|
|
20
|
+
margin: 20px auto;
|
|
21
|
+
background-color: #ffffff;
|
|
22
|
+
border: 2px solid #7c3aed;
|
|
23
|
+
border-radius: 12px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header {
|
|
28
|
+
background-color: #7c3aed;
|
|
29
|
+
padding: 30px 20px;
|
|
30
|
+
text-align: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.header img {
|
|
34
|
+
max-width: 180px;
|
|
35
|
+
height: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.content {
|
|
39
|
+
padding: 30px;
|
|
40
|
+
color: #2c2721;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.button {
|
|
44
|
+
display: inline-block;
|
|
45
|
+
padding: 12px 24px;
|
|
46
|
+
background-color: #7c3aed;
|
|
47
|
+
color: #ffffff !important;
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
font-weight: bold;
|
|
51
|
+
margin-top: 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.button:hover {
|
|
55
|
+
background-color: #6d28d9;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.footer {
|
|
59
|
+
background-color: #ebe7db;
|
|
60
|
+
padding: 20px;
|
|
61
|
+
text-align: center;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
color: #5c5545;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.warning-box {
|
|
67
|
+
background-color: #fff3cd;
|
|
68
|
+
border-left: 4px solid #ffc107;
|
|
69
|
+
padding: 16px;
|
|
70
|
+
margin: 20px 0;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.details-table {
|
|
75
|
+
width: 100%;
|
|
76
|
+
border: 1px solid #e5e7eb;
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
border-collapse: collapse;
|
|
79
|
+
margin: 20px 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.details-table td {
|
|
83
|
+
padding: 10px 12px;
|
|
84
|
+
border-bottom: 1px solid #f3f4f6;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.details-label {
|
|
89
|
+
color: #5c5545;
|
|
90
|
+
width: 160px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.details-value {
|
|
94
|
+
color: #0e0d0c;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
|
|
99
|
+
<body>
|
|
100
|
+
<div class="container">
|
|
101
|
+
<div class="header">
|
|
102
|
+
<img src="https://assets.heliyos.ai/heliyos-logo-white.png" alt="Heliyos AI">
|
|
103
|
+
</div>
|
|
104
|
+
<div class="content">
|
|
105
|
+
<h2 style="margin-top: 0; color: #0e0d0c;">Action Required: Integration Not Synched</h2>
|
|
106
|
+
<p>Hi {{first_name}},</p>
|
|
107
|
+
<p>We detected that your integration <strong>{{integration_name}}</strong> is currently not synched.</p>
|
|
108
|
+
<p>{{summary}}</p>
|
|
109
|
+
|
|
110
|
+
<div class="warning-box">
|
|
111
|
+
<p style="margin: 0;"><strong>Detected at:</strong> {{timestamp}}</p>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{{#if details}}
|
|
115
|
+
<table class="details-table">
|
|
116
|
+
{{#each details}}
|
|
117
|
+
<tr>
|
|
118
|
+
<td class="details-label">{{label}}</td>
|
|
119
|
+
<td class="details-value">{{value}}</td>
|
|
120
|
+
</tr>
|
|
121
|
+
{{/each}}
|
|
122
|
+
</table>
|
|
123
|
+
{{/if}}
|
|
124
|
+
|
|
125
|
+
<p style="text-align: center;">
|
|
126
|
+
<a href="{{reconnect_url}}" class="button">Reconnect Integration</a>
|
|
127
|
+
</p>
|
|
128
|
+
<p>If you're having trouble clicking the button, copy and paste the following URL into your web browser:</p>
|
|
129
|
+
<p style="word-break: break-all; font-size: 13px; color: #5c5545;">{{reconnect_url}}</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="footer">
|
|
132
|
+
<p>© {{year}} {{company_name}}. All rights reserved.</p>
|
|
133
|
+
<p>This is an automated message, please do not reply to this email.</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</body>
|
|
137
|
+
|
|
138
|
+
</html>
|