@oneuptime/common 7.0.4349 → 7.0.4358

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 (34) hide show
  1. package/Models/DatabaseModels/StatusPage.ts +37 -0
  2. package/Models/DatabaseModels/StatusPageSubscriber.ts +60 -0
  3. package/Server/API/StatusPageAPI.ts +104 -10
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.ts +23 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.ts +17 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  7. package/Server/Services/ScheduledMaintenanceService.ts +21 -0
  8. package/Server/Services/StatusPageSubscriberService.ts +116 -1
  9. package/Server/Utils/OpenAPI.ts +176 -11
  10. package/Server/Utils/Workspace/Slack/Slack.ts +14 -0
  11. package/Utils/Schema/ModelSchema.ts +1303 -11
  12. package/build/dist/Models/DatabaseModels/StatusPage.js +39 -0
  13. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  14. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +62 -0
  15. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  16. package/build/dist/Server/API/StatusPageAPI.js +73 -10
  17. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  18. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.js +14 -0
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.js.map +1 -0
  20. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.js +12 -0
  21. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.js.map +1 -0
  22. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  23. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  24. package/build/dist/Server/Services/ScheduledMaintenanceService.js +19 -0
  25. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  26. package/build/dist/Server/Services/StatusPageSubscriberService.js +98 -1
  27. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  28. package/build/dist/Server/Utils/OpenAPI.js +135 -11
  29. package/build/dist/Server/Utils/OpenAPI.js.map +1 -1
  30. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +15 -0
  31. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  32. package/build/dist/Utils/Schema/ModelSchema.js +1101 -6
  33. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  34. package/package.json +1 -1
@@ -1136,6 +1136,43 @@ export default class StatusPage extends BaseModel {
1136
1136
  })
1137
1137
  public enableSmsSubscribers?: boolean = undefined;
1138
1138
 
