@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.
@@ -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;
@@ -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.cookies.user_session,
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
- const token_detail_string = Buffer.from(token_parts[0], "base64");
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
- console.log(error);
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(`/v2/auth/api_key/verify`, {
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
- console.log(err);
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";
@@ -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) => Promise<{
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
  }>;
@@ -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(`v1/auth/user/${userId}`, { resourceAction, organizationId });
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
- if ((err === null || err === void 0 ? void 0 : err.status) === 401) {
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 ((err === null || err === void 0 ? void 0 : err.status) === 501) {
38
+ if (status === "403") {
34
39
  throw err;
35
40
  }
36
41
  const error = new customError_1.HttpError("Something went wrong with Authentication");
@@ -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;
@@ -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>&copy; {{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>
@@ -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
- console.error("=== Begin Error ===\n---\n" +
193
- "Error: " +
194
- message +
195
- "\n" +
196
- "Status: " +
197
- status +
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);
@@ -4,6 +4,7 @@ interface IAuthPolicy {
4
4
  ROLES_PERMISSIONS: RolesPermissionsType;
5
5
  }
6
6
  export type RolesPermissionsType = {
7
+ MEMBER?: string[];
7
8
  TEAM_MEMBER: string[];
8
9
  ADMIN: string[];
9
10
  OWNER: string[];
@@ -700,3 +700,5 @@ exports.authPolicy = {
700
700
  ],
701
701
  },
702
702
  };
703
+ const memberRolePermissions = Array.from(new Set(exports.authPolicy.ROLES_PERMISSIONS.TEAM_MEMBER));
704
+ exports.authPolicy.ROLES_PERMISSIONS.MEMBER = memberRolePermissions;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heliyos/heliyos-api-core",
3
- "version": "1.0.69",
3
+ "version": "1.0.71",
4
4
  "description": "Heliyos's core api functions and middlewares. Its a private package hosted on npm.",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -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>&copy; {{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>