@oneuptime/common 9.1.0 → 9.1.2

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.
Files changed (75) hide show
  1. package/Models/DatabaseModels/BillingPaymentMethod.ts +45 -9
  2. package/Models/DatabaseModels/Incident.ts +37 -0
  3. package/Models/DatabaseModels/StatusPageDomain.ts +3 -2
  4. package/Models/DatabaseModels/User.ts +1 -1
  5. package/Server/API/BillingPaymentMethodAPI.ts +6 -7
  6. package/Server/API/StatusPageAPI.ts +6 -1
  7. package/Server/EnvironmentConfig.ts +11 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1764324618043-MigrationName.ts +30 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  10. package/Server/Services/IncidentService.ts +38 -0
  11. package/Server/Services/MonitorService.ts +49 -31
  12. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +18 -0
  13. package/Server/Services/StatusPageDomainService.ts +17 -10
  14. package/Server/Services/TeamMemberService.ts +8 -7
  15. package/Server/Services/TeamService.ts +2 -1
  16. package/Server/Types/Workflow/Components/Email.ts +10 -1
  17. package/Server/Utils/Browser.ts +28 -2
  18. package/Server/Utils/Captcha.ts +98 -0
  19. package/Server/Utils/Greenlock/Greenlock.ts +10 -3
  20. package/Server/Utils/Telemetry.ts +10 -2
  21. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +4 -1
  22. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +23 -10
  23. package/Tests/Server/Services/TeamMemberService.test.ts +119 -0
  24. package/UI/Components/Captcha/Captcha.tsx +75 -0
  25. package/UI/Config.ts +3 -0
  26. package/build/dist/Models/DatabaseModels/BillingPaymentMethod.js +45 -9
  27. package/build/dist/Models/DatabaseModels/BillingPaymentMethod.js.map +1 -1
  28. package/build/dist/Models/DatabaseModels/Incident.js +39 -0
  29. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  30. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +2 -2
  31. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  32. package/build/dist/Models/DatabaseModels/User.js +1 -1
  33. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  34. package/build/dist/Server/API/BillingPaymentMethodAPI.js +6 -5
  35. package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
  36. package/build/dist/Server/API/StatusPageAPI.js +6 -1
  37. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  38. package/build/dist/Server/EnvironmentConfig.js +6 -0
  39. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  40. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1764324618043-MigrationName.js +17 -0
  41. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1764324618043-MigrationName.js.map +1 -0
  42. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  43. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  44. package/build/dist/Server/Services/IncidentService.js +32 -1
  45. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  46. package/build/dist/Server/Services/MonitorService.js +44 -24
  47. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  48. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +12 -0
  49. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  50. package/build/dist/Server/Services/StatusPageDomainService.js +12 -9
  51. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  52. package/build/dist/Server/Services/TeamMemberService.js +9 -8
  53. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  54. package/build/dist/Server/Services/TeamService.js +2 -1
  55. package/build/dist/Server/Services/TeamService.js.map +1 -1
  56. package/build/dist/Server/Types/Workflow/Components/Email.js +8 -1
  57. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  58. package/build/dist/Server/Utils/Browser.js +24 -2
  59. package/build/dist/Server/Utils/Browser.js.map +1 -1
  60. package/build/dist/Server/Utils/Captcha.js +58 -0
  61. package/build/dist/Server/Utils/Captcha.js.map +1 -0
  62. package/build/dist/Server/Utils/Greenlock/Greenlock.js +7 -2
  63. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  64. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  65. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +3 -1
  66. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  67. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +19 -11
  68. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  69. package/build/dist/Tests/Server/Services/TeamMemberService.test.js +79 -0
  70. package/build/dist/Tests/Server/Services/TeamMemberService.test.js.map +1 -1
  71. package/build/dist/UI/Components/Captcha/Captcha.js +37 -0
  72. package/build/dist/UI/Components/Captcha/Captcha.js.map +1 -0
  73. package/build/dist/UI/Config.js +2 -0
  74. package/build/dist/UI/Config.js.map +1 -1
  75. package/package.json +2 -1