1139
+ @ColumnAccessControl({
1140
+ create: [
1141
+ Permission.ProjectOwner,
1142
+ Permission.ProjectAdmin,
1143
+ Permission.ProjectMember,
1144
+ Permission.CreateProjectStatusPage,
1145
+ ],
1146
+ read: [
1147
+ Permission.ProjectOwner,
1148
+ Permission.ProjectAdmin,
1149
+ Permission.ProjectMember,
1150
+ Permission.ReadProjectStatusPage,
1151
+ ],
1152
+ update: [
1153
+ Permission.ProjectOwner,
1154
+ Permission.ProjectAdmin,
1155
+ Permission.ProjectMember,
1156
+ Permission.EditProjectStatusPage,
1157
+ ],
1158
+ })
1159
+ @TableColumn({
1160
+ isDefaultValueColumn: true,
1161
+ type: TableColumnType.Boolean,
1162
+ title: "Enable Slack Subscribers",
1163
+ description: "Can Slack subscribers subscribe to this Status Page?",
1164
+ })
1165
+ @Column({
1166
+ type: ColumnType.Boolean,
1167
+ default: false,
1168
+ })
1169
+ @ColumnBillingAccessControl({
1170
+ read: PlanType.Free,
1171
+ update: PlanType.Scale,
1172
+ create: PlanType.Free,
1173
+ })
1174
+ public enableSlackSubscribers?: boolean = undefined;
1175
+
1139
1176
  @ColumnAccessControl({
1140
1177
  create: [
1141
1178
  Permission.ProjectOwner,
@@ -319,6 +319,66 @@ export default class StatusPageSubscriber extends BaseModel {
319
319
  })
320
320
  public subscriberWebhook?: URL = undefined;
321
321
 
322
+ @ColumnAccessControl({
323
+ create: [
324
+ Permission.ProjectOwner,
325
+ Permission.ProjectAdmin,
326
+ Permission.ProjectMember,
327
+ Permission.CreateStatusPageSubscriber,
328
+ Permission.Public,
329
+ ],
330
+ read: [],
331
+ update: [],
332
+ })
333
+ @TableColumn({
334
+ required: false,
335
+ type: TableColumnType.ShortURL,
336
+ title: "Slack Incoming Webhook URL",
337
+ description:
338
+ "Slack incoming webhook URL to send notifications to Slack channel",
339
+ })
340
+ @Column({
341
+ nullable: true,
342
+ type: ColumnType.ShortURL,
343
+ transformer: URL.getDatabaseTransformer(),
344
+ })
345
+ public slackIncomingWebhookUrl?: URL = undefined;
346
+
347
+ @ColumnAccessControl({
348
+ create: [
349
+ Permission.ProjectOwner,
350
+ Permission.ProjectAdmin,
351
+ Permission.ProjectMember,
352
+ Permission.CreateStatusPageSubscriber,
353
+ Permission.Public,
354
+ ],
355
+ read: [
356
+ Permission.ProjectOwner,
357
+ Permission.ProjectAdmin,
358
+ Permission.ProjectMember,
359
+ Permission.ReadStatusPageSubscriber,
360
+ ],
361
+ update: [
362
+ Permission.ProjectOwner,
363
+ Permission.ProjectAdmin,
364
+ Permission.ProjectMember,
365
+ Permission.EditStatusPageSubscriber,
366
+ ],
367
+ })
368
+ @TableColumn({
369
+ required: false,
370
+ type: TableColumnType.ShortText,
371
+ title: "Slack Workspace Name",
372
+ description:
373
+ "Name of the Slack workspace for validation and identification",
374
+ })
375
+ @Column({
376
+ nullable: true,
377
+ type: ColumnType.ShortText,
378
+ length: ColumnLength.ShortText,
379
+ })
380
+ public slackWorkspaceName?: string = undefined;
381
+
322
382
  @ColumnAccessControl({
323
383
  create: [
324
384
  Permission.ProjectOwner,
@@ -89,6 +89,7 @@ import DatabaseConfig from "../DatabaseConfig";
89
89
  import { FileRoute } from "../../ServiceRoute";
90
90
  import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
91
91
  import ForbiddenException from "../../Types/Exception/ForbiddenException";
92
+ import SlackUtil from "../Utils/Workspace/Slack/Slack";
92
93
 
93
94
  export default class StatusPageAPI extends BaseAPI<
94
95
  StatusPage,
@@ -495,6 +496,7 @@ export default class StatusPageAPI extends BaseAPI<
495
496
  headerHTML: true,
496
497
  footerHTML: true,
497
498
  enableEmailSubscribers: true,
499
+ enableSlackSubscribers: true,
498
500
  enableSmsSubscribers: true,
499
501
  isPublicStatusPage: true,
500
502
  allowSubscribersToChooseResources: true,
@@ -2135,6 +2137,7 @@ export default class StatusPageAPI extends BaseAPI<
2135
2137
  _id: true,
2136
2138
  projectId: true,
2137
2139
  enableEmailSubscribers: true,
2140
+ enableSlackSubscribers: true,
2138
2141
  enableSmsSubscribers: true,
2139
2142
  allowSubscribersToChooseResources: true,
2140
2143
  allowSubscribersToChooseEventTypes: true,
@@ -2173,6 +2176,18 @@ export default class StatusPageAPI extends BaseAPI<
2173
2176
  );
2174
2177
  }
2175
2178
 
2179
+ if (
2180
+ req.body.data["slackIncomingWebhookUrl"] &&
2181
+ !statusPage.enableSlackSubscribers
2182
+ ) {
2183
+ logger.debug(
2184
+ `Slack subscribers not enabled for status page with ID: ${statusPageId}`,
2185
+ );
2186
+ throw new BadDataException(
2187
+ "Slack subscribers not enabled for this status page.",
2188
+ );
2189
+ }
2190
+
2176
2191
  if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
2177
2192
  logger.debug(
2178
2193
  `SMS subscribers not enabled for status page with ID: ${statusPageId}`,
@@ -2186,13 +2201,14 @@ export default class StatusPageAPI extends BaseAPI<
2186
2201
 
2187
2202
  if (
2188
2203
  !req.body.data["subscriberEmail"] &&
2189
- !req.body.data["subscriberPhone"]
2204
+ !req.body.data["subscriberPhone"] &&
2205
+ !req.body.data["slackWorkspaceName"]
2190
2206
  ) {
2191
2207
  logger.debug(
2192
- `No email or phone provided for subscription to status page with ID: ${statusPageId}`,
2208
+ `No email, slack workspace name or phone provided for subscription to status page with ID: ${statusPageId}`,
2193
2209
  );
2194
2210
  throw new BadDataException(
2195
- "Email or phone is required to subscribe to this status page.",
2211
+ "Email, phone or slack workspace name is required to subscribe to this status page.",
2196
2212
  );
2197
2213
  }
2198
2214
 
@@ -2204,6 +2220,12 @@ export default class StatusPageAPI extends BaseAPI<
2204
2220
  ? new Phone(req.body.data["subscriberPhone"] as string)
2205
2221
  : undefined;
2206
2222
 
2223
+ const slackWorkspaceName: string | undefined = req.body.data[
2224
+ "slackWorkspaceName"
2225
+ ]
2226
+ ? (req.body.data["slackWorkspaceName"] as string)
2227
+ : undefined;
2228
+
2207
2229
  let statusPageSubscriber: StatusPageSubscriber | null = null;
2208
2230
 
2209
2231
  if (email) {
@@ -2240,19 +2262,39 @@ export default class StatusPageAPI extends BaseAPI<
2240
2262
  });
2241
2263
  }
2242
2264
 
2265
+ if (slackWorkspaceName) {
2266
+ logger.debug(`Setting subscriber slack workspace: ${slackWorkspaceName}`);
2267
+ statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
2268
+ query: {
2269
+ slackWorkspaceName: slackWorkspaceName,
2270
+ statusPageId: statusPageId,
2271
+ },
2272
+ select: {
2273
+ _id: true,
2274
+ slackWorkspaceName: true,
2275
+ slackIncomingWebhookUrl: true,
2276
+ },
2277
+ props: {
2278
+ isRoot: true,
2279
+ },
2280
+ });
2281
+ }
2282
+
2243
2283
  if (!statusPageSubscriber) {
2244
2284
  // not found, return bad data
2245
2285
  logger.debug(
2246
- `Subscriber not found for email: ${email} or phone: ${phone}`,
2286
+ `Subscriber not found for email: ${email}, phone: ${phone}, or slack workspace: ${slackWorkspaceName}`,
2247
2287
  );
2248
2288
 
2249
- let emailOrPhone: string = "email";
2289
+ let identifierType: string = "email";
2250
2290
  if (phone) {
2251
- emailOrPhone = "phone";
2291
+ identifierType = "phone";
2292
+ } else if (slackWorkspaceName) {
2293
+ identifierType = "slack workspace name";
2252
2294
  }
2253
2295
 
2254
2296
  throw new BadDataException(
2255
- `Subscription not found for this status page. Please make sure your ${emailOrPhone} is correct.`,
2297
+ `Subscription not found for this status page. Please make sure your ${identifierType} is correct.`,
2256
2298
  );
2257
2299
  }
