1
+ /**
2
+ * Imports for web push, Firebase messaging, Prisma ORM, and type definitions.
3
+ */
1
4
import webpush , { WebPushError } from 'web-push'
2
5
import { messaging } from './firebase' // Adjust as needed
3
- import { PrismaClient , Subscription } from '@prisma/client'
6
+ import { Subscription } from '@prisma/client'
4
7
import { JsonValue } from '@prisma/client/runtime/library'
8
+ import prisma from '@/lib/db'
5
9
6
- const prisma = new PrismaClient ( )
7
-
8
- // Interfaces extending the Prisma Subscription type
9
- export interface WebSubscription extends Subscription {
10
- type : 'web'
11
- keys : JsonValue // Represents { auth: string; p256dh: string } as JsonValue
12
- }
13
-
14
- // Interface extending the Prisma Subscription type
15
- export interface FcmSubscription extends Subscription {
16
- type : 'fcm'
17
- keys : JsonValue // Represents { token: string } as JsonValue
18
- }
19
-
20
- // Union type covering both subscription types
21
- export type SubscriptionRecord = WebSubscription | FcmSubscription
22
-
23
- // Notification payload interface
24
- interface NotificationPayload {
25
- title : string
26
- body : string
27
- url : string
28
- icon ?: string
29
- badge ?: string
30
- }
31
-
32
- // Helper function to validate web keys
33
- export function isWebKeys ( keys : JsonValue ) : keys is { auth : string ; p256dh : string } {
34
- return (
35
- typeof keys === 'object' &&
36
- keys !== null &&
37
- 'auth' in keys &&
38
- 'p256dh' in keys &&
39
- typeof ( keys as any ) . auth === 'string' &&
40
- typeof ( keys as any ) . p256dh === 'string'
41
- )
42
- }
43
-
44
- // Helper function to validate FCM keys
45
- export function isFcmKeys ( keys : JsonValue ) : keys is { token : string } {
46
- return (
47
- typeof keys === 'object' &&
48
- keys !== null &&
49
- 'token' in keys &&
50
- typeof ( keys as any ) . token === 'string'
51
- )
52
- }
53
-
54
- // Unified function to send notifications
10
+ /**
11
+ * Sends a push notification to a subscription using either web push (VAPID) or FCM.
12
+ *
13
+ * @param subscription - The subscription record, which can be either WebSubscription or FcmSubscription.
14
+ * @param notificationPayload - The notification payload containing title, body, url, etc.
15
+ *
16
+ * If the subscription type is 'web', it will send a web push notification.
17
+ * If the subscription type is 'fcm', it will send an FCM notification.
18
+ * If the subscription is invalid (e.g., expired or unregistered), it will be removed from the database.
19
+ */
55
20
export async function sendNotification (
56
21
subscription : SubscriptionRecord ,
57
22
notificationPayload : NotificationPayload
58
23
) : Promise < void > {
59
24
try {
60
25
console . log ( 'Sending notification to subscription:' , subscription )
61
26
27
+ // Handle browser-based web push notifications.
62
28
if ( subscription . type === 'web' ) {
29
+ // Validate that the subscription keys match the structure required by web push.
63
30
if ( ! isWebKeys ( subscription . keys ) ) {
64
31
throw new Error ( `Invalid keys for web subscription: ${ JSON . stringify ( subscription . keys ) } ` )
65
32
}
66
33
34
+ // Construct and send a web push notification.
67
35
await webpush . sendNotification (
68
36
{
69
37
endpoint : subscription . endpoint ,
@@ -80,11 +48,15 @@ export async function sendNotification(
80
48
url : notificationPayload . url ,
81
49
} )
82
50
)
83
- } else if ( subscription . type === 'fcm' ) {
51
+ }
52
+ // Handle mobile push notifications using Firebase Cloud Messaging.
53
+ else if ( subscription . type === 'fcm' ) {
54
+ // Validate that the subscription keys match the structure required by FCM.
84
55
if ( ! isFcmKeys ( subscription . keys ) ) {
85
56
throw new Error ( `Invalid keys for FCM subscription: ${ JSON . stringify ( subscription . keys ) } ` )
86
57
}
87
58
59
+ // Construct the FCM message.
88
60
const fcmMessage = {
89
61
notification : {
90
62
title : notificationPayload . title ,
@@ -96,11 +68,17 @@ export async function sendNotification(
96
68
token : subscription . keys . token ,
97
69
}
98
70
71
+ // Send the FCM message using Firebase.
99
72
const response = await messaging . send ( fcmMessage )
100
73
console . log ( 'FCM response:' , response )
101
74
}
102
75
} catch ( error ) {
76
+ /**
77
+ * Handle errors appropriately. Depending on the type of subscription,
78
+ * remove invalid or expired subscriptions from the database if necessary.
79
+ */
103
80
if ( subscription . type === 'web' && error instanceof WebPushError ) {
81
+ // Status code 410 indicates that the subscription is no longer valid.
104
82
if ( error . statusCode === 410 ) {
105
83
await prisma . subscription . delete ( { where : { id : subscription . id } } )
106
84
console . log ( `Subscription with id ${ subscription . id } removed due to expiration.` )
@@ -109,25 +87,96 @@ export async function sendNotification(
109
87
`Failed to send notification to subscription id ${ subscription . id } :` ,
110
88
error . statusCode ,
111
89
error . body
112
- ) //error()
90
+ )
113
91
}
114
92
} else if ( subscription . type === 'fcm' ) {
115
- // Handle FCM-specific errors
93
+ // These error codes indicate an invalid or unregistered FCM token.
116
94
if (
117
95
error . code === 'messaging/invalid-registration-token' ||
118
96
error . code === 'messaging/registration-token-not-registered'
119
97
) {
120
- // Remove the invalid token from the database
121
98
await prisma . subscription . delete ( { where : { id : subscription . id } } )
122
99
console . log ( `Removed invalid FCM token for subscription id ${ subscription . id } .` )
123
100
} else {
124
- console . log ( `Failed to send FCM notification to subscription id ${ subscription . id } :` , error ) //error()
101
+ console . log ( `Failed to send FCM notification to subscription id ${ subscription . id } :` , error )
125
102
}
126
103
} else {
127
104
console . log (
128
105
`An error occurred while sending notification to subscription id ${ subscription . id } :` ,
129
106
error
130
- ) //error()
107
+ )
131
108
}
132
109
}
133
110
}
111
+
112
+ /**
113
+ * An interface that extends the Prisma Subscription model for 'web' subscriptions.
114
+ * This includes the additional `type` and `keys` fields relevant to web push subscriptions.
115
+ */
116
+ export interface WebSubscription extends Subscription {
117
+ type : 'web'
118
+ // The keys stored as JsonValue actually correspond to { auth: string; p256dh: string }
119
+ keys : JsonValue
120
+ }
121
+
122
+ /**
123
+ * An interface that extends the Prisma Subscription model for 'fcm' subscriptions.
124
+ * This includes the additional `type` and `keys` fields relevant to Firebase Cloud Messaging subscriptions.
125
+ */
126
+ export interface FcmSubscription extends Subscription {
127
+ type : 'fcm'
128
+ // The keys stored as JsonValue actually correspond to { token: string }
129
+ keys : JsonValue
130
+ }
131
+
132
+ /**
133
+ * A union type that represents either a WebSubscription or an FcmSubscription.
134
+ * Used for handling either type of push subscription in a unified way.
135
+ */
136
+ export type SubscriptionRecord = WebSubscription | FcmSubscription
137
+
138
+ /**
139
+ * The payload for the notification, including title, body, url, icon, and badge.
140
+ * Used for constructing messages for both web push and FCM.
141
+ */
142
+ interface NotificationPayload {
143
+ title : string
144
+ body : string
145
+ url : string
146
+ icon ?: string
147
+ badge ?: string
148
+ }
149
+
150
+ /**
151
+ * Type guard to check if the given JsonValue matches
152
+ * the structure required by a web push subscription ({ auth, p256dh }).
153
+ *
154
+ * @param keys The JsonValue that should contain web push keys.
155
+ * @returns True if the object has valid auth and p256dh strings; otherwise, false.
156
+ */
157
+ export function isWebKeys ( keys : JsonValue ) : keys is { auth : string ; p256dh : string } {
158
+ return (
159
+ typeof keys === 'object' &&
160
+ keys !== null &&
161
+ 'auth' in keys &&
162
+ 'p256dh' in keys &&
163
+ typeof ( keys as any ) . auth === 'string' &&
164
+ typeof ( keys as any ) . p256dh === 'string'
165
+ )
166
+ }
167
+
168
+ /**
169
+ * Type guard to check if the given JsonValue matches
170
+ * the structure required by an FCM subscription ({ token }).
171
+ *
172
+ * @param keys The JsonValue that should contain the FCM token.
173
+ * @returns True if the object has a valid token string; otherwise, false.
174
+ */
175
+ export function isFcmKeys ( keys : JsonValue ) : keys is { token : string } {
176
+ return (
177
+ typeof keys === 'object' &&
178
+ keys !== null &&
179
+ 'token' in keys &&
180
+ typeof ( keys as any ) . token === 'string'
181
+ )
182
+ }
0 commit comments