@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.
Files changed (121) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/UserNotificationRule.ts +47 -0
  3. package/Models/DatabaseModels/UserNotificationSetting.ts +16 -0
  4. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +47 -0
  5. package/Models/DatabaseModels/UserPush.ts +302 -0
  6. package/Server/API/UserPushAPI.ts +302 -0
  7. package/Server/EnvironmentConfig.ts +10 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1752659054949-MigrationName.ts +87 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1752774923063-MigrationName.ts +27 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1753109689244-MigrationName.ts +41 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  12. package/Server/Services/MonitorService.ts +14 -0
  13. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +14 -0
  14. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +15 -0
  15. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +14 -0
  16. package/Server/Services/OnCallDutyPolicyScheduleService.ts +29 -0
  17. package/Server/Services/ProbeService.ts +23 -0
  18. package/Server/Services/PushNotificationService.ts +253 -0
  19. package/Server/Services/UserNotificationRuleService.ts +148 -2
  20. package/Server/Services/UserNotificationSettingService.ts +20 -0
  21. package/Server/Services/UserPushService.ts +95 -0
  22. package/Server/Utils/PushNotificationUtil.ts +308 -0
  23. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +1 -1
  24. package/Types/PushNotification/PushNotificationMessage.ts +18 -0
  25. package/Types/PushNotification/PushNotificationRequest.ts +22 -0
  26. package/UI/Components/Button/Button.tsx +9 -9
  27. package/UI/Components/Card/Card.tsx +2 -2
  28. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  29. package/UI/Components/Date/StartAndEndDate.tsx +8 -3
  30. package/UI/Components/HeaderAlert/HeaderAlert.tsx +9 -1
  31. package/UI/Components/HeaderAlert/HeaderModelAlert.tsx +2 -1
  32. package/UI/Components/Modal/Modal.tsx +16 -14
  33. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  34. package/UI/Components/Page/Page.tsx +1 -1
  35. package/UI/Components/Table/Table.tsx +48 -25
  36. package/UI/Components/Table/TableBody.tsx +52 -0
  37. package/UI/Components/Table/TableRow.tsx +221 -0
  38. package/UI/Config.ts +3 -0
  39. package/UI/Utils/Cookie.ts +1 -3
  40. package/UI/Utils/LocalStorage.ts +1 -3
  41. package/UI/Utils/SessionStorage.ts +1 -3
  42. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  43. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  44. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +48 -0
  45. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +18 -0
  47. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  48. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +48 -0
  49. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  50. package/build/dist/Models/DatabaseModels/UserPush.js +324 -0
  51. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -0
  52. package/build/dist/Server/API/UserPushAPI.js +175 -0
  53. package/build/dist/Server/API/UserPushAPI.js.map +1 -0
  54. package/build/dist/Server/EnvironmentConfig.js +4 -0
  55. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752659054949-MigrationName.js +36 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752659054949-MigrationName.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752774923063-MigrationName.js +16 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1752774923063-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1753109689244-MigrationName.js +20 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1753109689244-MigrationName.js.map +1 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  64. package/build/dist/Server/Services/MonitorService.js +12 -0
  65. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  66. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +13 -4
  67. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  68. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +13 -4
  69. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  70. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +12 -3
  71. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  72. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +22 -0
  73. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  74. package/build/dist/Server/Services/ProbeService.js +12 -1
  75. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  76. package/build/dist/Server/Services/PushNotificationService.js +172 -0
  77. package/build/dist/Server/Services/PushNotificationService.js.map +1 -0
  78. package/build/dist/Server/Services/UserNotificationRuleService.js +108 -3
  79. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  80. package/build/dist/Server/Services/UserNotificationSettingService.js +10 -0
  81. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  82. package/build/dist/Server/Services/UserPushService.js +102 -0
  83. package/build/dist/Server/Services/UserPushService.js.map +1 -0
  84. package/build/dist/Server/Utils/PushNotificationUtil.js +222 -0
  85. package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -0
  86. package/build/dist/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.js +1 -1
  87. package/build/dist/Types/PushNotification/PushNotificationMessage.js +2 -0
  88. package/build/dist/Types/PushNotification/PushNotificationMessage.js.map +1 -0
  89. package/build/dist/Types/PushNotification/PushNotificationRequest.js +2 -0
  90. package/build/dist/Types/PushNotification/PushNotificationRequest.js.map +1 -0
  91. package/build/dist/UI/Components/Button/Button.js +9 -9
  92. package/build/dist/UI/Components/Card/Card.js +2 -2
  93. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  94. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  95. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  96. package/build/dist/UI/Components/Date/StartAndEndDate.js +8 -8
  97. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  98. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +2 -1
  99. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  100. package/build/dist/UI/Components/HeaderAlert/HeaderModelAlert.js +1 -1
  101. package/build/dist/UI/Components/HeaderAlert/HeaderModelAlert.js.map +1 -1
  102. package/build/dist/UI/Components/Modal/Modal.js +14 -14
  103. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  104. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  105. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  106. package/build/dist/UI/Components/Page/Page.js +1 -1
  107. package/build/dist/UI/Components/Table/Table.js +29 -15
  108. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  109. package/build/dist/UI/Components/Table/TableBody.js +19 -1
  110. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  111. package/build/dist/UI/Components/Table/TableRow.js +68 -0
  112. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  113. package/build/dist/UI/Config.js +2 -0
  114. package/build/dist/UI/Config.js.map +1 -1
  115. package/build/dist/UI/Utils/Cookie.js +1 -3
  116. package/build/dist/UI/Utils/Cookie.js.map +1 -1
  117. package/build/dist/UI/Utils/LocalStorage.js +1 -3
  118. package/build/dist/UI/Utils/LocalStorage.js.map +1 -1
  119. package/build/dist/UI/Utils/SessionStorage.js +1 -3
  120. package/build/dist/UI/Utils/SessionStorage.js.map +1 -1
  121. 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("Call, SMS, or Email is required");
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();