2258
2300
 
@@ -2327,6 +2369,17 @@ export default class StatusPageAPI extends BaseAPI<
2327
2369
  });
2328
2370
  }
2329
2371
 
2372
+ if (statusPageSubscriber.slackIncomingWebhookUrl) {
2373
+ const slackMessage: string = `You have selected to manage your subscription for the status page: ${statusPage.name}. You can manage your subscription here: ${manageUrlink}`;
2374
+
2375
+ SlackUtil.sendMessageToChannelViaIncomingWebhook({
2376
+ url: statusPageSubscriber.slackIncomingWebhookUrl,
2377
+ text: slackMessage,
2378
+ }).catch((err: Error) => {
2379
+ logger.error(err);
2380
+ });
2381
+ }
2382
+
2330
2383
  logger.debug(
2331
2384
  `Subscription management link sent to subscriber with ID: ${statusPageSubscriber.id}`,
2332
2385
  );
@@ -2355,6 +2408,7 @@ export default class StatusPageAPI extends BaseAPI<
2355
2408
  projectId: true,
2356
2409
  enableEmailSubscribers: true,
2357
2410
  enableSmsSubscribers: true,
2411
+ enableSlackSubscribers: true,
2358
2412
  allowSubscribersToChooseResources: true,
2359
2413
  allowSubscribersToChooseEventTypes: true,
2360
2414
  showSubscriberPageOnStatusPage: true,
@@ -2403,15 +2457,28 @@ export default class StatusPageAPI extends BaseAPI<
2403
2457
 
2404
2458
  // if no email or phone, throw error.
2405
2459
 
