@oneuptime/common 7.0.4707 → 7.0.4720
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/Index.ts +2 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +47 -0
- package/Models/DatabaseModels/UserNotificationSetting.ts +16 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +47 -0
- package/Models/DatabaseModels/UserPush.ts +302 -0
- package/Server/API/UserPushAPI.ts +302 -0
- package/Server/EnvironmentConfig.ts +10 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1752659054949-MigrationName.ts +87 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1752774923063-MigrationName.ts +27 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1753109689244-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/MonitorService.ts +14 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +14 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +15 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +14 -0
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +29 -0
- package/Server/Services/ProbeService.ts +23 -0
- package/Server/Services/PushNotificationService.ts +253 -0
- package/Server/Services/UserNotificationRuleService.ts +148 -2
- package/Server/Services/UserNotificationSettingService.ts +20 -0
- package/Server/Services/UserPushService.ts +95 -0
- package/Server/Utils/PushNotificationUtil.ts +308 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +1 -1
- package/Types/PushNotification/PushNotificationMessage.ts +18 -0
- package/Types/PushNotification/PushNotificationRequest.ts +22 -0
- package/UI/Components/Button/Button.tsx +9 -9
- package/UI/Components/Card/Card.tsx +2 -2
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
- package/UI/Components/Date/StartAndEndDate.tsx +8 -3
- package/UI/Components/HeaderAlert/HeaderAlert.tsx +9 -1
- package/UI/Components/HeaderAlert/HeaderModelAlert.tsx +2 -1
- package/UI/Components/Modal/Modal.tsx +16 -14
- package/UI/Components/Modal/ModalFooter.tsx +1 -1
- package/UI/Components/Page/Page.tsx +1 -1
- package/UI/Components/Table/Table.tsx +48 -25
- package/UI/Components/Table/TableBody.tsx +52 -0
- package/UI/Components/Table/TableRow.tsx +221 -0
- package/UI/Config.ts +3 -0
- package/UI/Utils/Cookie.ts +1 -3
- package/UI/Utils/LocalStorage.ts +1 -3
- package/UI/Utils/SessionStorage.ts +1 -3
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +48 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +18 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +48 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +324 -0
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -0
- package/build/dist/Server/API/UserPushAPI.js +175 -0
- package/build/dist/Server/API/UserPushAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +4 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752659054949-MigrationName.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752659054949-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752774923063-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752774923063-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1753109689244-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1753109689244-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +12 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +13 -4
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +13 -4
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +12 -3
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +22 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +12 -1
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/PushNotificationService.js +172 -0
- package/build/dist/Server/Services/PushNotificationService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +108 -3
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +10 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +102 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -0
- package/build/dist/Server/Utils/PushNotificationUtil.js +222 -0
- package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -0
- package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +1 -1
- package/build/dist/Types/PushNotification/PushNotificationMessage.js +2 -0
- package/build/dist/Types/PushNotification/PushNotificationMessage.js.map +1 -0
- package/build/dist/Types/PushNotification/PushNotificationRequest.js +2 -0
- package/build/dist/Types/PushNotification/PushNotificationRequest.js.map +1 -0
- package/build/dist/UI/Components/Button/Button.js +9 -9
- package/build/dist/UI/Components/Card/Card.js +2 -2
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Date/StartAndEndDate.js +8 -8
- package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +2 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderModelAlert.js +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderModelAlert.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +14 -14
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +1 -1
- package/build/dist/UI/Components/Table/Table.js +29 -15
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +19 -1
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +68 -0
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Config.js +2 -0
- package/build/dist/UI/Config.js.map +1 -1
- package/build/dist/UI/Utils/Cookie.js +1 -3
- package/build/dist/UI/Utils/Cookie.js.map +1 -1
- package/build/dist/UI/Utils/LocalStorage.js +1 -3
- package/build/dist/UI/Utils/LocalStorage.js.map +1 -1
- package/build/dist/UI/Utils/SessionStorage.js +1 -3
- package/build/dist/UI/Utils/SessionStorage.js.map +1 -1
- package/package.json +3 -1
|
@@ -34,6 +34,8 @@ import { Green500 } from "../../Types/BrandColors";
|
|
|
34
34
|
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
|
|
35
35
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
36
36
|
import { OnDelete } from "../Types/Database/Hooks";
|
|
37
|
+
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
38
|
+
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
37
39
|
|
|
38
40
|
export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
39
41
|
private layerUtil = new LayerUtil();
|
|
@@ -253,12 +255,21 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
253
255
|
],
|
|
254
256
|
};
|
|
255
257
|
|
|
258
|
+
const pushMessage: PushNotificationMessage =
|
|
259
|
+
PushNotificationUtil.createGenericNotification({
|
|
260
|
+
title: "On-Call Duty Ended",
|
|
261
|
+
body: `You are no longer on-call for ${onCallPolicy.name!} as your roster on schedule ${onCallSchedule.name} has ended.`,
|
|
262
|
+
tag: "on-call-duty-ended",
|
|
263
|
+
requireInteraction: false,
|
|
264
|
+
});
|
|
265
|
+
|
|
256
266
|
await UserNotificationSettingService.sendUserNotification({
|
|
257
267
|
userId: sendEmailToUserId,
|
|
258
268
|
projectId: projectId,
|
|
259
269
|
emailEnvelope: emailMessage,
|
|
260
270
|
smsMessage: sms,
|
|
261
271
|
callRequestMessage: callMessage,
|
|
272
|
+
pushNotificationMessage: pushMessage,
|
|
262
273
|
eventType:
|
|
263
274
|
NotificationSettingEventType.SEND_WHEN_USER_IS_NO_LONGER_ACTIVE_ON_ON_CALL_ROSTER,
|
|
264
275
|
});
|
|
@@ -360,12 +371,21 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
360
371
|
],
|
|
361
372
|
};
|
|
362
373
|
|
|
374
|
+
const pushMessage: PushNotificationMessage =
|
|
375
|
+
PushNotificationUtil.createGenericNotification({
|
|
376
|
+
title: "On-Call Duty Started",
|
|
377
|
+
body: `You are now on-call for ${onCallPolicy.name!} on schedule ${onCallSchedule.name}.`,
|
|
378
|
+
tag: "on-call-duty-started",
|
|
379
|
+
requireInteraction: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
363
382
|
await UserNotificationSettingService.sendUserNotification({
|
|
364
383
|
userId: sendEmailToUserId,
|
|
365
384
|
projectId: projectId,
|
|
366
385
|
emailEnvelope: emailMessage,
|
|
367
386
|
smsMessage: sms,
|
|
368
387
|
callRequestMessage: callMessage,
|
|
388
|
+
pushNotificationMessage: pushMessage,
|
|
369
389
|
eventType:
|
|
370
390
|
NotificationSettingEventType.SEND_WHEN_USER_IS_ON_CALL_ROSTER,
|
|
371
391
|
});
|
|
@@ -487,12 +507,21 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
487
507
|
],
|
|
488
508
|
};
|
|
489
509
|
|
|
510
|
+
const pushMessage: PushNotificationMessage =
|
|
511
|
+
PushNotificationUtil.createGenericNotification({
|
|
512
|
+
title: "Next On-Call Duty",
|
|
513
|
+
body: `You are next on-call for ${onCallPolicy.name!} on schedule ${onCallSchedule.name}.`,
|
|
514
|
+
tag: "next-on-call-duty",
|
|
515
|
+
requireInteraction: false,
|
|
516
|
+
});
|
|
517
|
+
|
|
490
518
|
await UserNotificationSettingService.sendUserNotification({
|
|
491
519
|
userId: sendEmailToUserId,
|
|
492
520
|
projectId: projectId,
|
|
493
521
|
emailEnvelope: emailMessage,
|
|
494
522
|
smsMessage: sms,
|
|
495
523
|
callRequestMessage: callMessage,
|
|
524
|
+
pushNotificationMessage: pushMessage,
|
|
496
525
|
eventType:
|
|
497
526
|
NotificationSettingEventType.SEND_WHEN_USER_IS_NEXT_ON_CALL_ROSTER,
|
|
498
527
|
});
|
|
@@ -28,6 +28,8 @@ import DatabaseConfig from "../DatabaseConfig";
|
|
|
28
28
|
import URL from "../../Types/API/URL";
|
|
29
29
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
30
30
|
import MonitorService from "./MonitorService";
|
|
31
|
+
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
32
|
+
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
31
33
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
32
34
|
import { IsBillingEnabled } from "../EnvironmentConfig";
|
|
33
35
|
import GlobalCache from "../Infrastructure/GlobalCache";
|
|
@@ -365,12 +367,33 @@ export class Service extends DatabaseService<Model> {
|
|
|
365
367
|
],
|
|
366
368
|
};
|
|
367
369
|
|
|
370
|
+
const pushMessageParams: {
|
|
371
|
+
probeName: string;
|
|
372
|
+
projectName: string;
|
|
373
|
+
connectionStatus: string;
|
|
374
|
+
clickAction?: string;
|
|
375
|
+
} = {
|
|
376
|
+
probeName: probe.name!,
|
|
377
|
+
projectName: probe.project?.name || "Project",
|
|
378
|
+
connectionStatus: connectionStatus,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
if (vars["viewProbesLink"]) {
|
|
382
|
+
pushMessageParams.clickAction = vars["viewProbesLink"];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const pushMessage: PushNotificationMessage =
|
|
386
|
+
PushNotificationUtil.createProbeStatusChangedNotification(
|
|
387
|
+
pushMessageParams,
|
|
388
|
+
);
|
|
389
|
+
|
|
368
390
|
await UserNotificationSettingService.sendUserNotification({
|
|
369
391
|
userId: user.id!,
|
|
370
392
|
projectId: probe.projectId!,
|
|
371
393
|
emailEnvelope: emailMessage,
|
|
372
394
|
smsMessage: sms,
|
|
373
395
|
callRequestMessage: callMessage,
|
|
396
|
+
pushNotificationMessage: pushMessage,
|
|
374
397
|
eventType:
|
|
375
398
|
NotificationSettingEventType.SEND_PROBE_STATUS_CHANGED_OWNER_NOTIFICATION,
|
|
376
399
|
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import PushNotificationRequest from "../../Types/PushNotification/PushNotificationRequest";
|
|
2
|
+
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import logger from "../Utils/Logger";
|
|
5
|
+
import UserPushService from "./UserPushService";
|
|
6
|
+
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
|
7
|
+
import UserNotificationStatus from "../../Types/UserNotification/UserNotificationStatus";
|
|
8
|
+
import {
|
|
9
|
+
VapidPublicKey,
|
|
10
|
+
VapidPrivateKey,
|
|
11
|
+
VapidSubject,
|
|
12
|
+
} from "../EnvironmentConfig";
|
|
13
|
+
import webpush from "web-push";
|
|
14
|
+
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
15
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
16
|
+
import UserPush from "../../Models/DatabaseModels/UserPush";
|
|
17
|
+
|
|
18
|
+
export interface PushNotificationOptions {
|
|
19
|
+
projectId?: ObjectID | undefined;
|
|
20
|
+
isSensitive?: boolean;
|
|
21
|
+
userOnCallLogTimelineId?: ObjectID | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default class PushNotificationService {
|
|
25
|
+
public static isWebPushInitialized = false;
|
|
26
|
+
|
|
27
|
+
public static initializeWebPush(): void {
|
|
28
|
+
if (this.isWebPushInitialized) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!VapidPublicKey || !VapidPrivateKey) {
|
|
33
|
+
logger.warn(
|
|
34
|
+
"VAPID keys not configured. Web push notifications will not work.",
|
|
35
|
+
);
|
|
36
|
+
logger.warn(`VapidPublicKey present: ${Boolean(VapidPublicKey)}`);
|
|
37
|
+
logger.warn(`VapidPrivateKey present: ${Boolean(VapidPrivateKey)}`);
|
|
38
|
+
logger.warn(`VapidSubject: ${VapidSubject}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.info(`Initializing web push with VAPID subject: ${VapidSubject}`);
|
|
43
|
+
webpush.setVapidDetails(VapidSubject, VapidPublicKey, VapidPrivateKey);
|
|
44
|
+
this.isWebPushInitialized = true;
|
|
45
|
+
logger.info("Web push notifications initialized successfully");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static async sendPushNotification(
|
|
49
|
+
request: PushNotificationRequest,
|
|
50
|
+
options: PushNotificationOptions = {},
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
logger.info(
|
|
53
|
+
`Sending push notification to ${request.deviceTokens?.length} devices`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!request.deviceTokens || request.deviceTokens.length === 0) {
|
|
57
|
+
logger.error("No device tokens provided for push notification");
|
|
58
|
+
throw new Error("No device tokens provided");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (request.deviceType !== "web") {
|
|
62
|
+
logger.error(`Unsupported device type: ${request.deviceType}`);
|
|
63
|
+
throw new Error("Only web push notifications are supported");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger.info(
|
|
67
|
+
`Sending web push notifications to ${request.deviceTokens.length} devices`,
|
|
68
|
+
);
|
|
69
|
+
logger.info(`Notification message: ${JSON.stringify(request.message)}`);
|
|
70
|
+
|
|
71
|
+
const promises: Promise<void>[] = [];
|
|
72
|
+
|
|
73
|
+
for (const deviceToken of request.deviceTokens) {
|
|
74
|
+
promises.push(
|
|
75
|
+
this.sendWebPushNotification(deviceToken, request.message, options),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const results: Array<any> = await Promise.allSettled(promises);
|
|
80
|
+
|
|
81
|
+
let successCount: number = 0;
|
|
82
|
+
let errorCount: number = 0;
|
|
83
|
+
|
|
84
|
+
results.forEach((result: any, index: number) => {
|
|
85
|
+
if (result.status === "fulfilled") {
|
|
86
|
+
successCount++;
|
|
87
|
+
logger.info(`Device ${index + 1}: Notification sent successfully`);
|
|
88
|
+
} else {
|
|
89
|
+
errorCount++;
|
|
90
|
+
logger.error(
|
|
91
|
+
`Failed to send notification to device ${index + 1}: ${result.reason}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
logger.info(
|
|
97
|
+
`Push notification results: ${successCount} successful, ${errorCount} failed`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Update user on call log timeline status if provided
|
|
101
|
+
if (options.userOnCallLogTimelineId) {
|
|
102
|
+
const status: UserNotificationStatus =
|
|
103
|
+
successCount > 0
|
|
104
|
+
? UserNotificationStatus.Sent
|
|
105
|
+
: UserNotificationStatus.Error;
|
|
106
|
+
const statusMessage: string =
|
|
107
|
+
successCount > 0
|
|
108
|
+
? "Push notification sent successfully"
|
|
109
|
+
: `Failed to send push notification: ${errorCount} errors`;
|
|
110
|
+
|
|
111
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
112
|
+
id: options.userOnCallLogTimelineId,
|
|
113
|
+
data: {
|
|
114
|
+
status,
|
|
115
|
+
statusMessage,
|
|
116
|
+
},
|
|
117
|
+
props: {
|
|
118
|
+
isRoot: true,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (errorCount > 0 && successCount === 0) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Failed to send push notification to all ${errorCount} devices`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private static async sendWebPushNotification(
|
|
131
|
+
deviceToken: string,
|
|
132
|
+
message: PushNotificationMessage,
|
|
133
|
+
_options: PushNotificationOptions,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
if (!this.isWebPushInitialized) {
|
|
136
|
+
this.initializeWebPush();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!this.isWebPushInitialized) {
|
|
140
|
+
throw new Error("Web push notifications not configured");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const payload: string = JSON.stringify({
|
|
145
|
+
title: message.title,
|
|
146
|
+
body: message.body,
|
|
147
|
+
icon: message.icon || PushNotificationUtil.DEFAULT_ICON,
|
|
148
|
+
badge: message.badge || PushNotificationUtil.DEFAULT_BADGE,
|
|
149
|
+
data: message.data || {},
|
|
150
|
+
tag: message.tag || "oneuptime-notification",
|
|
151
|
+
requireInteraction: message.requireInteraction || false,
|
|
152
|
+
actions: message.actions || [],
|
|
153
|
+
url: message.url || message.clickAction,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
logger.debug(`Sending push notification with payload: ${payload}`);
|
|
157
|
+
logger.debug(`Device token: ${deviceToken}`);
|
|
158
|
+
|
|
159
|
+
let subscriptionObject: any;
|
|
160
|
+
try {
|
|
161
|
+
subscriptionObject = JSON.parse(deviceToken);
|
|
162
|
+
logger.debug(
|
|
163
|
+
`Parsed subscription object: ${JSON.stringify(subscriptionObject)}`,
|
|
164
|
+
);
|
|
165
|
+
} catch (parseError) {
|
|
166
|
+
logger.error(`Failed to parse device token: ${parseError}`);
|
|
167
|
+
throw new Error(`Invalid device token format: ${parseError}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const result: webpush.SendResult = await webpush.sendNotification(
|
|
171
|
+
subscriptionObject,
|
|
172
|
+
payload,
|
|
173
|
+
{
|
|
174
|
+
TTL: 24 * 60 * 60, // 24 hours
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
logger.debug(`Web push notification sent successfully:`);
|
|
179
|
+
logger.debug(`Result: ${JSON.stringify(result, null, 2)}`);
|
|
180
|
+
logger.debug(`Payload: ${JSON.stringify(payload, null, 2)}`);
|
|
181
|
+
logger.debug(
|
|
182
|
+
`Subscription object: ${JSON.stringify(subscriptionObject, null, 2)}`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
logger.info(`Web push notification sent successfully`);
|
|
186
|
+
} catch (error: any) {
|
|
187
|
+
logger.error(`Failed to send web push notification: ${error.message}`);
|
|
188
|
+
logger.error(error);
|
|
189
|
+
|
|
190
|
+
// If the subscription is no longer valid, remove it
|
|
191
|
+
if (error.statusCode === 410 || error.statusCode === 404) {
|
|
192
|
+
logger.info("Removing invalid web push subscription");
|
|
193
|
+
// You would implement removal logic here
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public static async sendPushNotificationToUser(
|
|
201
|
+
userId: ObjectID,
|
|
202
|
+
projectId: ObjectID,
|
|
203
|
+
message: PushNotificationMessage,
|
|
204
|
+
options: PushNotificationOptions = {},
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
// Get all verified push devices for the user
|
|
207
|
+
const userPushDevices: UserPush[] = await UserPushService.findBy({
|
|
208
|
+
query: {
|
|
209
|
+
userId: userId,
|
|
210
|
+
projectId: projectId,
|
|
211
|
+
isVerified: true,
|
|
212
|
+
},
|
|
213
|
+
select: {
|
|
214
|
+
deviceToken: true,
|
|
215
|
+
deviceType: true,
|
|
216
|
+
_id: true,
|
|
217
|
+
},
|
|
218
|
+
limit: LIMIT_PER_PROJECT,
|
|
219
|
+
skip: 0,
|
|
220
|
+
props: {
|
|
221
|
+
isRoot: true,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (userPushDevices.length === 0) {
|
|
226
|
+
logger.info(
|
|
227
|
+
`No verified web push devices found for user ${userId.toString()}`,
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Get web device tokens
|
|
233
|
+
const webDevices: string[] = [];
|
|
234
|
+
|
|
235
|
+
for (const device of userPushDevices) {
|
|
236
|
+
if (device.deviceType === "web") {
|
|
237
|
+
webDevices.push(device.deviceToken!);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Send notifications to web devices
|
|
242
|
+
if (webDevices.length > 0) {
|
|
243
|
+
await this.sendPushNotification(
|
|
244
|
+
{
|
|
245
|
+
deviceTokens: webDevices,
|
|
246
|
+
message: message,
|
|
247
|
+
deviceType: "web",
|
|
248
|
+
},
|
|
249
|
+
options,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -45,8 +45,11 @@ import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
|
|
45
45
|
import AlertSeverityService from "./AlertSeverityService";
|
|
46
46
|
import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule";
|
|
47
47
|
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
|
|
48
|
+
import PushNotificationService from "./PushNotificationService";
|
|
48
49
|
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
|
|
49
50
|
import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
|
|
51
|
+
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
52
|
+
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
50
53
|
import logger from "../Utils/Logger";
|
|
51
54
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
52
55
|
|
|
@@ -135,6 +138,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
135
138
|
email: true,
|
|
136
139
|
isVerified: true,
|
|
137
140
|
},
|
|
141
|
+
userPush: {
|
|
142
|
+
deviceToken: true,
|
|
143
|
+
deviceType: true,
|
|
144
|
+
isVerified: true,
|
|
145
|
+
},
|
|
138
146
|
},
|
|
139
147
|
props: {
|
|
140
148
|
isRoot: true,
|
|
@@ -585,6 +593,140 @@ export class Service extends DatabaseService<Model> {
|
|
|
585
593
|
},
|
|
586
594
|
});
|
|
587
595
|
}
|
|
596
|
+
|
|
597
|
+
// send push notification.
|
|
598
|
+
if (
|
|
599
|
+
notificationRuleItem.userPush?.deviceToken &&
|
|
600
|
+
notificationRuleItem.userPush?.isVerified
|
|
601
|
+
) {
|
|
602
|
+
// send push notification for alert
|
|
603
|
+
if (
|
|
604
|
+
options.userNotificationEventType ===
|
|
605
|
+
UserNotificationEventType.AlertCreated &&
|
|
606
|
+
alert
|
|
607
|
+
) {
|
|
608
|
+
// create a log.
|
|
609
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
610
|
+
logTimelineItem.statusMessage = `Sending push notification to device.`;
|
|
611
|
+
logTimelineItem.userPushId = notificationRuleItem.userPush.id!;
|
|
612
|
+
|
|
613
|
+
const updatedLog: UserOnCallLogTimeline =
|
|
614
|
+
await UserOnCallLogTimelineService.create({
|
|
615
|
+
data: logTimelineItem,
|
|
616
|
+
props: {
|
|
617
|
+
isRoot: true,
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const pushMessage: PushNotificationMessage =
|
|
622
|
+
PushNotificationUtil.createAlertCreatedNotification({
|
|
623
|
+
alertTitle: alert.title!,
|
|
624
|
+
projectName: alert.project?.name || "OneUptime",
|
|
625
|
+
alertViewLink: (
|
|
626
|
+
await AlertService.getAlertLinkInDashboard(
|
|
627
|
+
alert.projectId!,
|
|
628
|
+
alert.id!,
|
|
629
|
+
)
|
|
630
|
+
).toString(),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// send push notification.
|
|
634
|
+
PushNotificationService.sendPushNotification(
|
|
635
|
+
{
|
|
636
|
+
deviceTokens: [notificationRuleItem.userPush.deviceToken!],
|
|
637
|
+
message: pushMessage,
|
|
638
|
+
deviceType: notificationRuleItem.userPush.deviceType!,
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
projectId: options.projectId,
|
|
642
|
+
userOnCallLogTimelineId: updatedLog.id!,
|
|
643
|
+
},
|
|
644
|
+
).catch(async (err: Error) => {
|
|
645
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
646
|
+
id: updatedLog.id!,
|
|
647
|
+
data: {
|
|
648
|
+
status: UserNotificationStatus.Error,
|
|
649
|
+
statusMessage: err.message || "Error sending push notification.",
|
|
650
|
+
},
|
|
651
|
+
props: {
|
|
652
|
+
isRoot: true,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// send push notification for incident
|
|
659
|
+
if (
|
|
660
|
+
options.userNotificationEventType ===
|
|
661
|
+
UserNotificationEventType.IncidentCreated &&
|
|
662
|
+
incident
|
|
663
|
+
) {
|
|
664
|
+
// create a log.
|
|
665
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
666
|
+
logTimelineItem.statusMessage = `Sending push notification to device.`;
|
|
667
|
+
logTimelineItem.userPushId = notificationRuleItem.userPush.id!;
|
|
668
|
+
|
|
669
|
+
const updatedLog: UserOnCallLogTimeline =
|
|
670
|
+
await UserOnCallLogTimelineService.create({
|
|
671
|
+
data: logTimelineItem,
|
|
672
|
+
props: {
|
|
673
|
+
isRoot: true,
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const pushMessage: PushNotificationMessage =
|
|
678
|
+
PushNotificationUtil.createIncidentCreatedNotification({
|
|
679
|
+
incidentTitle: incident.title!,
|
|
680
|
+
projectName: incident.project?.name || "OneUptime",
|
|
681
|
+
incidentViewLink: (
|
|
682
|
+
await IncidentService.getIncidentLinkInDashboard(
|
|
683
|
+
incident.projectId!,
|
|
684
|
+
incident.id!,
|
|
685
|
+
)
|
|
686
|
+
).toString(),
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// send push notification.
|
|
690
|
+
PushNotificationService.sendPushNotification(
|
|
691
|
+
{
|
|
692
|
+
deviceTokens: [notificationRuleItem.userPush.deviceToken!],
|
|
693
|
+
message: pushMessage,
|
|
694
|
+
deviceType: notificationRuleItem.userPush.deviceType!,
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
projectId: options.projectId,
|
|
698
|
+
userOnCallLogTimelineId: updatedLog.id!,
|
|
699
|
+
},
|
|
700
|
+
).catch(async (err: Error) => {
|
|
701
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
702
|
+
id: updatedLog.id!,
|
|
703
|
+
data: {
|
|
704
|
+
status: UserNotificationStatus.Error,
|
|
705
|
+
statusMessage: err.message || "Error sending push notification.",
|
|
706
|
+
},
|
|
707
|
+
props: {
|
|
708
|
+
isRoot: true,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (
|
|
716
|
+
notificationRuleItem.userPush?.deviceToken &&
|
|
717
|
+
!notificationRuleItem.userPush?.isVerified
|
|
718
|
+
) {
|
|
719
|
+
// create a log.
|
|
720
|
+
logTimelineItem.status = UserNotificationStatus.Error;
|
|
721
|
+
logTimelineItem.statusMessage = `Push notification not sent because device is not verified.`;
|
|
722
|
+
|
|
723
|
+
await UserOnCallLogTimelineService.create({
|
|
724
|
+
data: logTimelineItem,
|
|
725
|
+
props: {
|
|
726
|
+
isRoot: true,
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
}
|
|
588
730
|
}
|
|
589
731
|
|
|
590
732
|
@CaptureSpan()
|
|
@@ -989,9 +1131,13 @@ export class Service extends DatabaseService<Model> {
|
|
|
989
1131
|
!createBy.data.userEmail &&
|
|
990
1132
|
!createBy.data.userSms &&
|
|
991
1133
|
!createBy.data.userSmsId &&
|
|
992
|
-
!createBy.data.userEmailId
|
|
1134
|
+
!createBy.data.userEmailId &&
|
|
1135
|
+
!createBy.data.userPushId &&
|
|
1136
|
+
!createBy.data.userPush
|
|
993
1137
|
) {
|
|
994
|
-
throw new BadDataException(
|
|
1138
|
+
throw new BadDataException(
|
|
1139
|
+
"Call, SMS, Email, or Push notification is required",
|
|
1140
|
+
);
|
|
995
1141
|
}
|
|
996
1142
|
|
|
997
1143
|
return {
|
|
@@ -9,6 +9,7 @@ import TeamMemberService from "./TeamMemberService";
|
|
|
9
9
|
import UserCallService from "./UserCallService";
|
|
10
10
|
import UserEmailService from "./UserEmailService";
|
|
11
11
|
import UserSmsService from "./UserSmsService";
|
|
12
|
+
import PushNotificationService from "./PushNotificationService";
|
|
12
13
|
import { CallRequestMessage } from "../../Types/Call/CallRequest";
|
|
13
14
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
14
15
|
import { EmailEnvelope } from "../../Types/Email/EmailMessage";
|
|
@@ -17,6 +18,7 @@ import NotificationSettingEventType from "../../Types/NotificationSetting/Notifi
|
|
|
17
18
|
import ObjectID from "../../Types/ObjectID";
|
|
18
19
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
19
20
|
import { SMSMessage } from "../../Types/SMS/SMS";
|
|
21
|
+
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
20
22
|
import UserCall from "../../Models/DatabaseModels/UserCall";
|
|
21
23
|
import UserEmail from "../../Models/DatabaseModels/UserEmail";
|
|
22
24
|
import UserNotificationSetting from "../../Models/DatabaseModels/UserNotificationSetting";
|
|
@@ -36,6 +38,7 @@ export class Service extends DatabaseService<UserNotificationSetting> {
|
|
|
36
38
|
emailEnvelope: EmailEnvelope;
|
|
37
39
|
smsMessage: SMSMessage;
|
|
38
40
|
callRequestMessage: CallRequestMessage;
|
|
41
|
+
pushNotificationMessage: PushNotificationMessage;
|
|
39
42
|
}): Promise<void> {
|
|
40
43
|
if (!data.projectId) {
|
|
41
44
|
throw new BadDataException(
|
|
@@ -54,6 +57,7 @@ export class Service extends DatabaseService<UserNotificationSetting> {
|
|
|
54
57
|
alertByEmail: true,
|
|
55
58
|
alertBySMS: true,
|
|
56
59
|
alertByCall: true,
|
|
60
|
+
alertByPush: true,
|
|
57
61
|
},
|
|
58
62
|
props: {
|
|
59
63
|
isRoot: true,
|
|
@@ -157,6 +161,22 @@ export class Service extends DatabaseService<UserNotificationSetting> {
|
|
|
157
161
|
});
|
|
158
162
|
}
|
|
159
163
|
}
|
|
164
|
+
|
|
165
|
+
if (notificationSettings.alertByPush) {
|
|
166
|
+
logger.debug(
|
|
167
|
+
`Sending push notification to user ${data.userId.toString()} for event ${data.eventType}`,
|
|
168
|
+
);
|
|
169
|
+
PushNotificationService.sendPushNotificationToUser(
|
|
170
|
+
data.userId,
|
|
171
|
+
data.projectId,
|
|
172
|
+
data.pushNotificationMessage,
|
|
173
|
+
{
|
|
174
|
+
projectId: data.projectId,
|
|
175
|
+
},
|
|
176
|
+
).catch((err: Error) => {
|
|
177
|
+
logger.error(err);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
160
180
|
}
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
2
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
3
|
+
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
|
4
|
+
import DatabaseService from "./DatabaseService";
|
|
5
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
6
|
+
import PositiveNumber from "../../Types/PositiveNumber";
|
|
7
|
+
import UserPush from "../../Models/DatabaseModels/UserPush";
|
|
8
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
9
|
+
|
|
10
|
+
export class Service extends DatabaseService<UserPush> {
|
|
11
|
+
public constructor() {
|
|
12
|
+
super(UserPush);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@CaptureSpan()
|
|
16
|
+
protected override async onBeforeCreate(
|
|
17
|
+
createBy: CreateBy<UserPush>,
|
|
18
|
+
): Promise<OnCreate<UserPush>> {
|
|
19
|
+
if (!createBy.data.deviceToken) {
|
|
20
|
+
throw new BadDataException("Device token is required");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!createBy.data.deviceType) {
|
|
24
|
+
throw new BadDataException("Device type is required");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Validate device type
|
|
28
|
+
const validDeviceTypes: string[] = ["web", "android", "ios"];
|
|
29
|
+
if (!validDeviceTypes.includes(createBy.data.deviceType)) {
|
|
30
|
+
throw new BadDataException(
|
|
31
|
+
"Device type must be one of: " + validDeviceTypes.join(", "),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if this device token already exists for this user and project
|
|
36
|
+
const existingCount: PositiveNumber = await this.countBy({
|
|
37
|
+
query: {
|
|
38
|
+
deviceToken: createBy.data.deviceToken,
|
|
39
|
+
userId: createBy.data.userId!,
|
|
40
|
+
projectId: createBy.data.projectId!,
|
|
41
|
+
},
|
|
42
|
+
props: {
|
|
43
|
+
isRoot: true,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (existingCount.toNumber() > 0) {
|
|
48
|
+
throw new BadDataException(
|
|
49
|
+
"This device is already registered for push notifications",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { carryForward: null, createBy };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@CaptureSpan()
|
|
57
|
+
protected override async onBeforeDelete(
|
|
58
|
+
deleteBy: DeleteBy<UserPush>,
|
|
59
|
+
): Promise<OnDelete<UserPush>> {
|
|
60
|
+
// Add any cleanup logic here if needed
|
|
61
|
+
return { carryForward: null, deleteBy };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@CaptureSpan()
|
|
65
|
+
public async verifyDevice(deviceId: string): Promise<void> {
|
|
66
|
+
await this.updateOneBy({
|
|
67
|
+
query: {
|
|
68
|
+
_id: deviceId,
|
|
69
|
+
},
|
|
70
|
+
data: {
|
|
71
|
+
isVerified: true,
|
|
72
|
+
},
|
|
73
|
+
props: {
|
|
74
|
+
isRoot: true,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@CaptureSpan()
|
|
80
|
+
public async unverifyDevice(deviceId: string): Promise<void> {
|
|
81
|
+
await this.updateOneBy({
|
|
82
|
+
query: {
|
|
83
|
+
_id: deviceId,
|
|
84
|
+
},
|
|
85
|
+
data: {
|
|
86
|
+
isVerified: false,
|
|
87
|
+
},
|
|
88
|
+
props: {
|
|
89
|
+
isRoot: true,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default new Service();
|