@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.
- package/Models/DatabaseModels/StatusPage.ts +37 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +60 -0
- package/Server/API/StatusPageAPI.ts +104 -10
- package/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.ts +23 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/ScheduledMaintenanceService.ts +21 -0
- package/Server/Services/StatusPageSubscriberService.ts +116 -1
- package/Server/Utils/OpenAPI.ts +176 -11
- package/Server/Utils/Workspace/Slack/Slack.ts +14 -0
- package/Utils/Schema/ModelSchema.ts +1303 -11
- package/build/dist/Models/DatabaseModels/StatusPage.js +39 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +62 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +73 -10
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +19 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +98 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Utils/OpenAPI.js +135 -11
- package/build/dist/Server/Utils/OpenAPI.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +15 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Utils/Schema/ModelSchema.js +1101 -6
- package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
- 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
|
|
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}
|
|
2286
|
+
`Subscriber not found for email: ${email}, phone: ${phone}, or slack workspace: ${slackWorkspaceName}`,
|
|
2247
2287
|
);
|
|
2248
2288
|
|
|
2249
|
-
let
|
|
2289
|
+
let identifierType: string = "email";
|
|
2250
2290
|
if (phone) {
|
|
2251
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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();
|