2460
+ if (
2461
+ req.body.data["slackWorkspaceName"] &&
2462
+ !statusPage.enableSlackSubscribers
2463
+ ) {
2464
+ logger.debug(
2465
+ `Slack subscribers not enabled for status page with ID: ${objectId}`,
2466
+ );
2467
+ throw new BadDataException(
2468
+ "Slack subscribers not enabled for this status page.",
2469
+ );
2470
+ }
2471
+
2406
2472
  if (
2407
2473
  !req.body.data["subscriberEmail"] &&
2408
- !req.body.data["subscriberPhone"]
2474
+ !req.body.data["subscriberPhone"] &&
2475
+ !req.body.data["slackWorkspaceName"]
2409
2476
  ) {
2410
2477
  logger.debug(
2411
- `No email or phone provided for subscription to status page with ID: ${objectId}`,
2478
+ `No email, phone, or slack workspace name provided for subscription to status page with ID: ${objectId}`,
2412
2479
  );
2413
2480
  throw new BadDataException(
2414
- "Email or phone is required to subscribe to this status page.",
2481
+ "Email, phone or slack workspace name is required to subscribe to this status page.",
2415
2482
  );
2416
2483
  }
2417
2484
 
@@ -2423,6 +2490,18 @@ export default class StatusPageAPI extends BaseAPI<
2423
2490
  ? new Phone(req.body.data["subscriberPhone"] as string)
2424
2491
  : undefined;
2425
2492
 
2493
+ const slackIncomingWebhookUrl: string | undefined = req.body.data[
2494
+ "slackIncomingWebhookUrl"
2495
+ ]
2496
+ ? (req.body.data["slackIncomingWebhookUrl"] as string)
2497
+ : undefined;
2498
+
2499
+ const slackWorkspaceName: string | undefined = req.body.data[
2500
+ "slackWorkspaceName"
2501
+ ]
2502
+ ? (req.body.data["slackWorkspaceName"] as string)
2503
+ : undefined;
2504
+
2426
2505
  let statusPageSubscriber: StatusPageSubscriber | null = null;
2427
2506
 
2428
2507
  let isUpdate: boolean = false;
@@ -2467,6 +2546,20 @@ export default class StatusPageAPI extends BaseAPI<
2467
2546
  statusPageSubscriber.subscriberPhone = phone;
2468
2547
  }
2469
2548
 
2549
+ if (slackIncomingWebhookUrl) {
2550
+ logger.debug(`Setting subscriber slack: ${slackIncomingWebhookUrl}`);
2551
+ statusPageSubscriber.slackIncomingWebhookUrl = URL.fromString(
2552
+ slackIncomingWebhookUrl,
2553
+ );
2554
+ }
2555
+
2556
+ if (slackWorkspaceName) {
2557
+ logger.debug(
2558
+ `Setting subscriber slack workspace name: ${slackWorkspaceName}`,
2559
+ );
2560
+ statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
2561
+ }
2562
+
2470
2563
  if (
2471
2564
  req.body.data["statusPageResources"] &&
2472
2565
  !statusPage.allowSubscribersToChooseResources
@@ -2606,6 +2699,7 @@ export default class StatusPageAPI extends BaseAPI<
2606
2699
  isUnsubscribed: true,
2607
2700
  subscriberEmail: true,
2608
2701
  subscriberPhone: true,
2702
+ slackWorkspaceName: true,
2609
2703
  statusPageId: true,
2610
2704
  statusPageResources: true,
2611
2705
  isSubscribedToAllResources: true,
@@ -0,0 +1,23 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1749065784320 implements MigrationInterface {
4
+ public name = "MigrationName1749065784320";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "StatusPage" ADD "enableSlackSubscribers" boolean NOT NULL DEFAULT false`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "StatusPageSubscriber" ADD "slackIncomingWebhookUrl" character varying`,
12
+ );
13
+ }
14
+
15
+ public async down(queryRunner: QueryRunner): Promise<void> {
16
+ await queryRunner.query(
17
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "slackIncomingWebhookUrl"`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "StatusPage" DROP COLUMN "enableSlackSubscribers"`,
21
+ );
22
+ }
23
+ }
@@ -0,0 +1,17 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1749133333893 implements MigrationInterface {
4
+ public name = "MigrationName1749133333893";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "StatusPageSubscriber" ADD "slackWorkspaceName" character varying(100)`,
9
+ );
10
+ }
11
+
12
+ public async down(queryRunner: QueryRunner): Promise<void> {
13
+ await queryRunner.query(
14
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "slackWorkspaceName"`,
15
+ );
16
+ }
17
+ }
@@ -134,6 +134,8 @@ import { MigrationName1744809770336 } from "./1744809770336-MigrationName";
134
134
  import { MigrationName1747305098533 } from "./1747305098533-MigrationName";
