1+ /**
2+ * Imports for web push, Firebase messaging, Prisma ORM, and type definitions.
3+ */
14import webpush , { WebPushError } from 'web-push'
25import { messaging } from './firebase' // Adjust as needed
3- import { PrismaClient , Subscription } from '@prisma/client'
6+ import { Subscription } from '@prisma/client'
47import { JsonValue } from '@prisma/client/runtime/library'
8+ import prisma from '@/lib/db'
59
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+ */
5520export async function sendNotification (
5621 subscription : SubscriptionRecord ,
5722 notificationPayload : NotificationPayload
5823) : Promise < void > {
5924 try {
6025 console . log ( 'Sending notification to subscription:' , subscription )
6126
27+ // Handle browser-based web push notifications.
6228 if ( subscription . type === 'web' ) {
29+ // Validate that the subscription keys match the structure required by web push.
6330 if ( ! isWebKeys ( subscription . keys ) ) {
6431 throw new Error ( `Invalid keys for web subscription: ${ JSON . stringify ( subscription . keys ) } ` )
6532 }
6633
34+ // Construct and send a web push notification.
6735 await webpush . sendNotification (
6836 {
6937 endpoint : subscription . endpoint ,
@@ -80,11 +48,15 @@ export async function sendNotification(
8048 url : notificationPayload . url ,
8149 } )
8250 )
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.
8455 if ( ! isFcmKeys ( subscription . keys ) ) {
8556 throw new Error ( `Invalid keys for FCM subscription: ${ JSON . stringify ( subscription . keys ) } ` )
8657 }
8758
59+ // Construct the FCM message.
8860 const fcmMessage = {
8961 notification : {
9062 title : notificationPayload . title ,
@@ -96,11 +68,17 @@ export async function sendNotification(
9668 token : subscription . keys . token ,
9769 }
9870
71+ // Send the FCM message using Firebase.
9972 const response = await messaging . send ( fcmMessage )
10073 console . log ( 'FCM response:' , response )
10174 }
10275 } 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+ */
10380 if ( subscription . type === 'web' && error instanceof WebPushError ) {
81+ // Status code 410 indicates that the subscription is no longer valid.
10482 if ( error . statusCode === 410 ) {
10583 await prisma . subscription . delete ( { where : { id : subscription . id } } )
10684 console . log ( `Subscription with id ${ subscription . id } removed due to expiration.` )
@@ -109,25 +87,96 @@ export async function sendNotification(
10987 `Failed to send notification to subscription id ${ subscription . id } :` ,
11088 error . statusCode ,
11189 error . body
112- ) //error()
90+ )
11391 }
11492 } else if ( subscription . type === 'fcm' ) {
115- // Handle FCM-specific errors
93+ // These error codes indicate an invalid or unregistered FCM token.
11694 if (
11795 error . code === 'messaging/invalid-registration-token' ||
11896 error . code === 'messaging/registration-token-not-registered'
11997 ) {
120- // Remove the invalid token from the database
12198 await prisma . subscription . delete ( { where : { id : subscription . id } } )
12299 console . log ( `Removed invalid FCM token for subscription id ${ subscription . id } .` )
123100 } 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 )
125102 }
126103 } else {
127104 console . log (
128105 `An error occurred while sending notification to subscription id ${ subscription . id } :` ,
129106 error
130- ) //error()
107+ )
131108 }
132109 }
133110}
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