From de3d9fce9721650059e91d90ff6e32d83ea43471 Mon Sep 17 00:00:00 2001 From: Gian Lazzarini Date: Sat, 1 Jun 2024 14:24:20 -0700 Subject: [PATCH] handle and use web push notification subscription object --- src/dal/entity/userDevice.entity.ts | 4 ++ .../postgres/.snapshot-postgres.json | 9 ++++ .../postgres/Migration20240601210639.ts | 13 ++++++ src/notification/notification.service.ts | 42 ++++++++++++++++--- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/dal/migrations/postgres/Migration20240601210639.ts diff --git a/src/dal/entity/userDevice.entity.ts b/src/dal/entity/userDevice.entity.ts index f454eac3f..ae63f3027 100644 --- a/src/dal/entity/userDevice.entity.ts +++ b/src/dal/entity/userDevice.entity.ts @@ -1,6 +1,7 @@ import { Entity, IdentifiedReference, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/core'; import { Field, ID, ObjectType } from '@nestjs/graphql'; import { User } from './user.entity'; +import webpush from 'web-push'; @ObjectType() @Entity() @@ -14,6 +15,9 @@ export class UserDevice { @Property({ fieldName: 'fcmPushUserToken' }) public fcmPushUserToken!: string; + @Property({ type: 'json', nullable: true }) + webPushSubscription?: webpush.PushSubscription; + @ManyToOne({ entity: () => User, fieldName: 'userId', diff --git a/src/dal/migrations/postgres/.snapshot-postgres.json b/src/dal/migrations/postgres/.snapshot-postgres.json index 3e3eb3ec5..8ae2dcc68 100644 --- a/src/dal/migrations/postgres/.snapshot-postgres.json +++ b/src/dal/migrations/postgres/.snapshot-postgres.json @@ -1480,6 +1480,15 @@ "nullable": false, "mappedType": "string" }, + "web_push_subscription": { + "name": "web_push_subscription", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, "userId": { "name": "userId", "type": "int", diff --git a/src/dal/migrations/postgres/Migration20240601210639.ts b/src/dal/migrations/postgres/Migration20240601210639.ts new file mode 100644 index 000000000..a01902e13 --- /dev/null +++ b/src/dal/migrations/postgres/Migration20240601210639.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240601210639 extends Migration { + + async up(): Promise { + this.addSql('alter table "user_device" add column "web_push_subscription" jsonb null;'); + } + + async down(): Promise { + this.addSql('alter table "user_device" drop column "web_push_subscription";'); + } + +} diff --git a/src/notification/notification.service.ts b/src/notification/notification.service.ts index fc2d6a0b7..92e10abc6 100644 --- a/src/notification/notification.service.ts +++ b/src/notification/notification.service.ts @@ -14,6 +14,7 @@ import { import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import webpush from 'web-push'; +import _ from 'lodash'; @Service() export class NotificationService { @@ -76,6 +77,25 @@ export class NotificationService { } } + public async addUserWebPushNotificationSubscription( + userId: any, + subscription: webpush.PushSubscription, + ): Promise { + this.logger.debug(this.addUserFcmNotificationToken.name); + const user = await this.userRepository.findOne({ id: userId }); + + if (!(await user.userDevices.loadItems()).find((x) => _.isEqual(x.webPushSubscription, subscription))) { + const userDevice = this.userDeviceRepository.create({ + user: { id: user.id }, + webPushSubscription: subscription, + }); + await this.userDeviceRepository.persistAndFlush(userDevice); + // TODO notify via email that a new device has been used on the account for security. + } else { + this.logger.warn('User device web push notification subscription already stored.'); + } + } + /** * Fetch a users in app notifications, with optional pagination * @param userId users id @@ -132,21 +152,31 @@ export class NotificationService { notification: PushNotificationDto, ): Promise { this.logger.debug(this.sendPushToUser.name); - const user = await this.userRepository.findOne({ id: userId }); + + // native push notifications const fcmUserTokens = (await user.userDevices.loadItems()).map( (x) => x.fcmPushUserToken, ); - this.logger.debug( `${fcmUserTokens.length} push notification tokens found for userId: ${userId}`, ); - for (const iterator of fcmUserTokens) { await this.sendPushNotification(notification, iterator); - this.logger.debug(`Sent push notification to fcmToken ${iterator}`); } + + // web push notifications + const webPushSubscriptions = (await user.userDevices.loadItems()).map( + (x) => x.webPushSubscription, + ); + this.logger.debug( + `${webPushSubscriptions.length} web push notification subscriptions found for userId: ${userId}`, + ); + for (const subscription of webPushSubscriptions) { + await this.sendWebPushNotification(notification, subscription); + this.logger.debug(`Sent web push notification to subscription: ${JSON.stringify(subscription)}`); + } } private async sendPushNotification( @@ -173,12 +203,12 @@ export class NotificationService { private async sendWebPushNotification( notification: PushNotificationDto, - to: string, + to: webpush.PushSubscription, ) { this.logger.debug(this.sendWebPushNotification.name); webpush .sendNotification( - subscription, + to, JSON.stringify({ notification }),