135
135
  import { MigrationName1747674762672 } from "./1747674762672-MigrationName";
136
136
  import { MigrationName1748456937826 } from "./1748456937826-MigrationName";
137
+ import { MigrationName1749065784320 } from "./1749065784320-MigrationName";
138
+ import { MigrationName1749133333893 } from "./1749133333893-MigrationName";
137
139
 
138
140
  export default [
139
141
  InitialMigration,
@@ -272,4 +274,6 @@ export default [
272
274
  MigrationName1747305098533,
273
275
  MigrationName1747674762672,
274
276
  MigrationName1748456937826,
277
+ MigrationName1749065784320,
278
+ MigrationName1749133333893,
275
279
  ];
@@ -49,6 +49,7 @@ import { IsBillingEnabled } from "../EnvironmentConfig";
49
49
  import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
50
50
  import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
51
51
  import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
52
+ import SlackUtil from "../Utils/Workspace/Slack/Slack";
52
53
  import { Gray500, Red500 } from "../../Types/BrandColors";
53
54
  import Label from "../../Models/DatabaseModels/Label";
54
55
  import LabelService from "./LabelService";
@@ -254,6 +255,26 @@ export class Service extends DatabaseService<Model> {
254
255
  });
255
256
  }
256
257
 
258
+ if (subscriber.slackIncomingWebhookUrl) {
259
+ const slackMessage: string = `## 🔧 Scheduled Maintenance - ${event.title || ""}
260
+
261
+ **Scheduled Date:** ${OneUptimeDate.getDateAsFormattedString(event.startsAt!)}
262
+
263
+ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
264
+
265
+ **Description:** ${event.description || ""}
266
+
267
+ [View Status Page](${statusPageURL}) | [Unsubscribe](${unsubscribeUrl})`;
268
+
269
+ // send Slack notification here.
270
+ SlackUtil.sendMessageToChannelViaIncomingWebhook({
271
+ url: subscriber.slackIncomingWebhookUrl,
272
+ text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
273
+ }).catch((err: Error) => {
274
+ logger.error(err);
275
+ });
276
+ }
277
+
257
278
  if (subscriber.subscriberEmail) {
258
279
  // send email here.
259
280
 
@@ -31,6 +31,7 @@ import Model from "../../Models/DatabaseModels/StatusPageSubscriber";
31
31
  import PositiveNumber from "../../Types/PositiveNumber";
32
32
  import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
33
33
  import NumberUtil from "../../Utils/Number";
34
+ import SlackUtil from "../Utils/Workspace/Slack/Slack";
34
35
 
35
36
  export class Service extends DatabaseService<Model> {
36
37
  public constructor() {
@@ -143,6 +144,7 @@ export class Service extends DatabaseService<Model> {
143
144
  ignoreHooks: true,
144
145
  },
145
146
  });
147
+
146
148
  logger.debug(`Found Subscriber by Phone: ${JSON.stringify(subscriber)}`);
147
149
  }
148
150
 