@@ -44,10 +44,11 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
44
44
  }
45
45
 
46
46
  @CaptureSpan()
47
- private async isSCIMEnabled(projectId: ObjectID): Promise<boolean> {
47
+ private async isSCIMPushGroupsEnabled(projectId: ObjectID): Promise<boolean> {
48
48
  const count: PositiveNumber = await ProjectSCIMService.countBy({
49
49
  query: {
50
50
  projectId: projectId,
51
+ enablePushGroups: true,
51
52
  },
52
53
  props: {
53
54
  isRoot: true,
@@ -63,12 +64,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
63
64
  // Check if SCIM is enabled for the project
64
65
  if (
65
66
  !createBy.props.isRoot &&
66
- (await this.isSCIMEnabled(
67
+ (await this.isSCIMPushGroupsEnabled(
67
68
  createBy.data.projectId! || createBy.props.tenantId,
68
69
  ))
69
70
  ) {
70
71
  throw new BadDataException(
71
- "Cannot invite team members when SCIM is enabled for this project.",
72
+ "Cannot invite team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
72
73
  );
73
74
  }
74
75
 
@@ -311,10 +312,10 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
311
312
  !deleteBy.props.isRoot &&
312
313
  members.length > 0 &&
313
314
  members[0]?.projectId &&
314
- (await this.isSCIMEnabled(members[0].projectId))
315
+ (await this.isSCIMPushGroupsEnabled(members[0].projectId))
315
316
  ) {
316
317
  throw new BadDataException(
317
- "Cannot delete team members when SCIM is enabled for this project.",
318
+ "Cannot delete team members while SCIM Push Groups is enabled for this project. Disable Push Groups to manage members from OneUptime.",
318
319
  );
319
320
  }
320
321
 
@@ -346,11 +347,11 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
346
347
  });
347
348
 
348
349
  // Skip the one-member guard when SCIM manages membership for the project.
349
- const isSCIMEnabled: boolean = await this.isSCIMEnabled(
350
+ const isPushGroupsManaged: boolean = await this.isSCIMPushGroupsEnabled(
350
351
  member.projectId!,
351
352
  );
352
353
 
353
- if (!isSCIMEnabled && membersInTeam.toNumber() <= 1) {
354
+ if (!isPushGroupsManaged && membersInTeam.toNumber() <= 1) {
354
355
  throw new BadDataException(
355
356
  Errors.TeamMemberService.ONE_MEMBER_REQUIRED,
356
357
  );
@@ -71,6 +71,7 @@ export class Service extends DatabaseService<Model> {
71
71
  const scimCount: PositiveNumber = await ProjectSCIMService.countBy({
72
72
  query: {
73
73
  projectId: projectId,
74
+ enablePushGroups: true,
74
75
  },
75
76
  skip: new PositiveNumber(0),
76
77
  limit: new PositiveNumber(1),
@@ -82,7 +83,7 @@ export class Service extends DatabaseService<Model> {
82
83
 
83
84
  if (scimCount.toNumber() > 0) {
84
85
  throw new BadDataException(
85
- `Cannot ${data.action} teams when SCIM is enabled for this project.`,
86
+ `Cannot ${data.action} teams while SCIM Push Groups is enabled for this project. Disable Push Groups to manage teams from OneUptime.`,
86
87
  );
87
88
  }
88
89
  }
@@ -115,9 +115,18 @@ export default class Email extends ComponentCode {
115
115
  const smtpTransport: SMTPTransport.Options = {
116
116
  host: args["smtp-host"]?.toString(),
117
117
  port: args["smtp-port"] as number,
118
- secure: Boolean(args["secure"]),
119
118
  };
120
119
 
120
+ if (
121
+ args["secure"] === true ||
122
+ args["secure"] === "true" ||
123
+ args["secure"] === 1
124
+ ) {
125
+ smtpTransport.secure = true;
126
+ } else {
127
+ smtpTransport.secure = false;
128
+ }
129
+
121
130
  if (username && password) {
122
131
  smtpTransport.auth = {
123
132
  user: username,
@@ -166,7 +166,20 @@ export default class BrowserUtil {
166
166
  throw new BadDataException("Chrome executable path not found.");
167
167
  }
168
168
 
169
- return `/root/.cache/ms-playwright/${chromeInstallationName}/chrome-linux/chrome`;
169
+ const chromeExecutableCandidates: Array<string> = [
170
+ `/root/.cache/ms-playwright/${chromeInstallationName}/chrome-linux/chrome`,
171
+ `/root/.cache/ms-playwright/${chromeInstallationName}/chrome-linux64/chrome`,
172
+ `/root/.cache/ms-playwright/${chromeInstallationName}/chrome64/chrome`,
173
+ `/root/.cache/ms-playwright/${chromeInstallationName}/chrome/chrome`,
174
+ ];
175
+
176
+ for (const executablePath of chromeExecutableCandidates) {
177
+ if (await LocalFile.doesFileExist(executablePath)) {
178
+ return executablePath;
179
+ }
180
+ }
181
+
182
+ throw new BadDataException("Chrome executable path not found.");
170
183
  }
171
184
 
172
185
  @CaptureSpan()
@@ -197,6 +210,19 @@ export default class BrowserUtil {
197
210
  throw new BadDataException("Firefox executable path not found.");
198
211
  }
199
212
 
200
- return `/root/.cache/ms-playwright/${firefoxInstallationName}/firefox/firefox`;
213
+ const firefoxExecutableCandidates: Array<string> = [
214
+ `/root/.cache/ms-playwright/${firefoxInstallationName}/firefox/firefox`,
215
+ `/root/.cache/ms-playwright/${firefoxInstallationName}/firefox-linux64/firefox`,
216
+ `/root/.cache/ms-playwright/${firefoxInstallationName}/firefox64/firefox`,
217
+ `/root/.cache/ms-playwright/${firefoxInstallationName}/firefox-64/firefox`,
218
+ ];
219
+
220
+ for (const executablePath of firefoxExecutableCandidates) {
221
+ if (await LocalFile.doesFileExist(executablePath)) {
222
+ return executablePath;
223
+ }
224
+ }
225
+
226
+ throw new BadDataException("Firefox executable path not found.");
201
227
  }
202
228
  }
@@ -0,0 +1,98 @@
1
+ import axios, { AxiosError, AxiosResponse } from "axios";
2
+ import BadDataException from "../../Types/Exception/BadDataException";
3
+ import logger from "./Logger";
4
+ import { CaptchaEnabled, CaptchaSecretKey } from "../EnvironmentConfig";
5
+
6
+ export interface VerifyCaptchaOptions {
7
+ token: string | null | undefined;
8
+ remoteIp?: string | null;
9
+ }
10
+
11
+ const REQUEST_TIMEOUT_MS: number = 5000;
12
+ const GENERIC_ERROR_MESSAGE: string =
13
+ "Captcha verification failed. Please try again.";
14
+
15
+ type HCaptchaResponse = {
16
+ success?: boolean;
17
+ [key: string]: unknown;
18
+ };
19
+
20
+ class CaptchaUtil {
21
+ public static isCaptchaEnabled(): boolean {
22
+ return CaptchaEnabled && Boolean(CaptchaSecretKey);
23
+ }
24
+
25
+ public static async verifyCaptcha(
26
+ options: VerifyCaptchaOptions,
27
+ ): Promise<void> {
28
+ if (!CaptchaEnabled) {
29
+ return;
30
+ }
31
+
32
+ if (!CaptchaSecretKey) {
33
+ logger.error(
34
+ "Captcha is enabled but CAPTCHA_SECRET_KEY is not configured.",
35
+ );
36
+ throw new BadDataException(GENERIC_ERROR_MESSAGE);
37
+ }
38
+
39
+ const token: string = (options.token || "").trim();
40
+
41
+ if (!token) {
42
+ throw new BadDataException(
43
+ "Captcha token is missing. Please complete the verification challenge.",
44
+ );
45
+ }
46
+
47
+ try {
48
+ await this.verifyHCaptcha(token, options.remoteIp || undefined);
49
+ } catch (err) {
50
+ if (axios.isAxiosError(err)) {
51
+ const axiosError: AxiosError = err as AxiosError;
52
+ logger.error(
53
+ `Captcha provider verification failure: ${axiosError.message}`,
54
+ );
55
+ } else {
56
+ logger.error(
57
+ `Captcha provider verification failure: ${(err as Error).message}`,
58
+ );
59
+ }
60
+
61
+ throw new BadDataException(GENERIC_ERROR_MESSAGE);
62
+ }
63
+ }
64
+
65
+ private static async verifyHCaptcha(
66
+ token: string,
67
+ remoteIp?: string,
68
+ ): Promise<void> {
69
+ const params: URLSearchParams = new URLSearchParams();
70
+ params.append("secret", CaptchaSecretKey);
71
+ params.append("response", token);
72
+
73
+ if (remoteIp) {
74
+ params.append("remoteip", remoteIp);
75
+ }
76
+
77
+ const response: AxiosResponse<HCaptchaResponse> =
78
+ await axios.post<HCaptchaResponse>(
79
+ "https://hcaptcha.com/siteverify",
80
+ params.toString(),
81
+ {
82
+ headers: {
83
+ "content-type": "application/x-www-form-urlencoded",
84
+ },
85
+ timeout: REQUEST_TIMEOUT_MS,
86
+ },
87
+ );
88
+
89
+ if (!response.data?.success) {
90
+ logger.warn(
91
+ `hCaptcha verification failed: ${JSON.stringify(response.data || {})}`,
92
+ );
93
+ throw new BadDataException(GENERIC_ERROR_MESSAGE);
94
+ }
95
+ }
96
+ }
97
+
98
+ export default CaptchaUtil;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ IsBillingEnabled,
2
3
  LetsEncryptAccountKey,
3
4
  LetsEncryptNotificationEmail,
4
5
  } from "../../../Server/EnvironmentConfig";
@@ -325,9 +326,15 @@ export default class GreenlockUtil {
325
326
  throw e;
326
327
  }
327
328
 
328
- throw new ServerException(
329
- `Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
330
- );
329
+ if (IsBillingEnabled) {
330
+ throw new ServerException(
331
+ `Unable to order certificate for ${data.domain}. Please contact support at support@oneuptime.com for more information.`,
332
+ );
333
+ } else {
334
+ throw new ServerException(
335
+ `Unable to order certificate for ${data.domain}. Please make sure that your server can be accessed publicly over port 80 (HTTP) and port 443 (HTTPS). If the problem persists, please refer to server logs for more information. Please also set up LOG_LEVEL=DEBUG to get more detailed server logs.`,
336
+ );
337
+ }
331
338
  }
332
339
  }
333
340
  }
@@ -226,7 +226,11 @@ export default class Telemetry {
226
226
  };
227
227
 
228
228
  if (logRecordProcessors.length > 0) {
229
- loggerProviderConfig.processors = logRecordProcessors;
229
+ (
230
+ loggerProviderConfig as LoggerProviderConfig & {
231
+ processors?: Array<LogRecordProcessor>;
232
+ }
233
+ ).processors = logRecordProcessors;
230
234
  }
231
235
 
232
236
  this.loggerProvider = new LoggerProvider(loggerProviderConfig);
@@ -254,7 +258,11 @@ export default class Telemetry {
254
258
  */
255
259
 
256
260
  if (logRecordProcessors.length > 0) {
257
- nodeSdkConfiguration.logRecordProcessors = logRecordProcessors;
261
+ (
262
+ nodeSdkConfiguration as opentelemetry.NodeSDKConfiguration & {
263
+ logRecordProcessors?: Array<LogRecordProcessor>;
264
+ }
265
+ ).logRecordProcessors = logRecordProcessors;
258
266
  }
259
267
 
260
268
  const sdk: opentelemetry.NodeSDK = new opentelemetry.NodeSDK(
@@ -320,6 +320,7 @@ export default class MicrosoftTeamsIncidentActions {
320
320
  name: true,
321
321
  },
322
322
  createdAt: true,
323
+ declaredAt: true,
323
324
  },
324
325
  props: {
325
326
  isRoot: true,
@@ -331,7 +332,9 @@ export default class MicrosoftTeamsIncidentActions {
331
332
  return;
332
333
  }
333
334
 
334
- const message: string = `**Incident Details**\n\n**Title:** ${incident.title}\n**Description:** ${incident.description || "No description"}\n**State:** ${incident.currentIncidentState?.name || "Unknown"}\n**Severity:** ${incident.incidentSeverity?.name || "Unknown"}\n**Created At:** ${incident.createdAt ? new Date(incident.createdAt).toLocaleString() : "Unknown"}`;
335
+ const declaredAt: Date | undefined =
336
+ incident.declaredAt || incident.createdAt || undefined;
337
+ const message: string = `**Incident Details**\n\n**Title:** ${incident.title}\n**Description:** ${incident.description || "No description"}\n**State:** ${incident.currentIncidentState?.name || "Unknown"}\n**Severity:** ${incident.incidentSeverity?.name || "Unknown"}\n**Declared At:** ${declaredAt ? new Date(declaredAt).toLocaleString() : "Unknown"}`;
335
338
 
336
339
  await turnContext.sendActivity(message);
337
340
  return;
@@ -43,6 +43,7 @@ import OneUptimeDate from "../../../../Types/Date";
43
43
  import {
44
44
  MicrosoftTeamsAppClientId,
45
45
  MicrosoftTeamsAppClientSecret,
46
+ MicrosoftTeamsAppTenantId,
46
47
  } from "../../../EnvironmentConfig";
47
48
 
48
49
  // Import services for bot commands
@@ -91,18 +92,25 @@ const MICROSOFT_TEAMS_APP_TYPE: string = "SingleTenant";
91
92
  const MICROSOFT_TEAMS_MAX_PAGES: number = 500;
92
93
 
93
94
  export default class MicrosoftTeamsUtil extends WorkspaceBase {
95
+ private static cachedAdapter: CloudAdapter | null = null;
94
96
  private static readonly WELCOME_CARD_STATE_KEY: string =
95
97
  "oneuptime.microsoftTeams.welcomeCardSent";
96
98
  // Get or create Bot Framework adapter for a specific tenant
97
- private static getBotAdapter(microsoftAppTenantId: string): CloudAdapter {
99
+ private static getBotAdapter(): CloudAdapter {
100
+ if (this.cachedAdapter) {
101
+ return this.cachedAdapter;
102
+ }
103
+
98
104
  if (!MicrosoftTeamsAppClientId || !MicrosoftTeamsAppClientSecret) {
99
105
  throw new BadDataException(
100
106
  "Microsoft Teams App credentials not configured",
101
107
  );
102
108
  }
103
109
 
104
- if (!microsoftAppTenantId) {
105
- throw new BadDataException("Microsoft Teams tenant ID is required");
110
+ if (!MicrosoftTeamsAppTenantId) {
111
+ throw new BadDataException(
112
+ "Microsoft Teams app tenant ID is not configured",
113
+ );
106
114
  }
107
115
 
108
116
  logger.debug(
@@ -110,18 +118,19 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
110
118
  );
111
119
  logger.debug(`App ID: ${MicrosoftTeamsAppClientId}`);
112
120
  logger.debug(`App Type: ${MICROSOFT_TEAMS_APP_TYPE}`);
113
- logger.debug(`Tenant ID: ${microsoftAppTenantId}`);
121
+ logger.debug(`Tenant ID: ${MicrosoftTeamsAppTenantId}`);
114
122
 
115
123
  const authConfig: ConfigurationBotFrameworkAuthenticationOptions = {
116
124
  MicrosoftAppId: MicrosoftTeamsAppClientId,
117
125
  MicrosoftAppPassword: MicrosoftTeamsAppClientSecret,
118
126
  MicrosoftAppType: MICROSOFT_TEAMS_APP_TYPE,
119
- MicrosoftAppTenantId: microsoftAppTenantId,
127
+ MicrosoftAppTenantId: MicrosoftTeamsAppTenantId,
120
128
  };
121
129
 
122
130
  const botFrameworkAuthentication: ConfigurationBotFrameworkAuthentication =
123
131
  new ConfigurationBotFrameworkAuthentication(authConfig);
124
132
  const adapter: CloudAdapter = new CloudAdapter(botFrameworkAuthentication);
133
+ this.cachedAdapter = adapter;
125
134
 
126
135
  logger.debug("Bot Framework adapter created successfully");
127
136
  return adapter;
@@ -1141,7 +1150,7 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1141
1150
  logger.debug(`Using bot ID: ${miscData.botId}`);
1142
1151
 
1143
1152
  // Get Bot Framework adapter
1144
- const adapter: CloudAdapter = this.getBotAdapter(tenantId);
1153
+ const adapter: CloudAdapter = this.getBotAdapter();
1145
1154
 
1146
1155
  // Create conversation reference for the channel
1147
1156
  const conversationReference: ConversationReference = {
@@ -1920,11 +1929,13 @@ Just type any of these commands to get the information you need!`;
1920
1929
  color: true,
1921
1930
  },
1922
1931
  createdAt: true,
1932
+ declaredAt: true,
1923
1933
  monitors: {
1924
1934
  name: true,
1925
1935
  },
1926
1936
  },
1927
1937
  sort: {
1938
+ declaredAt: SortOrder.Descending,
1928
1939
  createdAt: SortOrder.Descending,
1929
1940
  },
1930
1941
  limit: 10,
@@ -1949,8 +1960,10 @@ If you need to report an incident or check historical incidents, please visit th
1949
1960
  for (const incident of activeIncidents) {
1950
1961
  const severity: string = incident.incidentSeverity?.name || "Unknown";
1951
1962
  const state: string = incident.currentIncidentState?.name || "Unknown";
1952
- const createdAt: string = incident.createdAt
1953
- ? OneUptimeDate.getDateAsFormattedString(incident.createdAt)
1963
+ const declaredAt: Date | undefined =
1964
+ incident.declaredAt || incident.createdAt;
1965
+ const declaredAtText: string = declaredAt
1966
+ ? OneUptimeDate.getDateAsFormattedString(declaredAt)
1954
1967
  : "Unknown";
1955
1968
 
1956
1969
  const severityIcon: string = ["Critical", "Major"].includes(severity)
@@ -1968,7 +1981,7 @@ If you need to report an incident or check historical incidents, please visit th
1968
1981
  message += `${severityIcon} **[Incident #${incident.incidentNumber}: ${incident.title}](${incidentUrl.toString()})**
1969
1982
  • **Severity:** ${severity}
1970
1983
  • **Status:** ${state}
1971
- • **Created:** ${createdAt}
1984
+ • **Declared:** ${declaredAtText}
1972
1985
  `;
1973
1986
 
1974
1987
  if (incident.monitors && incident.monitors.length > 0) {
@@ -2564,7 +2577,7 @@ All monitoring checks are passing normally.`;
2564
2577
  }
2565
2578
 
2566
2579
  // Get Bot Framework adapter
2567
- const adapter: CloudAdapter = this.getBotAdapter(tenantId);
2580
+ const adapter: CloudAdapter = this.getBotAdapter();
2568
2581
 
2569
2582
  // Create custom activity handler class that extends TeamsActivityHandler
2570
2583
  class OneUptimeTeamsActivityHandler extends TeamsActivityHandler {
@@ -5,6 +5,8 @@ import MailService from "../../../Server/Services/MailService";
5
5
  import TeamMemberService from "../../../Server/Services/TeamMemberService";
6
6
  import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
7
7
  import UserNotificationSettingService from "../../../Server/Services/UserNotificationSettingService";
8
+ import ProjectSCIMService from "../../../Server/Services/ProjectSCIMService";
9
+ import ProjectSCIM from "../../../Models/DatabaseModels/ProjectSCIM";
8
10
  import Errors from "../../../Server/Utils/Errors";
9
11
  import "../TestingUtils/Init";
10
12
  import ProjectServiceHelper from "../TestingUtils/Services/ProjectServiceHelper";
@@ -334,6 +336,123 @@ describe("TeamMemberService", () => {
334
336
  },
335
337
  );
336
338
  });
339
+
340
+ it("should block inviting users when SCIM push groups is enabled", async () => {
341
+ const owner: User = await UserServiceHelper.genrateAndSaveRandomUser(
342
+ null,
343
+ {
344
+ isRoot: true,
345
+ },
346
+ );
347
+
348
+ const project: Project =
349
+ await ProjectServiceHelper.generateAndSaveRandomProject(null, {
350
+ isRoot: true,
351
+ userId: owner.id!,
352
+ });
353
+
354
+ const team: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
355
+ {
356
+ projectId: new ObjectID(project.id!),
357
+ },
358
+ {
359
+ isRoot: true,
360
+ },
361
+ );
362
+
363
+ const memberUser: User =
364
+ await UserServiceHelper.genrateAndSaveRandomUser(null, {
365
+ isRoot: true,
366
+ });
367
+
368
+ const scimWithPushGroups: ProjectSCIM = new ProjectSCIM();
369
+ scimWithPushGroups.projectId = new ObjectID(project._id!);
370
+ scimWithPushGroups.name = "Test SCIM Push Groups";
371
+ scimWithPushGroups.bearerToken = ObjectID.generate().toString();
372
+ scimWithPushGroups.enablePushGroups = true;
373
+
374
+ await ProjectSCIMService.create({
375
+ data: scimWithPushGroups,
376
+ props: {
377
+ isRoot: true,
378
+ },
379
+ });
380
+
381
+ const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
382
+ {
383
+ projectId: new ObjectID(project._id!),
384
+ userId: new ObjectID(memberUser._id!),
385
+ teamId: new ObjectID(team._id!),
386
+ },
387
+ );
388
+
389
+ await expect(
390
+ TeamMemberService.create({
391
+ data: tm,
392
+ props: { isRoot: false, tenantId: project.id! },
393
+ }),
394
+ ).rejects.toThrow(/SCIM Push Groups/i);
395
+ });
396
+
397
+ it("should allow inviting users when SCIM push groups is disabled", async () => {
398
+ const owner: User = await UserServiceHelper.genrateAndSaveRandomUser(
399
+ null,
400
+ {
401
+ isRoot: true,
402
+ },
403
+ );
404
+
405
+ const project: Project =
406
+ await ProjectServiceHelper.generateAndSaveRandomProject(null, {
407
+ isRoot: true,
408
+ userId: owner.id!,
409
+ });
410
+
411
+ const team: Team = await TeamServiceHelper.generateAndSaveRandomTeam(
412
+ {
413
+ projectId: new ObjectID(project.id!),
414
+ },
415
+ {
416
+ isRoot: true,
417
+ },
418
+ );
419
+
420
+ const memberUser: User =
421
+ await UserServiceHelper.genrateAndSaveRandomUser(null, {
422
+ isRoot: true,
423
+ });
424
+
425
+ const scimWithoutPushGroups: ProjectSCIM = new ProjectSCIM();
426
+ scimWithoutPushGroups.projectId = new ObjectID(project._id!);
427
+ scimWithoutPushGroups.name = "Test SCIM without Push Groups";
428
+ scimWithoutPushGroups.bearerToken = ObjectID.generate().toString();
429
+ scimWithoutPushGroups.enablePushGroups = false;
430
+
431
+ await ProjectSCIMService.create({
432
+ data: scimWithoutPushGroups,
433
+ props: {
434
+ isRoot: true,
435
+ },
436
+ });
437
+
438
+ const tm: TeamMember = TeamMemberServiceHelper.generateRandomTeamMember(
439
+ {
440
+ projectId: new ObjectID(project._id!),
441
+ userId: new ObjectID(memberUser._id!),
442
+ teamId: new ObjectID(team._id!),
443
+ },
444
+ );
445
+
446
+ const teamMember: TeamMember = await TeamMemberService.create({
447
+ data: tm,
448
+ props: { isRoot: false, tenantId: project.id! },
449
+ });
450
+
451
+ expect(teamMember).toBeDefined();
452
+ expect(teamMember.projectId?.toString()).toEqual(
453
+ project._id?.toString(),
454
+ );
455
+ });
337
456
  });
338
457
 
339
458
  describe("onCreateSuccess", () => {
@@ -0,0 +1,75 @@
1
+ import HCaptcha from "@hcaptcha/react-hcaptcha";
2
+ import React from "react";
3
+
4
+ export interface CaptchaProps {
5
+ siteKey: string;
6
+ resetSignal?: number | undefined;
7
+ error?: string | undefined;
8
+ onTokenChange?: (token: string) => void;
9
+ onBlur?: (() => void) | undefined;
10
+ className?: string | undefined;
11
+ }
12
+
13
+ const Captcha: React.FC<CaptchaProps> = ({
14
+ siteKey,
15
+ resetSignal = 0,
16
+ error,
17
+ onTokenChange,
18
+ onBlur,
19
+ className,
20
+ }: CaptchaProps): JSX.Element => {
21
+ const captchaRef: React.MutableRefObject<HCaptcha | null> =
22
+ React.useRef<HCaptcha | null>(null);
23
+ const onTokenChangeRef: React.MutableRefObject<
24
+ CaptchaProps["onTokenChange"]
25
+ > = React.useRef<CaptchaProps["onTokenChange"]>(onTokenChange);
26
+
27
+ React.useEffect(() => {
28
+ onTokenChangeRef.current = onTokenChange;
29
+ }, [onTokenChange]);
30
+
31
+ const handleTokenChange: (token: string | null) => void = React.useCallback(
32
+ (token: string | null) => {
33
+ onTokenChangeRef.current?.(token || "");
34
+ },
35
+ [],
36
+ );
37
+
38
+ React.useEffect(() => {
39
+ captchaRef.current?.resetCaptcha();
40
+ handleTokenChange("");
41
+ }, [resetSignal, handleTokenChange]);
42
+
43
+ if (!siteKey) {
44
+ return (
45
+ <div className={className || "text-center text-sm text-red-500"}>
46
+ Captcha is not configured.
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div className={className || "flex flex-col items-center gap-2"}>
53
+ <HCaptcha
54
+ sitekey={siteKey}
55
+ ref={captchaRef}
56
+ onVerify={(token: string) => {
57
+ handleTokenChange(token);
58
+ onBlur?.();
59
+ }}
60
+ onExpire={() => {
61
+ handleTokenChange(null);
62
+ captchaRef.current?.resetCaptcha();
63
+ onBlur?.();
64
+ }}
65
+ onError={() => {
66
+ handleTokenChange(null);
67
+ onBlur?.();
68
+ }}
69
+ />
70
+ {error && <span className="text-sm text-red-500">{error}</span>}
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export default Captcha;
package/UI/Config.ts CHANGED
@@ -51,6 +51,9 @@ export const IS_ENTERPRISE_EDITION: boolean =
51
51
  env("IS_ENTERPRISE_EDITION") === "true";
52
52
  export const BILLING_PUBLIC_KEY: string = env("BILLING_PUBLIC_KEY") || "";
53
53
 
54
+ export const CAPTCHA_ENABLED: boolean = env("CAPTCHA_ENABLED") === "true";
55
+ export const CAPTCHA_SITE_KEY: string = env("CAPTCHA_SITE_KEY") || "";
56
+
54
57
  // VAPID Configuration for Push Notifications
55
58
  export const VAPID_PUBLIC_KEY: string = env("VAPID_PUBLIC_KEY") || "";
56
59