@@ -197,12 +199,28 @@ export class Service extends DatabaseService<Model> {
197
199
  if (isEmailSubscriber && !isSubscriptionConfirmed) {
198
200
  data.data.isSubscriptionConfirmed = false;
199
201
  } else {
200
- data.data.isSubscriptionConfirmed = true; // if the subscriber is not email, then set it to true for SMS subscribers.
202
+ data.data.isSubscriptionConfirmed = true; // if the subscriber is not email, then set it to true for SMS subscribers / slack subscribers.
201
203
  }
202
204
  logger.debug(
203
205
  `Final Subscription Confirmed: ${data.data.isSubscriptionConfirmed}`,
204
206
  );
205
207
 
208
+ // if slack incoming webhook is provided, then see if it starts with https://hooks.slack.com/services/
209
+
210
+ if (data.data.slackIncomingWebhookUrl) {
211
+ logger.debug(
212
+ `Slack Incoming Webhook URL: ${data.data.slackIncomingWebhookUrl}`,
213
+ );
214
+ if (
215
+ !SlackUtil.isValidSlackIncomingWebhookUrl(
216
+ data.data.slackIncomingWebhookUrl,
217
+ )
218
+ ) {
219
+ logger.debug("Invalid Slack Incoming Webhook URL.");
220
+ throw new BadDataException("Invalid Slack Incoming Webhook URL.");
221
+ }
222
+ }
223
+
206
224
  data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(
207
225
  100000,
208
226
  999999,
@@ -331,6 +349,38 @@ export class Service extends DatabaseService<Model> {
331
349
  }
332
350
  }
333
351
 
352
+ // if slack incoming webhook is provided, then send a message to the slack channel.
353
+ if (createdItem.slackIncomingWebhookUrl) {
354
+ logger.debug("Sending Slack notification for new subscriber.");
355
+ const slackMessage: string = `## 📢 New Subscription to ${statusPageName}
356
+
357
+ **You have successfully subscribed to receive status updates!**
358
+
359
+ 🔗 **Status Page:** [${statusPageName}](${statusPageURL})
360
+ 📧 **Manage Subscription:** [Update preferences or unsubscribe](${unsubscribeLink})
361
+
362
+ You will receive real-time notifications for:
363
+ • Incidents and outages
364
+ • Scheduled maintenance events
365
+ • Service announcements
366
+ • Status updates
367
+
368
+ Stay informed about service availability! 🚀`;
369
+
370
+ logger.debug(`Slack Message: ${slackMessage}`);
371
+
372
+ try {
373
+ await SlackUtil.sendMessageToChannelViaIncomingWebhook({
374
+ url: URL.fromString(createdItem.slackIncomingWebhookUrl.toString()),
375
+ text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
376
+ });
377
+ logger.debug("Slack notification sent successfully.");
378
+ } catch (error) {
379
+ logger.error("Error sending Slack notification:");
380
+ logger.error(error);
381
+ }
382
+ }
383
+
334
384
  logger.debug("onCreateSuccess completed.");
335
385
  return createdItem;
336
386
  }
@@ -637,6 +687,7 @@ export class Service extends DatabaseService<Model> {
637
687
  subscriberEmail: true,
638
688
  subscriberPhone: true,
639
689
  subscriberWebhook: true,
690
+ slackIncomingWebhookUrl: true,
640
691
  isSubscribedToAllResources: true,
641
692
  statusPageResources: true,
642
693
  isSubscribedToAllEventTypes: true,
@@ -820,5 +871,69 @@ export class Service extends DatabaseService<Model> {
820
871
 
821
872
  return statusPages;
822
873
  }
874
+
875
+ @CaptureSpan()
876
+ public async testSlackWebhook(data: {
877
+ webhookUrl: string;
878
+ statusPageId: ObjectID;
879
+ }): Promise<void> {
880
+ // Validate the webhook URL
881
+ if (!data.webhookUrl.startsWith("https://hooks.slack.com/services/")) {
882
+ throw new BadDataException("Invalid Slack webhook URL");
883
+ }
884
+
885
+ // Get status page info
886
+ const statusPage: StatusPage | null = await StatusPageService.findOneById({
887
+ id: data.statusPageId,
888
+ props: {
889
+ isRoot: true,
890
+ },
891
+ select: {
892
+ name: true,
893
+ pageTitle: true,
894
+ projectId: true,
895
+ _id: true,
896
+ },
897
+ });
898
+
899
+ if (!statusPage) {
900
+ throw new BadDataException("Status page not found");
901
+ }
902
+
903
+ // Create test notification message
904
+ const statusPageName: string =
905
+ statusPage.pageTitle || statusPage.name || "Status Page";
906
+ const statusPageURL: string = await StatusPageService.getStatusPageURL(
907
+ statusPage.id!,
908
+ );
909
+
910
+ // Create markdown message for Slack
911
+ const markdownMessage: string = `## Test Notification - ${statusPageName}
912
+
913
+ **This is a test notification from OneUptime.**
914
+
915
+ You have successfully configured Slack notifications for this status page.
916
+
917
+ You will receive real-time notifications for:
918
+ - Incidents
919
+ - Scheduled Maintenance Events
920
+ - Status Updates
921
+ - Announcements
922
+
923
+ [View Status Page](${statusPageURL})`;
924
+
925
+ // Send the test notification
926
+ try {
927
+ await SlackUtil.sendMessageToChannelViaIncomingWebhook({
928
+ url: URL.fromString(data.webhookUrl),
929
+ text: SlackUtil.convertMarkdownToSlackRichText(markdownMessage),
930
+ });
931
+ } catch (error) {
932
+ logger.error("Error sending test Slack notification:");
933
+ logger.error(error);
934
+ throw error;
935
+ }
936
+ }
823
937
  }
938
+
824
939
  export default new Service();