From 906d45f6864ff0bd58ea0503a9e5f67f028ea1f8 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Fri, 16 Jul 2021 20:52:43 +0200 Subject: [PATCH 01/12] feat: add basic logic for push notifications --- .eslintrc.js | 2 + packages/api/.env.example | 5 + packages/api/package.json | 4 +- packages/api/src/main.ts | 2 +- packages/api/src/modules/app.module.ts | 2 + .../api/src/modules/config/config-loader.ts | 4 + packages/api/src/modules/config/config.ts | 5 + .../push-notifications.controller.ts | 20 +++ .../push-notifications.module.ts | 12 ++ .../push-notifications.service.spec.ts | 19 ++ .../push-notifications.service.ts | 48 +++++ .../push-notifications.types.d.ts | 29 +++ .../src/modules/repository/base.repository.ts | 3 +- packages/client/.env.example | 8 + packages/client/package.json | 26 +-- packages/client/public/index.html | 2 +- packages/client/public/manifest.json | 11 +- packages/client/src/i18n/index.ts | 4 +- packages/client/src/index.tsx | 2 +- .../src/providers/ThemeTypeProvider/index.tsx | 16 +- .../src/providers/api/AxiosProvider/index.tsx | 17 +- .../src/{serviceWorker => }/service-worker.ts | 9 +- .../serviceWorkerRegistration.ts | 45 ++++- packages/client/tsconfig.json | 1 + yarn.lock | 170 +++++++----------- 25 files changed, 326 insertions(+), 140 deletions(-) create mode 100644 packages/api/src/modules/push-notifications/push-notifications.controller.ts create mode 100644 packages/api/src/modules/push-notifications/push-notifications.module.ts create mode 100644 packages/api/src/modules/push-notifications/push-notifications.service.spec.ts create mode 100644 packages/api/src/modules/push-notifications/push-notifications.service.ts create mode 100644 packages/api/src/modules/push-notifications/push-notifications.types.d.ts rename packages/client/src/{serviceWorker => }/service-worker.ts (92%) rename packages/client/src/{serviceWorker => }/serviceWorkerRegistration.ts (81%) diff --git a/.eslintrc.js b/.eslintrc.js index 863a4bad..b022117e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,8 @@ module.exports = { '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/no-unsafe-member-access': 0, '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/no-unsafe-call': 0, + '@typescript-eslint/no-unsafe-return': 0, '@typescript-eslint/no-unused-expressions': [2, { allowShortCircuit: true }], '@typescript-eslint/no-floating-promises': [2, { ignoreIIFE: true, ignoreVoid: true }], '@typescript-eslint/naming-convention': [ diff --git a/packages/api/.env.example b/packages/api/.env.example index d436434b..dff2ea10 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -8,6 +8,11 @@ SUPER_ADMIN_EMAILS=example1@mail.com;example2@mail.com RATE_LIMIT_MIN_TIME=60 RATE_LIMIT_MAX_CONCURRENT=10 +# Push notifications +# For more details check: https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey +PUSH_NOTIFICATION_PUBLIC_VAPID_KEY= +PUSH_NOTIFICATION_PRIVATE_VAPID_KEY= + # Database configuration DB_DATABASE=smart-gate-db DB_DATABASE_TEST=smart-gate-db-test diff --git a/packages/api/package.json b/packages/api/package.json index 1138ca1c..76fccb8b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -45,6 +45,7 @@ "@types/nodemailer": "^6.4.2", "@types/socket.io": "^3.0.2", "@types/uuid": "^8.3.0", + "@types/web-push": "^3.3.2", "bcrypt": "^5.0.1", "cache-manager": "^3.4.4", "class-transformer": "^0.4.0", @@ -63,7 +64,8 @@ "tsconfig-paths": "^3.9.0", "typeorm": "^0.2.34", "typescript": "^4.3.2", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "web-push": "^3.4.5" }, "devDependencies": { "@nestjs/testing": "^7.6.17", diff --git a/packages/api/src/main.ts b/packages/api/src/main.ts index 114dddda..7b03a8cf 100644 --- a/packages/api/src/main.ts +++ b/packages/api/src/main.ts @@ -15,7 +15,7 @@ const bootstrap = async (): Promise => { const config = app.get(Config); app.enableCors({ - origin: config.clientUrl, + origin: config.clientUrl.split(';'), credentials: true, }); app.use(cookieParser(config.cookie.secret)); diff --git a/packages/api/src/modules/app.module.ts b/packages/api/src/modules/app.module.ts index 2687916a..6a8ee66e 100644 --- a/packages/api/src/modules/app.module.ts +++ b/packages/api/src/modules/app.module.ts @@ -8,6 +8,7 @@ import { DatabaseModule } from './database/database.module'; import { InvitationsModule } from './invitations/invitations.module'; import { MailerModule } from './mailer/mailer.module'; import { PasswordResetModule } from './password-reset/password-reset.module'; +import { PushNotificationsModule } from './push-notifications/push-notifications.module'; import { RepositoryModule } from './repository/repository.module'; import { SentryModule } from './sentry/sentry.module'; import { TicketModule } from './ticket/ticket.module'; @@ -35,6 +36,7 @@ import { WebsocketModule } from './websocket/websocket.module'; RepositoryModule, WebsocketModule, TicketModule, + PushNotificationsModule, ], }) export class AppModule {} diff --git a/packages/api/src/modules/config/config-loader.ts b/packages/api/src/modules/config/config-loader.ts index 627b4682..b6fac9cf 100644 --- a/packages/api/src/modules/config/config-loader.ts +++ b/packages/api/src/modules/config/config-loader.ts @@ -29,6 +29,10 @@ export class ConfigLoader { firstName: this.envConfigService.get('TEST_USER_FIRSTNAME', isTest), lastName: this.envConfigService.get('TEST_USER_LASTNAME', isTest), }, + pushNotifications: { + publicVapidKey: this.envConfigService.get('PUSH_NOTIFICATION_PUBLIC_VAPID_KEY'), + privateVapidKey: this.envConfigService.get('PUSH_NOTIFICATION_PRIVATE_VAPID_KEY'), + }, rateLimiter: { minTime: this.envConfigService.get('RATE_LIMIT_MIN_TIME', isProd, Number), maxConcurrent: this.envConfigService.get('RATE_LIMIT_MAX_CONCURRENT', isProd, Number), diff --git a/packages/api/src/modules/config/config.ts b/packages/api/src/modules/config/config.ts index e582084e..96881db9 100644 --- a/packages/api/src/modules/config/config.ts +++ b/packages/api/src/modules/config/config.ts @@ -16,6 +16,11 @@ export class Config { lastName: string | undefined; }; + pushNotifications: { + publicVapidKey: string; + privateVapidKey: string; + }; + database: { database: string; databaseTest: string; diff --git a/packages/api/src/modules/push-notifications/push-notifications.controller.ts b/packages/api/src/modules/push-notifications/push-notifications.controller.ts new file mode 100644 index 00000000..e2013051 --- /dev/null +++ b/packages/api/src/modules/push-notifications/push-notifications.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { PushSubscription } from 'web-push'; + +import { PushNotificationsService } from './push-notifications.service'; + +// @Auth() +@Controller('push-notifications') +export class PushNotificationsController { + constructor(private readonly pushNotificationsService: PushNotificationsService) {} + + @Post() + subscribe(@Body() subscription: PushSubscription) { + this.pushNotificationsService.subscribe(subscription); + } + + @Get() + async send() { + await this.pushNotificationsService.send(); + } +} diff --git a/packages/api/src/modules/push-notifications/push-notifications.module.ts b/packages/api/src/modules/push-notifications/push-notifications.module.ts new file mode 100644 index 00000000..68c514fb --- /dev/null +++ b/packages/api/src/modules/push-notifications/push-notifications.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { ConfigModule } from '../config/config.module'; +import { PushNotificationsController } from './push-notifications.controller'; +import { PushNotificationsService } from './push-notifications.service'; + +@Module({ + imports: [ConfigModule], + providers: [PushNotificationsService], + controllers: [PushNotificationsController], +}) +export class PushNotificationsModule {} diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.spec.ts b/packages/api/src/modules/push-notifications/push-notifications.service.spec.ts new file mode 100644 index 00000000..7cf7b6e0 --- /dev/null +++ b/packages/api/src/modules/push-notifications/push-notifications.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { PushNotificationsService } from './push-notifications.service'; + +describe('pushNotificationsService', () => { + let service: PushNotificationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PushNotificationsService], + }).compile(); + + service = module.get(PushNotificationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts new file mode 100644 index 00000000..e625c38d --- /dev/null +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger } from '@nestjs/common'; +import webPush, { PushSubscription } from 'web-push'; + +import { Config } from '../config/config'; + +@Injectable() +export class PushNotificationsService { + private subs: Array = []; + + private logger: Logger = new Logger('PushNotifications'); + + constructor(private readonly config: Config) { + const { pushNotifications, mailer } = config; + const { publicVapidKey, privateVapidKey } = pushNotifications; + const { replyTo } = mailer; + + webPush.setVapidDetails(`mailto:${replyTo}`, publicVapidKey, privateVapidKey); + } + + subscribe(subscription: PushSubscription): void { + this.logger.log('New push subscriber'); + this.subs.push(subscription); + this.logger.log(`${this.subs.length} subscriptions`); + } + + async send(): Promise { + this.logger.log('Sending...'); + + if (!this.subs.length) { + this.logger.log('No subscribers'); + return; + } + + const payload = JSON.stringify({ + title: 'Push Test', + options: { + body: 'Smart Gate notification', + }, + } as PushNotificationPayload); + + const promises = this.subs.map((subscription) => + webPush.sendNotification(subscription, payload), + ); + + await Promise.all(promises); + this.logger.log('Sent'); + } +} diff --git a/packages/api/src/modules/push-notifications/push-notifications.types.d.ts b/packages/api/src/modules/push-notifications/push-notifications.types.d.ts new file mode 100644 index 00000000..583de082 --- /dev/null +++ b/packages/api/src/modules/push-notifications/push-notifications.types.d.ts @@ -0,0 +1,29 @@ +interface PushNotificationOptions { + dir?: PushNotificationDirection; + lang?: string; + body?: string; + tag?: string; + image?: string; + icon?: string; + badge?: string; + sound?: string; + vibrate?: number | number[]; + timestamp?: number; + renotify?: boolean; + silent?: boolean; + requireInteraction?: boolean; + actions?: PushNotificationAction[]; +} + +interface PushNotificationPayload { + title: string; + options: PushNotificationOptions; +} + +type PushNotificationDirection = 'auto' | 'ltr' | 'rtl'; + +interface PushNotificationAction { + action: string; + title: string; + icon?: string; +} diff --git a/packages/api/src/modules/repository/base.repository.ts b/packages/api/src/modules/repository/base.repository.ts index b2f11283..13debcc3 100644 --- a/packages/api/src/modules/repository/base.repository.ts +++ b/packages/api/src/modules/repository/base.repository.ts @@ -39,8 +39,7 @@ export const BaseRepository = ( try { return await this.repository.save(dataToCreate); } catch (err) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Cannot create entity with data ${dataToCreate}`); + throw new Error(`Cannot create entity with data ${JSON.stringify(dataToCreate)}`); } } diff --git a/packages/client/.env.example b/packages/client/.env.example index 45cab8df..42ec0db9 100644 --- a/packages/client/.env.example +++ b/packages/client/.env.example @@ -13,6 +13,10 @@ PORT=8080 # API REACT_APP_API_URL=http://localhost:3030 +REACT_APP_FORCE_API_URL=false + +# I18N +REACT_APP_I18N_DEBUG=false # Sentry REACT_APP_SENTRY_DSN=dns_url @@ -20,3 +24,7 @@ REACT_APP_SENTRY_DEBUG=false REACT_APP_SENTRY_ENABLED=false REACT_APP_SENTRY_ENVIRONMENT=development REACT_APP_SENTRY_TRACES_SAMPLE=0 + +# Push notifications +# For more details check: https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey +PUSH_NOTIFICATION_PUBLIC_VAPID_KEY= diff --git a/packages/client/package.json b/packages/client/package.json index 0de58d3b..1f15da65 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,19 +36,19 @@ "react-router-dom": "^5.2.0", "socket.io-client": "^2.4.0", "styled-components": "^5.3.0", - "web-vitals": "^2.0.1", - "workbox-background-sync": "^6.1.5", - "workbox-broadcast-update": "^6.1.5", - "workbox-cacheable-response": "^6.1.5", - "workbox-core": "^6.1.5", - "workbox-expiration": "^6.1.5", - "workbox-google-analytics": "^6.1.5", - "workbox-navigation-preload": "^6.1.5", - "workbox-precaching": "^6.1.5", - "workbox-range-requests": "^6.1.5", - "workbox-routing": "^6.1.5", - "workbox-strategies": "^6.1.5", - "workbox-streams": "^6.1.5" + "web-vitals": "^0.2.4", + "workbox-background-sync": "^5.1.3", + "workbox-broadcast-update": "^5.1.3", + "workbox-cacheable-response": "^5.1.3", + "workbox-core": "^5.1.3", + "workbox-expiration": "^5.1.3", + "workbox-google-analytics": "^5.1.3", + "workbox-navigation-preload": "^5.1.3", + "workbox-precaching": "^5.1.3", + "workbox-range-requests": "^5.1.3", + "workbox-routing": "^5.1.3", + "workbox-strategies": "^5.1.3", + "workbox-streams": "^5.1.3" }, "devDependencies": { "@storybook/addon-a11y": "^6.2.9", diff --git a/packages/client/public/index.html b/packages/client/public/index.html index 3ca90c9b..a89e6946 100644 --- a/packages/client/public/index.html +++ b/packages/client/public/index.html @@ -4,7 +4,7 @@ - + diff --git a/packages/client/public/manifest.json b/packages/client/public/manifest.json index 7dfabc74..317af663 100644 --- a/packages/client/public/manifest.json +++ b/packages/client/public/manifest.json @@ -23,5 +23,14 @@ "start_url": ".", "display": "standalone", "theme_color": "#257D69", - "background_color": "#22343C" + "background_color": "#22343C", + "shortcuts": [ + { + "name": "How's weather today?", + "short_name": "Today", + "description": "View weather information for today", + "url": "/", + "icons": [{ "src": "logo192.png", "sizes": "192x192" }] + } + ] } diff --git a/packages/client/src/i18n/index.ts b/packages/client/src/i18n/index.ts index feb793e4..44ed5960 100644 --- a/packages/client/src/i18n/index.ts +++ b/packages/client/src/i18n/index.ts @@ -10,7 +10,7 @@ export enum SGLocale { en = 'en', } -const { STORYBOOK, NODE_ENV } = process.env; +const { STORYBOOK, NODE_ENV, REACT_APP_I18N_DEBUG } = process.env; // eslint-disable-next-line @typescript-eslint/no-floating-promises i18n @@ -20,7 +20,7 @@ i18n resources, fallbackLng: SGLocale.en, supportedLngs: Object.values(SGLocale), - debug: !STORYBOOK && NODE_ENV === environments.DEV, + debug: !STORYBOOK && NODE_ENV === environments.DEV && REACT_APP_I18N_DEBUG === 'true', }); export default i18n; diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index da0a68dd..a76e3eaf 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'; import Providers from './providers'; import Routes from './routes'; -import * as serviceWorkerRegistration from './serviceWorker/serviceWorkerRegistration'; +import * as serviceWorkerRegistration from './serviceWorkerRegistration'; ReactDOM.render( diff --git a/packages/client/src/providers/ThemeTypeProvider/index.tsx b/packages/client/src/providers/ThemeTypeProvider/index.tsx index 0d3060f7..db23d910 100644 --- a/packages/client/src/providers/ThemeTypeProvider/index.tsx +++ b/packages/client/src/providers/ThemeTypeProvider/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import useLocalStorage from '../../hooks/useLocalStorage/useLocalStorage'; import { ThemeType } from '../../theme/Theme'; @@ -16,6 +16,20 @@ const ThemeTypeProvider = ({ children }: ThemeProviderProps) => { setThemeType(isSystemDarkTheme ? ThemeType.dark : ThemeType.light); }; + useEffect(() => { + const themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); + if (!themeColorMetaTag) { + return; + } + + if (themeType === ThemeType.light) { + themeColorMetaTag.setAttribute('content', '#efefef'); + } + if (themeType === ThemeType.dark) { + themeColorMetaTag.setAttribute('content', '#22343C'); + } + }, [themeType]); + return ( { + const { REACT_APP_API_URL, NODE_ENV, REACT_APP_FORCE_API_URL } = process.env; + + if (NODE_ENV === environments.DEV || REACT_APP_FORCE_API_URL === 'true') { + if (!REACT_APP_API_URL) { + throw new Error('Missing "REACT_APP_API_URL" env variable'); + } + return REACT_APP_API_URL; + } + + return '/api'; +}; + const AxiosProvider = ({ children }: AxiosProviderProps) => { const [, setUser] = useCurrentUser(); - const { REACT_APP_API_URL, NODE_ENV } = process.env; - const AxiosOverriddenInstance = axios.create({ - baseURL: NODE_ENV === environments.DEV ? REACT_APP_API_URL : '/api', + baseURL: getBaseURL(), headers: { 'Content-Type': 'application/json', }, diff --git a/packages/client/src/serviceWorker/service-worker.ts b/packages/client/src/service-worker.ts similarity index 92% rename from packages/client/src/serviceWorker/service-worker.ts rename to packages/client/src/service-worker.ts index 5b0e1a09..68822559 100644 --- a/packages/client/src/serviceWorker/service-worker.ts +++ b/packages/client/src/service-worker.ts @@ -24,7 +24,6 @@ clientsClaim(); // Their URLs are injected into the manifest variable below. // This variable must be present somewhere in your service worker file, // even if you decide not to use precaching. See https://cra.link/PWA -// eslint-disable-next-line no-underscore-dangle precacheAndRoute(self.__WB_MANIFEST); // Set up App Shell-style routing, so that all navigation requests @@ -46,7 +45,7 @@ registerRoute( // If this looks like a URL for a resource, because it contains // a file extension, skip. - if (fileExtensionRegexp.exec(url.pathname)) { + if (url.pathname.match(fileExtensionRegexp)) { return false; } @@ -80,4 +79,8 @@ self.addEventListener('message', (event) => { } }); -// Any other custom service worker logic can go here. +self.addEventListener('push', (event) => { + const { title, options } = event.data?.json(); + console.log('Push Received...'); + self.registration.showNotification(title, options); +}); diff --git a/packages/client/src/serviceWorker/serviceWorkerRegistration.ts b/packages/client/src/serviceWorkerRegistration.ts similarity index 81% rename from packages/client/src/serviceWorker/serviceWorkerRegistration.ts rename to packages/client/src/serviceWorkerRegistration.ts index c02ebb56..1fa2bcc0 100644 --- a/packages/client/src/serviceWorker/serviceWorkerRegistration.ts +++ b/packages/client/src/serviceWorkerRegistration.ts @@ -12,7 +12,7 @@ // To learn more about the benefits of this model and instructions on how to // opt-in, read https://cra.link/PWA -import { environments } from '../constants'; +import { environments } from './constants'; const isLocalhost = Boolean( window.location.hostname === 'localhost' || @@ -39,11 +39,10 @@ export function register(config?: Config) { } window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/serviceWorker/service-worker.js`; + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { // This is running on localhost. Let's check if a service worker still exists or not. - // eslint-disable-next-line @typescript-eslint/no-use-before-define checkValidServiceWorker(swUrl, config); // Add some additional logging to localhost, pointing developers to the @@ -56,18 +55,52 @@ export function register(config?: Config) { }); } else { // Is not localhost. Just register service worker - // eslint-disable-next-line @typescript-eslint/no-use-before-define registerValidSW(swUrl, config); } }); } } +const registerWebPush = async (registration: ServiceWorkerRegistration) => { + if (!('PushManager' in window)) { + console.log("Push isn't supported on this browser"); + return; + } + + const publicVapidKey = process.env.REACT_APP_PUSH_NOTIFICATION_PUBLIC_VAPID_KEY; + + console.log('Registering Push...'); + + const isAlreadySubscribed = Boolean(await registration.pushManager.getSubscription()); + if (isAlreadySubscribed) { + console.log('Push already subscribed'); + return; + } + + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: publicVapidKey, + }); + console.log('Push Registered...'); + + console.log('Register Api Push...'); + const apiUrl = process.env.REACT_APP_API_URL; + await fetch(`${apiUrl}/push-notifications`, { + method: 'POST', + body: JSON.stringify(subscription), + headers: { + 'content-type': 'application/json', + }, + }); + console.log('Api Push Registered...'); +}; + function registerValidSW(swUrl: string, config?: Config) { navigator.serviceWorker .register(swUrl) - .then((registration) => { - // eslint-disable-next-line no-param-reassign + .then(async (registration) => { + await registerWebPush(registration); + registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 83726cd2..e6c82f33 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, + "lib": ["dom", "dom.iterable", "esnext"], "baseUrl": "./", "outDir": "./build", "jsx": "react-jsx", diff --git a/yarn.lock b/yarn.lock index 73909d69..018d5adc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4885,6 +4885,13 @@ resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.1.4.tgz#d2e3c27523ce1b5d9dc13d16cbce65dc4db2adbe" integrity sha512-19C02B8mr53HufY7S+HO/EHBD7a/R22IwEwyqiHaR19iwL37dN3o0M8RianVInfSSqP7InVSg/o0mUATM4JWsQ== +"@types/web-push@^3.3.2": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15" + integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw== + dependencies: + "@types/node" "*" + "@types/webpack-env@^1.16.0": version "1.16.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.0.tgz#8c0a9435dfa7b3b1be76562f3070efb3f92637b4" @@ -5837,7 +5844,7 @@ asap@^2.0.0, asap@~2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -asn1.js@^5.2.0: +asn1.js@^5.2.0, asn1.js@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== @@ -11558,6 +11565,13 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http_ece@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75" + integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA== + dependencies: + urlsafe-base64 "~1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -13660,6 +13674,15 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" @@ -13668,6 +13691,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -21265,6 +21296,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" + integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY= + use-composed-ref@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.1.0.tgz#9220e4e94a97b7b02d7d27eaeab0b37034438bbc" @@ -21549,6 +21585,18 @@ web-namespaces@^1.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +web-push@^3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1" + integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g== + dependencies: + asn1.js "^5.3.0" + http_ece "1.1.0" + https-proxy-agent "^5.0.0" + jws "^4.0.0" + minimist "^1.2.5" + urlsafe-base64 "^1.0.0" + web-resource-inliner@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz#ac30db8096931f20a7c1b3ade54ff444e2e20f7b" @@ -21561,10 +21609,10 @@ web-resource-inliner@^5.0.0: node-fetch "^2.6.0" valid-data-url "^3.0.0" -web-vitals@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.0.1.tgz#d122720bd9c8dd69792b19c0f6ab0346861a880f" - integrity sha512-niqKyp2T6xF9EzSi+xx+V6qitE0YfagzfUmDAa9qeCrIVeyfzQQ85Uy0ykeRlEVDCCqkhYccoUunNf9ZIQcvtA== +web-vitals@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.4.tgz#ec3df43c834a207fd7cdefd732b2987896e08511" + integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg== webidl-conversions@^5.0.0: version "5.0.0" @@ -21882,34 +21930,20 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -workbox-background-sync@^5.1.4: +workbox-background-sync@^5.1.3, workbox-background-sync@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12" integrity sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA== dependencies: workbox-core "^5.1.4" -workbox-background-sync@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.1.5.tgz#83904fc6487722db98ed9b19eaa39ab5f826c33e" - integrity sha512-VbUmPLsdz+sLzuNxHvMylzyRTiM4q+q7rwLBk3p2mtRL5NZozI8j/KgoGbno96vs84jx4b9zCZMEOIKEUTPf6w== - dependencies: - workbox-core "^6.1.5" - -workbox-broadcast-update@^5.1.4: +workbox-broadcast-update@^5.1.3, workbox-broadcast-update@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-5.1.4.tgz#0eeb89170ddca7f6914fa3523fb14462891f2cfc" integrity sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA== dependencies: workbox-core "^5.1.4" -workbox-broadcast-update@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.1.5.tgz#49a2a4cc50c7b1cfe86bed6d8f15edf1891d1e79" - integrity sha512-zGrTTs+n4wHpYtqYMqBg6kl/x5j1UrczGCQnODSHTxIDV8GXLb/GtA1BCZdysNxpMmdVSeLmTcgIYAAqWFamrA== - dependencies: - workbox-core "^6.1.5" - workbox-build@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-5.1.4.tgz#23d17ed5c32060c363030c8823b39d0eabf4c8c7" @@ -21952,45 +21986,26 @@ workbox-build@^5.1.4: workbox-sw "^5.1.4" workbox-window "^5.1.4" -workbox-cacheable-response@^5.1.4: +workbox-cacheable-response@^5.1.3, workbox-cacheable-response@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-5.1.4.tgz#9ff26e1366214bdd05cf5a43da9305b274078a54" integrity sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA== dependencies: workbox-core "^5.1.4" -workbox-cacheable-response@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.1.5.tgz#2772e09a333cba47b0923ed91fd022416b69e75c" - integrity sha512-x8DC71lO/JCgiaJ194l9le8wc8lFPLgUpDkLhp2si7mXV6S/wZO+8Osvw1LLgYa8YYTWGbhbFhFTXIkEMknIIA== - dependencies: - workbox-core "^6.1.5" - -workbox-core@^5.1.4: +workbox-core@^5.1.3, workbox-core@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4" integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg== -workbox-core@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.1.5.tgz#424ff600e2c5448b14ebd58b2f5ac8ed91b73fb9" - integrity sha512-9SOEle7YcJzg3njC0xMSmrPIiFjfsFm9WjwGd5enXmI8Lwk8wLdy63B0nzu5LXoibEmS9k+aWF8EzaKtOWjNSA== - -workbox-expiration@^5.1.4: +workbox-expiration@^5.1.3, workbox-expiration@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-5.1.4.tgz#92b5df461e8126114943a3b15c55e4ecb920b163" integrity sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ== dependencies: workbox-core "^5.1.4" -workbox-expiration@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.1.5.tgz#a62a4ac953bb654aa969ede13507ca5bd154adc2" - integrity sha512-6cN+FVbh8fNq56LFKPMchGNKCJeyboHsDuGBqmhDUPvD4uDjsegQpDQzn52VaE0cpywbSIsDF/BSq9E9Yjh5oQ== - dependencies: - workbox-core "^6.1.5" - -workbox-google-analytics@^5.1.4: +workbox-google-analytics@^5.1.3, workbox-google-analytics@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-5.1.4.tgz#b3376806b1ac7d7df8418304d379707195fa8517" integrity sha512-0IFhKoEVrreHpKgcOoddV+oIaVXBFKXUzJVBI+nb0bxmcwYuZMdteBTp8AEDJacENtc9xbR0wa9RDCnYsCDLjA== @@ -22000,75 +22015,35 @@ workbox-google-analytics@^5.1.4: workbox-routing "^5.1.4" workbox-strategies "^5.1.4" -workbox-google-analytics@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.1.5.tgz#895fcc50e4976c176b5982e1a8fd08776f18d639" - integrity sha512-LYsJ/VxTkYVLxM1uJKXZLz4cJdemidY7kPyAYtKVZ6EiDG89noASqis75/5lhqM1m3HwQfp2DtoPrelKSpSDBA== - dependencies: - workbox-background-sync "^6.1.5" - workbox-core "^6.1.5" - workbox-routing "^6.1.5" - workbox-strategies "^6.1.5" - -workbox-navigation-preload@^5.1.4: +workbox-navigation-preload@^5.1.3, workbox-navigation-preload@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-5.1.4.tgz#30d1b720d26a05efc5fa11503e5cc1ed5a78902a" integrity sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ== dependencies: workbox-core "^5.1.4" -workbox-navigation-preload@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.1.5.tgz#47a0d3a6d2e74bd3a52b58b72ca337cb5b654310" - integrity sha512-hDbNcWlffv0uvS21jCAC/mYk7NzaGRSWOQXv1p7bj2aONAX5l699D2ZK4D27G8TO0BaLHUmW/1A5CZcsvweQdg== - dependencies: - workbox-core "^6.1.5" - -workbox-precaching@^5.1.4: +workbox-precaching@^5.1.3, workbox-precaching@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-5.1.4.tgz#874f7ebdd750dd3e04249efae9a1b3f48285fe6b" integrity sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA== dependencies: workbox-core "^5.1.4" -workbox-precaching@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.1.5.tgz#9e0fecb5c567192f46783323fccea10bffc9f79e" - integrity sha512-yhm1kb6wgi141JeM5X7z42XJxCry53tbMLB3NgrxktrZbwbrJF8JILzYy+RFKC9tHC6u2bPmL789GPLT2NCDzw== - dependencies: - workbox-core "^6.1.5" - workbox-routing "^6.1.5" - workbox-strategies "^6.1.5" - -workbox-range-requests@^5.1.4: +workbox-range-requests@^5.1.3, workbox-range-requests@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-5.1.4.tgz#7066a12c121df65bf76fdf2b0868016aa2bab859" integrity sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw== dependencies: workbox-core "^5.1.4" -workbox-range-requests@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.1.5.tgz#047ccd12838bebe51a720256a4ca0cfa7197dfd3" - integrity sha512-iACChSapzB0yuIum3ascP/+cfBNuZi5DRrE+u4u5mCHigPlwfSWtlaY+y8p+a8EwcDTVTZVtnrGrRnF31SiLqQ== - dependencies: - workbox-core "^6.1.5" - -workbox-routing@^5.1.4: +workbox-routing@^5.1.3, workbox-routing@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-5.1.4.tgz#3e8cd86bd3b6573488d1a2ce7385e547b547e970" integrity sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw== dependencies: workbox-core "^5.1.4" -workbox-routing@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.1.5.tgz#15884d6152dba03faef83f0b23331846d8b6ef8e" - integrity sha512-uC/Ctz+4GXGL42h1WxUNKxqKRik/38uS0NZ6VY/EHqL2F1ObLFqMHUZ4ZYvyQsKdyI82cxusvhJZHOrY0a2fIQ== - dependencies: - workbox-core "^6.1.5" - -workbox-strategies@^5.1.4: +workbox-strategies@^5.1.3, workbox-strategies@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-5.1.4.tgz#96b1418ccdfde5354612914964074d466c52d08c" integrity sha512-VVS57LpaJTdjW3RgZvPwX0NlhNmscR7OQ9bP+N/34cYMDzXLyA6kqWffP6QKXSkca1OFo/v6v7hW7zrrguo6EA== @@ -22076,14 +22051,7 @@ workbox-strategies@^5.1.4: workbox-core "^5.1.4" workbox-routing "^5.1.4" -workbox-strategies@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.1.5.tgz#2549a3e78f0eda371b760c4db21feb0d26143573" - integrity sha512-QhiOn9KT9YGBdbfWOmJT6pXZOIAxaVrs6J6AMYzRpkUegBTEcv36+ZhE/cfHoT0u2fxVtthHnskOQ/snEzaXQw== - dependencies: - workbox-core "^6.1.5" - -workbox-streams@^5.1.4: +workbox-streams@^5.1.3, workbox-streams@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-5.1.4.tgz#05754e5e3667bdc078df2c9315b3f41210d8cac0" integrity sha512-xU8yuF1hI/XcVhJUAfbQLa1guQUhdLMPQJkdT0kn6HP5CwiPOGiXnSFq80rAG4b1kJUChQQIGPrq439FQUNVrw== @@ -22091,14 +22059,6 @@ workbox-streams@^5.1.4: workbox-core "^5.1.4" workbox-routing "^5.1.4" -workbox-streams@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.1.5.tgz#bb7678677275fc23c9627565a1f238e4ca350290" - integrity sha512-OI1kLvRHGFXV+soDvs6aEwfBwdAkvPB0mRryqdh3/K17qUj/1gRXc8QtpgU+83xqx/I/ar2bTCIj0KPzI/ChCQ== - dependencies: - workbox-core "^6.1.5" - workbox-routing "^6.1.5" - workbox-sw@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-5.1.4.tgz#2bb34c9f7381f90d84cef644816d45150011d3db" From 23bcd3169226a31d6055bd29796830910d322293 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Sat, 17 Jul 2021 01:00:16 +0200 Subject: [PATCH 02/12] feat: handle auth for web push --- .../push-notifications.controller.ts | 3 +- .../push-notifications.module.ts | 6 ++- .../api/src/modules/users/users.module.ts | 2 +- .../src/pages/authorized/Dashboard/index.tsx | 8 +++- .../client/src/serviceWorkerRegistration.ts | 36 ----------------- packages/client/src/utils/registerWebPush.ts | 39 +++++++++++++++++++ 6 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 packages/client/src/utils/registerWebPush.ts diff --git a/packages/api/src/modules/push-notifications/push-notifications.controller.ts b/packages/api/src/modules/push-notifications/push-notifications.controller.ts index e2013051..0629e5a0 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.controller.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.controller.ts @@ -1,9 +1,10 @@ import { Body, Controller, Get, Post } from '@nestjs/common'; import { PushSubscription } from 'web-push'; +import { Auth } from '../auth/decorators/auth.decorator'; import { PushNotificationsService } from './push-notifications.service'; -// @Auth() +@Auth() @Controller('push-notifications') export class PushNotificationsController { constructor(private readonly pushNotificationsService: PushNotificationsService) {} diff --git a/packages/api/src/modules/push-notifications/push-notifications.module.ts b/packages/api/src/modules/push-notifications/push-notifications.module.ts index 68c514fb..c3c494ce 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.module.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.module.ts @@ -1,12 +1,16 @@ import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { TokenModule } from '../auth/token/token.module'; import { ConfigModule } from '../config/config.module'; +import { RepositoryModule } from '../repository/repository.module'; import { PushNotificationsController } from './push-notifications.controller'; import { PushNotificationsService } from './push-notifications.service'; @Module({ - imports: [ConfigModule], + imports: [ConfigModule, RepositoryModule, AuthModule, TokenModule], providers: [PushNotificationsService], controllers: [PushNotificationsController], + exports: [PushNotificationsService], }) export class PushNotificationsModule {} diff --git a/packages/api/src/modules/users/users.module.ts b/packages/api/src/modules/users/users.module.ts index c8f0a4c1..96442b83 100644 --- a/packages/api/src/modules/users/users.module.ts +++ b/packages/api/src/modules/users/users.module.ts @@ -8,7 +8,7 @@ import { UsersService } from './users.service'; @Module({ controllers: [UsersController], - imports: [AuthModule, RepositoryModule, AuthModule, TokenModule], + imports: [AuthModule, RepositoryModule, TokenModule], providers: [UsersService], exports: [UsersService], }) diff --git a/packages/client/src/pages/authorized/Dashboard/index.tsx b/packages/client/src/pages/authorized/Dashboard/index.tsx index 2c86face..1545fa0f 100644 --- a/packages/client/src/pages/authorized/Dashboard/index.tsx +++ b/packages/client/src/pages/authorized/Dashboard/index.tsx @@ -1,14 +1,16 @@ import React, { useContext, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useSnackbar } from '../../../hooks'; +import { useAxios, useSnackbar } from '../../../hooks'; import { WebSocketContext } from '../../../providers/api/WebSocketProvider/WebSocketProvider.context'; +import registerWebPush from '../../../utils/registerWebPush'; import { Title } from '../AuthorizedPages.styled'; import { ToggleButton } from './Dashboard.styled'; const Dashboard = () => { const { t } = useTranslation(); const showSnackbar = useSnackbar(); + const axios = useAxios(); const { connect, disconnect, toggleGate, connectionState, deviceStatus } = useContext(WebSocketContext); @@ -22,6 +24,10 @@ const Dashboard = () => { }; }, [disconnect]); + useEffect(() => { + void registerWebPush(axios); + }, [axios]); + const onToggle = () => { console.count('toggledGate'); toggleGate(); diff --git a/packages/client/src/serviceWorkerRegistration.ts b/packages/client/src/serviceWorkerRegistration.ts index 1fa2bcc0..cd87552e 100644 --- a/packages/client/src/serviceWorkerRegistration.ts +++ b/packages/client/src/serviceWorkerRegistration.ts @@ -61,46 +61,10 @@ export function register(config?: Config) { } } -const registerWebPush = async (registration: ServiceWorkerRegistration) => { - if (!('PushManager' in window)) { - console.log("Push isn't supported on this browser"); - return; - } - - const publicVapidKey = process.env.REACT_APP_PUSH_NOTIFICATION_PUBLIC_VAPID_KEY; - - console.log('Registering Push...'); - - const isAlreadySubscribed = Boolean(await registration.pushManager.getSubscription()); - if (isAlreadySubscribed) { - console.log('Push already subscribed'); - return; - } - - const subscription = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: publicVapidKey, - }); - console.log('Push Registered...'); - - console.log('Register Api Push...'); - const apiUrl = process.env.REACT_APP_API_URL; - await fetch(`${apiUrl}/push-notifications`, { - method: 'POST', - body: JSON.stringify(subscription), - headers: { - 'content-type': 'application/json', - }, - }); - console.log('Api Push Registered...'); -}; - function registerValidSW(swUrl: string, config?: Config) { navigator.serviceWorker .register(swUrl) .then(async (registration) => { - await registerWebPush(registration); - registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { diff --git a/packages/client/src/utils/registerWebPush.ts b/packages/client/src/utils/registerWebPush.ts new file mode 100644 index 00000000..edbf16e7 --- /dev/null +++ b/packages/client/src/utils/registerWebPush.ts @@ -0,0 +1,39 @@ +import { AxiosInstance } from 'axios'; + +import { onlyOnDevEnv } from './index'; + +const isPushNotificationSupported = () => 'serviceWorker' in navigator && 'PushManager' in window; + +const createNotificationSubscription = async ( + serviceWorker: ServiceWorkerRegistration, +): Promise => { + const applicationServerKey = process.env.REACT_APP_PUSH_NOTIFICATION_PUBLIC_VAPID_KEY; + return serviceWorker.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey, + }); +}; + +const saveSubscription = async (axios: AxiosInstance, subscription: PushSubscription) => { + await axios.post('/push-notifications', JSON.stringify(subscription)); + onlyOnDevEnv(() => console.log('Web push registered')); +}; + +const registerWebPush = async (axios: AxiosInstance) => { + if (!isPushNotificationSupported) { + onlyOnDevEnv(() => console.log("Push isn't supported on this browser")); + return; + } + + const serviceWorker = await navigator.serviceWorker.ready; + const isAlreadySubscribed = Boolean(await serviceWorker.pushManager.getSubscription()); + if (isAlreadySubscribed) { + onlyOnDevEnv(() => console.log('Web push already registered')); + return; + } + + const subscription = await createNotificationSubscription(serviceWorker); + await saveSubscription(axios, subscription); +}; + +export default registerWebPush; From e05212387e2413a2454e6e06fceef0d54a61786c Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Sat, 17 Jul 2021 02:52:23 +0200 Subject: [PATCH 03/12] feat: handle web push subscriptions in db --- .../src/modules/database/entities/index.ts | 3 +- .../entities/pushNotification.entity.ts | 27 ++++++++++ .../modules/database/entities/user.entity.ts | 7 +++ .../1626479469506-PushNotificationEntity.ts | 21 ++++++++ .../dto/send-push-notification.dto.ts | 13 +++++ .../dto/subscribe-push-notification.dto.ts | 12 +++++ .../push-notifications.controller.ts | 23 ++++++-- .../push-notifications.service.spec.ts | 19 ------- .../push-notifications.service.ts | 52 ++++++++++++------- .../push-notifications.types.d.ts | 2 +- .../push-notification.repository.ts | 4 ++ .../modules/repository/repository.module.ts | 15 +++++- .../api/src/modules/users/users.service.ts | 5 +- packages/client/src/service-worker.ts | 8 ++- packages/client/src/utils/registerWebPush.ts | 10 +--- 15 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 packages/api/src/modules/database/entities/pushNotification.entity.ts create mode 100644 packages/api/src/modules/database/migrations/1626479469506-PushNotificationEntity.ts create mode 100644 packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts create mode 100644 packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts delete mode 100644 packages/api/src/modules/push-notifications/push-notifications.service.spec.ts create mode 100644 packages/api/src/modules/repository/push-notification.repository.ts diff --git a/packages/api/src/modules/database/entities/index.ts b/packages/api/src/modules/database/entities/index.ts index f112bbd5..ce180348 100644 --- a/packages/api/src/modules/database/entities/index.ts +++ b/packages/api/src/modules/database/entities/index.ts @@ -1,5 +1,6 @@ import { InvitationEntity } from './invitation.entity'; +import { PushNotificationEntity } from './pushNotification.entity'; import { RefreshTokenEntity } from './refreshToken.entity'; import { UserEntity } from './user.entity'; -export default [UserEntity, RefreshTokenEntity, InvitationEntity]; +export default [UserEntity, RefreshTokenEntity, InvitationEntity, PushNotificationEntity]; diff --git a/packages/api/src/modules/database/entities/pushNotification.entity.ts b/packages/api/src/modules/database/entities/pushNotification.entity.ts new file mode 100644 index 00000000..79f6ee2f --- /dev/null +++ b/packages/api/src/modules/database/entities/pushNotification.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; + +import { BaseEntity } from './base.entity'; +// eslint-disable-next-line import/no-cycle +import { UserEntity } from './user.entity'; + +@Entity('push_notifications') +export class PushNotificationEntity extends BaseEntity { + @Column({ + type: 'varchar', + unique: true, + }) + public endpoint: string; + + @Column({ + type: 'varchar', + }) + public p256dh: string; + + @Column({ + type: 'varchar', + }) + public auth: string; + + @ManyToOne(() => UserEntity, (user) => user.refreshTokens, { onDelete: 'CASCADE' }) + public user: Promise; +} diff --git a/packages/api/src/modules/database/entities/user.entity.ts b/packages/api/src/modules/database/entities/user.entity.ts index c870ef75..a6b9eedf 100644 --- a/packages/api/src/modules/database/entities/user.entity.ts +++ b/packages/api/src/modules/database/entities/user.entity.ts @@ -5,6 +5,8 @@ import { BaseEntity } from './base.entity'; // eslint-disable-next-line import/no-cycle import { InvitationEntity } from './invitation.entity'; // eslint-disable-next-line import/no-cycle +import { PushNotificationEntity } from './pushNotification.entity'; +// eslint-disable-next-line import/no-cycle import { RefreshTokenEntity } from './refreshToken.entity'; @Entity('users') @@ -46,4 +48,9 @@ export class UserEntity extends BaseEntity { @OneToMany(() => InvitationEntity, (invitation) => invitation.updatedBy, { onDelete: 'CASCADE' }) public updatedInvitations: Array; + + @OneToMany(() => PushNotificationEntity, (pushNotification) => pushNotification.user, { + onDelete: 'CASCADE', + }) + public pushNotifications: Array; } diff --git a/packages/api/src/modules/database/migrations/1626479469506-PushNotificationEntity.ts b/packages/api/src/modules/database/migrations/1626479469506-PushNotificationEntity.ts new file mode 100644 index 00000000..4f8e6a79 --- /dev/null +++ b/packages/api/src/modules/database/migrations/1626479469506-PushNotificationEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PushNotificationEntity1626479469506 implements MigrationInterface { + name = 'PushNotificationEntity1626479469506'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "push_notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "endpoint" character varying NOT NULL, "p256dh" character varying NOT NULL, "auth" character varying NOT NULL, "userId" uuid, CONSTRAINT "UQ_0fbc76039fde71a789ccbfbf081" UNIQUE ("endpoint"), CONSTRAINT "PK_99bba16844a5a39fd0d23fb8835" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "push_notifications" ADD CONSTRAINT "FK_a4cb30fb825189ba472f54b163e" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "push_notifications" DROP CONSTRAINT "FK_a4cb30fb825189ba472f54b163e"`, + ); + await queryRunner.query(`DROP TABLE "push_notifications"`); + } +} diff --git a/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts b/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts new file mode 100644 index 00000000..6fb140cc --- /dev/null +++ b/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts @@ -0,0 +1,13 @@ +import { IsOptional, IsString, ValidateNested } from 'class-validator'; + +export class SendPushNotificationDto { + @IsString() + title: string; + + @IsString() + body: string; + + @IsOptional() + @ValidateNested() + options?: PushNotificationOptions; +} diff --git a/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts b/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts new file mode 100644 index 00000000..e4d751cd --- /dev/null +++ b/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts @@ -0,0 +1,12 @@ +import { ValidateNested } from 'class-validator'; +import { PushSubscription } from 'web-push'; + +import { UserEntity } from '../../database/entities/user.entity'; + +export class SubscribePushNotificationDto { + @ValidateNested() + subscription: PushSubscription; + + @ValidateNested() + userPromise: Promise; +} diff --git a/packages/api/src/modules/push-notifications/push-notifications.controller.ts b/packages/api/src/modules/push-notifications/push-notifications.controller.ts index 0629e5a0..1ba4bd0c 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.controller.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.controller.ts @@ -1,21 +1,34 @@ import { Body, Controller, Get, Post } from '@nestjs/common'; import { PushSubscription } from 'web-push'; +import { TokenPayload } from '../../interfaces/token-types'; +import { AuthService } from '../auth/auth.service'; import { Auth } from '../auth/decorators/auth.decorator'; +import { CookiePayload } from '../auth/decorators/cookiePayload.decorator'; import { PushNotificationsService } from './push-notifications.service'; @Auth() @Controller('push-notifications') export class PushNotificationsController { - constructor(private readonly pushNotificationsService: PushNotificationsService) {} + constructor( + private readonly pushNotificationsService: PushNotificationsService, + private readonly authService: AuthService, + ) {} @Post() - subscribe(@Body() subscription: PushSubscription) { - this.pushNotificationsService.subscribe(subscription); + async subscribe( + @CookiePayload() { sub }: TokenPayload, + @Body() subscription: PushSubscription, + ): Promise { + const userPromise = this.authService.getUser(sub); + await this.pushNotificationsService.subscribe({ subscription, userPromise }); } @Get() - async send() { - await this.pushNotificationsService.send(); + async send(): Promise { + await this.pushNotificationsService.send({ + title: 'Smart Gate', + body: 'Test msg', + }); } } diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.spec.ts b/packages/api/src/modules/push-notifications/push-notifications.service.spec.ts deleted file mode 100644 index 7cf7b6e0..00000000 --- a/packages/api/src/modules/push-notifications/push-notifications.service.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { PushNotificationsService } from './push-notifications.service'; - -describe('pushNotificationsService', () => { - let service: PushNotificationsService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [PushNotificationsService], - }).compile(); - - service = module.get(PushNotificationsService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index e625c38d..a4b1b483 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -2,14 +2,18 @@ import { Injectable, Logger } from '@nestjs/common'; import webPush, { PushSubscription } from 'web-push'; import { Config } from '../config/config'; +import { PushNotificationRepository } from '../repository/push-notification.repository'; +import { SendPushNotificationDto } from './dto/send-push-notification.dto'; +import { SubscribePushNotificationDto } from './dto/subscribe-push-notification.dto'; @Injectable() export class PushNotificationsService { - private subs: Array = []; - private logger: Logger = new Logger('PushNotifications'); - constructor(private readonly config: Config) { + constructor( + private readonly config: Config, + private readonly pushNotificationRepository: PushNotificationRepository, + ) { const { pushNotifications, mailer } = config; const { publicVapidKey, privateVapidKey } = pushNotifications; const { replyTo } = mailer; @@ -17,32 +21,44 @@ export class PushNotificationsService { webPush.setVapidDetails(`mailto:${replyTo}`, publicVapidKey, privateVapidKey); } - subscribe(subscription: PushSubscription): void { + async subscribe({ subscription, userPromise }: SubscribePushNotificationDto): Promise { this.logger.log('New push subscriber'); - this.subs.push(subscription); - this.logger.log(`${this.subs.length} subscriptions`); + await this.pushNotificationRepository.create({ + user: userPromise, + endpoint: subscription.endpoint, + p256dh: subscription.keys.p256dh, + auth: subscription.keys.auth, + }); } - async send(): Promise { + async send({ title, body, options }: SendPushNotificationDto): Promise { this.logger.log('Sending...'); - if (!this.subs.length) { - this.logger.log('No subscribers'); + const allSubscriptions = await this.pushNotificationRepository.find(); + + if (!allSubscriptions.length) { + this.logger.log('No push subscriptions'); return; } - const payload = JSON.stringify({ - title: 'Push Test', + const payload: PushNotificationPayload = { + title, options: { - body: 'Smart Gate notification', + body, + ...options, }, - } as PushNotificationPayload); + }; - const promises = this.subs.map((subscription) => - webPush.sendNotification(subscription, payload), - ); + const sendNotificationsPromises = allSubscriptions.map(({ endpoint, p256dh, auth }) => { + const sub: PushSubscription = { endpoint, keys: { p256dh, auth } }; + return webPush.sendNotification(sub, JSON.stringify(payload)); + }); - await Promise.all(promises); - this.logger.log('Sent'); + await Promise.all(sendNotificationsPromises); + this.logger.log( + `Sent ${allSubscriptions.length} ${ + allSubscriptions.length > 1 ? 'notifications' : 'notification' + }`, + ); } } diff --git a/packages/api/src/modules/push-notifications/push-notifications.types.d.ts b/packages/api/src/modules/push-notifications/push-notifications.types.d.ts index 583de082..0953a1af 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.types.d.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.types.d.ts @@ -17,7 +17,7 @@ interface PushNotificationOptions { interface PushNotificationPayload { title: string; - options: PushNotificationOptions; + options?: PushNotificationOptions; } type PushNotificationDirection = 'auto' | 'ltr' | 'rtl'; diff --git a/packages/api/src/modules/repository/push-notification.repository.ts b/packages/api/src/modules/repository/push-notification.repository.ts new file mode 100644 index 00000000..7d13579d --- /dev/null +++ b/packages/api/src/modules/repository/push-notification.repository.ts @@ -0,0 +1,4 @@ +import { PushNotificationEntity } from '../database/entities/pushNotification.entity'; +import { BaseRepository } from './base.repository'; + +export class PushNotificationRepository extends BaseRepository(PushNotificationEntity) {} diff --git a/packages/api/src/modules/repository/repository.module.ts b/packages/api/src/modules/repository/repository.module.ts index dcc484ae..ea28012a 100644 --- a/packages/api/src/modules/repository/repository.module.ts +++ b/packages/api/src/modules/repository/repository.module.ts @@ -3,12 +3,23 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import entities from '../database/entities'; import { InvitationRepository } from './invitation.repository'; +import { PushNotificationRepository } from './push-notification.repository'; import { RefreshTokenRepository } from './refresh-token.repository'; import { UserRepository } from './user.repository'; @Module({ imports: [TypeOrmModule.forFeature(entities)], - providers: [UserRepository, InvitationRepository, RefreshTokenRepository], - exports: [UserRepository, InvitationRepository, RefreshTokenRepository], + providers: [ + UserRepository, + InvitationRepository, + RefreshTokenRepository, + PushNotificationRepository, + ], + exports: [ + UserRepository, + InvitationRepository, + RefreshTokenRepository, + PushNotificationRepository, + ], }) export class RepositoryModule {} diff --git a/packages/api/src/modules/users/users.service.ts b/packages/api/src/modules/users/users.service.ts index c4658456..a381cdb6 100644 --- a/packages/api/src/modules/users/users.service.ts +++ b/packages/api/src/modules/users/users.service.ts @@ -11,10 +11,7 @@ import { UpdateUserDto } from './dto/update-user.dto'; @Injectable() export class UsersService { - constructor( - private readonly connection: Connection, - private readonly userRepository: UserRepository, - ) {} + constructor(private readonly userRepository: UserRepository) {} async create(createUserDto: CreateUserDto): Promise { return this.userRepository.create(createUserDto); diff --git a/packages/client/src/service-worker.ts b/packages/client/src/service-worker.ts index 68822559..54f8b4f0 100644 --- a/packages/client/src/service-worker.ts +++ b/packages/client/src/service-worker.ts @@ -81,6 +81,10 @@ self.addEventListener('message', (event) => { self.addEventListener('push', (event) => { const { title, options } = event.data?.json(); - console.log('Push Received...'); - self.registration.showNotification(title, options); + const optionsWithDefaults: NotificationOptions = { + icon: '/email-images/sg-logo.png', + ...options, + }; + + self.registration.showNotification(title, optionsWithDefaults); }); diff --git a/packages/client/src/utils/registerWebPush.ts b/packages/client/src/utils/registerWebPush.ts index edbf16e7..86f99daa 100644 --- a/packages/client/src/utils/registerWebPush.ts +++ b/packages/client/src/utils/registerWebPush.ts @@ -1,7 +1,5 @@ import { AxiosInstance } from 'axios'; -import { onlyOnDevEnv } from './index'; - const isPushNotificationSupported = () => 'serviceWorker' in navigator && 'PushManager' in window; const createNotificationSubscription = async ( @@ -14,21 +12,17 @@ const createNotificationSubscription = async ( }); }; -const saveSubscription = async (axios: AxiosInstance, subscription: PushSubscription) => { - await axios.post('/push-notifications', JSON.stringify(subscription)); - onlyOnDevEnv(() => console.log('Web push registered')); -}; +const saveSubscription = async (axios: AxiosInstance, subscription: PushSubscription) => + axios.post('/push-notifications', JSON.stringify(subscription)); const registerWebPush = async (axios: AxiosInstance) => { if (!isPushNotificationSupported) { - onlyOnDevEnv(() => console.log("Push isn't supported on this browser")); return; } const serviceWorker = await navigator.serviceWorker.ready; const isAlreadySubscribed = Boolean(await serviceWorker.pushManager.getSubscription()); if (isAlreadySubscribed) { - onlyOnDevEnv(() => console.log('Web push already registered')); return; } From e79a2aff133bb801dabb77c5aa3576d4e97c25aa Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Sun, 18 Jul 2021 23:38:09 +0200 Subject: [PATCH 04/12] feat: handle web push user permission --- packages/api/src/modules/mailer/mailer.service.ts | 2 +- packages/client/src/utils/registerWebPush.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/api/src/modules/mailer/mailer.service.ts b/packages/api/src/modules/mailer/mailer.service.ts index e7791183..8f90ff9a 100644 --- a/packages/api/src/modules/mailer/mailer.service.ts +++ b/packages/api/src/modules/mailer/mailer.service.ts @@ -14,7 +14,7 @@ export class MailerService { private readonly config: Config, ) {} - async sendEmail(options: Mail.Options): Promise { + private async sendEmail(options: Mail.Options): Promise { const transporterBaseConfig = await this.mailerConfigService.getTransporterConfig(); const transporter = nodemailer.createTransport(transporterBaseConfig); diff --git a/packages/client/src/utils/registerWebPush.ts b/packages/client/src/utils/registerWebPush.ts index 86f99daa..fc72bbad 100644 --- a/packages/client/src/utils/registerWebPush.ts +++ b/packages/client/src/utils/registerWebPush.ts @@ -12,17 +12,26 @@ const createNotificationSubscription = async ( }); }; +const isWebPushGranted = async () => (await Notification.requestPermission()) === 'granted'; + const saveSubscription = async (axios: AxiosInstance, subscription: PushSubscription) => axios.post('/push-notifications', JSON.stringify(subscription)); +const isAlreadySubscribed = async (serviceWorker: ServiceWorkerRegistration) => + Boolean(await serviceWorker.pushManager.getSubscription()); + const registerWebPush = async (axios: AxiosInstance) => { - if (!isPushNotificationSupported) { + if (!isPushNotificationSupported()) { + return; + } + + if (!(await isWebPushGranted())) { return; } const serviceWorker = await navigator.serviceWorker.ready; - const isAlreadySubscribed = Boolean(await serviceWorker.pushManager.getSubscription()); - if (isAlreadySubscribed) { + + if (await isAlreadySubscribed(serviceWorker)) { return; } From a54ee69691a5d43497337c9d970a2656f8c82f59 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Mon, 19 Jul 2021 00:25:47 +0200 Subject: [PATCH 05/12] feat(api): handle sending push notifications by roles --- .../dto/send-push-notification.dto.ts | 8 ++++++- .../push-notifications.controller.ts | 11 ++-------- .../push-notifications.service.ts | 21 ++++++++++++------- .../push-notification.repository.ts | 16 +++++++++++++- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts b/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts index 6fb140cc..34e666d6 100644 --- a/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts +++ b/packages/api/src/modules/push-notifications/dto/send-push-notification.dto.ts @@ -1,4 +1,6 @@ -import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; + +import { Role } from '../../../enums/role.enum'; export class SendPushNotificationDto { @IsString() @@ -10,4 +12,8 @@ export class SendPushNotificationDto { @IsOptional() @ValidateNested() options?: PushNotificationOptions; + + @IsOptional() + @IsArray() + roles?: [Role]; } diff --git a/packages/api/src/modules/push-notifications/push-notifications.controller.ts b/packages/api/src/modules/push-notifications/push-notifications.controller.ts index 1ba4bd0c..31f9c376 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.controller.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.controller.ts @@ -1,6 +1,7 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; +import { Body, Controller, Post } from '@nestjs/common'; import { PushSubscription } from 'web-push'; +import { Role } from '../../enums/role.enum'; import { TokenPayload } from '../../interfaces/token-types'; import { AuthService } from '../auth/auth.service'; import { Auth } from '../auth/decorators/auth.decorator'; @@ -23,12 +24,4 @@ export class PushNotificationsController { const userPromise = this.authService.getUser(sub); await this.pushNotificationsService.subscribe({ subscription, userPromise }); } - - @Get() - async send(): Promise { - await this.pushNotificationsService.send({ - title: 'Smart Gate', - body: 'Test msg', - }); - } } diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index a4b1b483..970a252b 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import webPush, { PushSubscription } from 'web-push'; +import { Role } from '../../enums/role.enum'; import { Config } from '../config/config'; import { PushNotificationRepository } from '../repository/push-notification.repository'; import { SendPushNotificationDto } from './dto/send-push-notification.dto'; @@ -31,12 +32,20 @@ export class PushNotificationsService { }); } - async send({ title, body, options }: SendPushNotificationDto): Promise { + private async getSubscriptions(roles: Array | undefined) { + if (!roles) { + return this.pushNotificationRepository.find(); + } + + return this.pushNotificationRepository.findByRoles([Role.SuperAdmin]); + } + + async send({ title, body, options, roles }: SendPushNotificationDto): Promise { this.logger.log('Sending...'); - const allSubscriptions = await this.pushNotificationRepository.find(); + const subscriptions = await this.getSubscriptions(roles); - if (!allSubscriptions.length) { + if (!subscriptions.length) { this.logger.log('No push subscriptions'); return; } @@ -49,16 +58,14 @@ export class PushNotificationsService { }, }; - const sendNotificationsPromises = allSubscriptions.map(({ endpoint, p256dh, auth }) => { + const sendNotificationsPromises = subscriptions.map(({ endpoint, p256dh, auth }) => { const sub: PushSubscription = { endpoint, keys: { p256dh, auth } }; return webPush.sendNotification(sub, JSON.stringify(payload)); }); await Promise.all(sendNotificationsPromises); this.logger.log( - `Sent ${allSubscriptions.length} ${ - allSubscriptions.length > 1 ? 'notifications' : 'notification' - }`, + `Sent ${subscriptions.length} ${subscriptions.length > 1 ? 'notifications' : 'notification'}`, ); } } diff --git a/packages/api/src/modules/repository/push-notification.repository.ts b/packages/api/src/modules/repository/push-notification.repository.ts index 7d13579d..00b9ac96 100644 --- a/packages/api/src/modules/repository/push-notification.repository.ts +++ b/packages/api/src/modules/repository/push-notification.repository.ts @@ -1,4 +1,18 @@ +import { In } from 'typeorm'; + +import { Role } from '../../enums/role.enum'; import { PushNotificationEntity } from '../database/entities/pushNotification.entity'; import { BaseRepository } from './base.repository'; -export class PushNotificationRepository extends BaseRepository(PushNotificationEntity) {} +export class PushNotificationRepository extends BaseRepository(PushNotificationEntity) { + async findByRoles(roles: Role[]): Promise> { + return this.find({ + relations: ['user'], + where: { + user: { + roles: In(roles), + }, + }, + }); + } +} From 4ed4fda1416d186ecfa6c045c1c4928dcfdeb600 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Mon, 19 Jul 2021 00:29:51 +0200 Subject: [PATCH 06/12] fix: getSubscriptions func roles --- .../modules/push-notifications/push-notifications.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index 970a252b..76b3977f 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -32,12 +32,12 @@ export class PushNotificationsService { }); } - private async getSubscriptions(roles: Array | undefined) { + private async getSubscriptions(roles?: [Role] | undefined) { if (!roles) { return this.pushNotificationRepository.find(); } - return this.pushNotificationRepository.findByRoles([Role.SuperAdmin]); + return this.pushNotificationRepository.findByRoles(roles); } async send({ title, body, options, roles }: SendPushNotificationDto): Promise { From 0dd3cc48d61a1926a7c86a5c3794cfea7334a2d7 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Mon, 19 Jul 2021 01:01:38 +0200 Subject: [PATCH 07/12] feat(api): require web-push envs only on prod --- packages/api/src/modules/config/config-loader.ts | 4 ++-- packages/api/src/modules/config/config.ts | 4 ++-- .../push-notifications/push-notifications.controller.ts | 1 - .../modules/push-notifications/push-notifications.service.ts | 5 +++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/api/src/modules/config/config-loader.ts b/packages/api/src/modules/config/config-loader.ts index b6fac9cf..99e0a24f 100644 --- a/packages/api/src/modules/config/config-loader.ts +++ b/packages/api/src/modules/config/config-loader.ts @@ -30,8 +30,8 @@ export class ConfigLoader { lastName: this.envConfigService.get('TEST_USER_LASTNAME', isTest), }, pushNotifications: { - publicVapidKey: this.envConfigService.get('PUSH_NOTIFICATION_PUBLIC_VAPID_KEY'), - privateVapidKey: this.envConfigService.get('PUSH_NOTIFICATION_PRIVATE_VAPID_KEY'), + publicVapidKey: this.envConfigService.get('PUSH_NOTIFICATION_PUBLIC_VAPID_KEY', isProd), + privateVapidKey: this.envConfigService.get('PUSH_NOTIFICATION_PRIVATE_VAPID_KEY', isProd), }, rateLimiter: { minTime: this.envConfigService.get('RATE_LIMIT_MIN_TIME', isProd, Number), diff --git a/packages/api/src/modules/config/config.ts b/packages/api/src/modules/config/config.ts index 96881db9..4a8e4a88 100644 --- a/packages/api/src/modules/config/config.ts +++ b/packages/api/src/modules/config/config.ts @@ -17,8 +17,8 @@ export class Config { }; pushNotifications: { - publicVapidKey: string; - privateVapidKey: string; + publicVapidKey?: string; + privateVapidKey?: string; }; database: { diff --git a/packages/api/src/modules/push-notifications/push-notifications.controller.ts b/packages/api/src/modules/push-notifications/push-notifications.controller.ts index 31f9c376..2309edd3 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.controller.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Post } from '@nestjs/common'; import { PushSubscription } from 'web-push'; -import { Role } from '../../enums/role.enum'; import { TokenPayload } from '../../interfaces/token-types'; import { AuthService } from '../auth/auth.service'; import { Auth } from '../auth/decorators/auth.decorator'; diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index 76b3977f..3a174b07 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -18,8 +18,9 @@ export class PushNotificationsService { const { pushNotifications, mailer } = config; const { publicVapidKey, privateVapidKey } = pushNotifications; const { replyTo } = mailer; - - webPush.setVapidDetails(`mailto:${replyTo}`, publicVapidKey, privateVapidKey); + if (publicVapidKey && privateVapidKey) { + webPush.setVapidDetails(`mailto:${replyTo}`, publicVapidKey, privateVapidKey); + } } async subscribe({ subscription, userPromise }: SubscribePushNotificationDto): Promise { From 6bfd9b330a5a2ae4ab8539f84f07235726dcab39 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Mon, 19 Jul 2021 01:10:48 +0200 Subject: [PATCH 08/12] fix(client): push notifications env variable name --- packages/client/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/.env.example b/packages/client/.env.example index 42ec0db9..8b0eb2c5 100644 --- a/packages/client/.env.example +++ b/packages/client/.env.example @@ -27,4 +27,4 @@ REACT_APP_SENTRY_TRACES_SAMPLE=0 # Push notifications # For more details check: https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey -PUSH_NOTIFICATION_PUBLIC_VAPID_KEY= +REACT_APP_PUSH_NOTIFICATION_PUBLIC_VAPID_KEY= From ac26b1990e9584035f4c6ce1ed74f36da6c47ab9 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Tue, 20 Jul 2021 22:41:12 +0200 Subject: [PATCH 09/12] chore: adjust api notifications service logs, remove unused variable, update workbox deps --- .../push-notifications.service.ts | 17 ++- .../api/src/modules/users/users.service.ts | 1 - packages/client/package.json | 26 ++-- yarn.lock | 120 +++++++++++++++--- 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index 3a174b07..837be294 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -18,13 +18,14 @@ export class PushNotificationsService { const { pushNotifications, mailer } = config; const { publicVapidKey, privateVapidKey } = pushNotifications; const { replyTo } = mailer; + if (publicVapidKey && privateVapidKey) { webPush.setVapidDetails(`mailto:${replyTo}`, publicVapidKey, privateVapidKey); } } async subscribe({ subscription, userPromise }: SubscribePushNotificationDto): Promise { - this.logger.log('New push subscriber'); + this.logger.log('New subscriber'); await this.pushNotificationRepository.create({ user: userPromise, endpoint: subscription.endpoint, @@ -47,7 +48,7 @@ export class PushNotificationsService { const subscriptions = await this.getSubscriptions(roles); if (!subscriptions.length) { - this.logger.log('No push subscriptions'); + this.logger.log('No subscriptions'); return; } @@ -64,9 +65,13 @@ export class PushNotificationsService { return webPush.sendNotification(sub, JSON.stringify(payload)); }); - await Promise.all(sendNotificationsPromises); - this.logger.log( - `Sent ${subscriptions.length} ${subscriptions.length > 1 ? 'notifications' : 'notification'}`, - ); + try { + await Promise.all(sendNotificationsPromises); + + const sendTotal = sendNotificationsPromises.length; + this.logger.log(`Sent ${sendTotal} ${sendTotal > 1 ? 'notifications' : 'notification'}`); + } catch (error) { + this.logger.error(error); + } } } diff --git a/packages/api/src/modules/users/users.service.ts b/packages/api/src/modules/users/users.service.ts index a381cdb6..6a8d2000 100644 --- a/packages/api/src/modules/users/users.service.ts +++ b/packages/api/src/modules/users/users.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import * as Sentry from '@sentry/node'; -import { Connection } from 'typeorm'; import { GetList } from '../../interfaces/react-admin-types'; import { UserEntity } from '../database/entities/user.entity'; diff --git a/packages/client/package.json b/packages/client/package.json index 1f15da65..728c123c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,19 +36,19 @@ "react-router-dom": "^5.2.0", "socket.io-client": "^2.4.0", "styled-components": "^5.3.0", - "web-vitals": "^0.2.4", - "workbox-background-sync": "^5.1.3", - "workbox-broadcast-update": "^5.1.3", - "workbox-cacheable-response": "^5.1.3", - "workbox-core": "^5.1.3", - "workbox-expiration": "^5.1.3", - "workbox-google-analytics": "^5.1.3", - "workbox-navigation-preload": "^5.1.3", - "workbox-precaching": "^5.1.3", - "workbox-range-requests": "^5.1.3", - "workbox-routing": "^5.1.3", - "workbox-strategies": "^5.1.3", - "workbox-streams": "^5.1.3" + "web-vitals": "^2.1.0", + "workbox-background-sync": "^6.1.5", + "workbox-broadcast-update": "^6.1.5", + "workbox-cacheable-response": "^6.1.5", + "workbox-core": "^6.1.5", + "workbox-expiration": "^6.1.5", + "workbox-google-analytics": "^6.1.5", + "workbox-navigation-preload": "^6.1.5", + "workbox-precaching": "^6.1.5", + "workbox-range-requests": "^6.1.5", + "workbox-routing": "^6.1.5", + "workbox-strategies": "^6.1.5", + "workbox-streams": "^6.1.5" }, "devDependencies": { "@storybook/addon-a11y": "^6.2.9", diff --git a/yarn.lock b/yarn.lock index 018d5adc..938722bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21609,10 +21609,10 @@ web-resource-inliner@^5.0.0: node-fetch "^2.6.0" valid-data-url "^3.0.0" -web-vitals@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.4.tgz#ec3df43c834a207fd7cdefd732b2987896e08511" - integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg== +web-vitals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.0.tgz#ebf5428875ab5bfc1056c2e80cd177001287de7b" + integrity sha512-npEyJP8jHf3J71t1tRTEtz9FeKp8H2udWJUUq5ykfPhhstr//TUxiYhIEzLNwk4zv2ybAilMn7v7N6Mxmuitmg== webidl-conversions@^5.0.0: version "5.0.0" @@ -21930,20 +21930,34 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -workbox-background-sync@^5.1.3, workbox-background-sync@^5.1.4: +workbox-background-sync@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12" integrity sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA== dependencies: workbox-core "^5.1.4" -workbox-broadcast-update@^5.1.3, workbox-broadcast-update@^5.1.4: +workbox-background-sync@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.1.5.tgz#83904fc6487722db98ed9b19eaa39ab5f826c33e" + integrity sha512-VbUmPLsdz+sLzuNxHvMylzyRTiM4q+q7rwLBk3p2mtRL5NZozI8j/KgoGbno96vs84jx4b9zCZMEOIKEUTPf6w== + dependencies: + workbox-core "^6.1.5" + +workbox-broadcast-update@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-5.1.4.tgz#0eeb89170ddca7f6914fa3523fb14462891f2cfc" integrity sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA== dependencies: workbox-core "^5.1.4" +workbox-broadcast-update@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.1.5.tgz#49a2a4cc50c7b1cfe86bed6d8f15edf1891d1e79" + integrity sha512-zGrTTs+n4wHpYtqYMqBg6kl/x5j1UrczGCQnODSHTxIDV8GXLb/GtA1BCZdysNxpMmdVSeLmTcgIYAAqWFamrA== + dependencies: + workbox-core "^6.1.5" + workbox-build@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-5.1.4.tgz#23d17ed5c32060c363030c8823b39d0eabf4c8c7" @@ -21986,26 +22000,45 @@ workbox-build@^5.1.4: workbox-sw "^5.1.4" workbox-window "^5.1.4" -workbox-cacheable-response@^5.1.3, workbox-cacheable-response@^5.1.4: +workbox-cacheable-response@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-5.1.4.tgz#9ff26e1366214bdd05cf5a43da9305b274078a54" integrity sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA== dependencies: workbox-core "^5.1.4" -workbox-core@^5.1.3, workbox-core@^5.1.4: +workbox-cacheable-response@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.1.5.tgz#2772e09a333cba47b0923ed91fd022416b69e75c" + integrity sha512-x8DC71lO/JCgiaJ194l9le8wc8lFPLgUpDkLhp2si7mXV6S/wZO+8Osvw1LLgYa8YYTWGbhbFhFTXIkEMknIIA== + dependencies: + workbox-core "^6.1.5" + +workbox-core@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4" integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg== -workbox-expiration@^5.1.3, workbox-expiration@^5.1.4: +workbox-core@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.1.5.tgz#424ff600e2c5448b14ebd58b2f5ac8ed91b73fb9" + integrity sha512-9SOEle7YcJzg3njC0xMSmrPIiFjfsFm9WjwGd5enXmI8Lwk8wLdy63B0nzu5LXoibEmS9k+aWF8EzaKtOWjNSA== + +workbox-expiration@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-5.1.4.tgz#92b5df461e8126114943a3b15c55e4ecb920b163" integrity sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ== dependencies: workbox-core "^5.1.4" -workbox-google-analytics@^5.1.3, workbox-google-analytics@^5.1.4: +workbox-expiration@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.1.5.tgz#a62a4ac953bb654aa969ede13507ca5bd154adc2" + integrity sha512-6cN+FVbh8fNq56LFKPMchGNKCJeyboHsDuGBqmhDUPvD4uDjsegQpDQzn52VaE0cpywbSIsDF/BSq9E9Yjh5oQ== + dependencies: + workbox-core "^6.1.5" + +workbox-google-analytics@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-5.1.4.tgz#b3376806b1ac7d7df8418304d379707195fa8517" integrity sha512-0IFhKoEVrreHpKgcOoddV+oIaVXBFKXUzJVBI+nb0bxmcwYuZMdteBTp8AEDJacENtc9xbR0wa9RDCnYsCDLjA== @@ -22015,35 +22048,75 @@ workbox-google-analytics@^5.1.3, workbox-google-analytics@^5.1.4: workbox-routing "^5.1.4" workbox-strategies "^5.1.4" -workbox-navigation-preload@^5.1.3, workbox-navigation-preload@^5.1.4: +workbox-google-analytics@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.1.5.tgz#895fcc50e4976c176b5982e1a8fd08776f18d639" + integrity sha512-LYsJ/VxTkYVLxM1uJKXZLz4cJdemidY7kPyAYtKVZ6EiDG89noASqis75/5lhqM1m3HwQfp2DtoPrelKSpSDBA== + dependencies: + workbox-background-sync "^6.1.5" + workbox-core "^6.1.5" + workbox-routing "^6.1.5" + workbox-strategies "^6.1.5" + +workbox-navigation-preload@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-5.1.4.tgz#30d1b720d26a05efc5fa11503e5cc1ed5a78902a" integrity sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ== dependencies: workbox-core "^5.1.4" -workbox-precaching@^5.1.3, workbox-precaching@^5.1.4: +workbox-navigation-preload@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.1.5.tgz#47a0d3a6d2e74bd3a52b58b72ca337cb5b654310" + integrity sha512-hDbNcWlffv0uvS21jCAC/mYk7NzaGRSWOQXv1p7bj2aONAX5l699D2ZK4D27G8TO0BaLHUmW/1A5CZcsvweQdg== + dependencies: + workbox-core "^6.1.5" + +workbox-precaching@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-5.1.4.tgz#874f7ebdd750dd3e04249efae9a1b3f48285fe6b" integrity sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA== dependencies: workbox-core "^5.1.4" -workbox-range-requests@^5.1.3, workbox-range-requests@^5.1.4: +workbox-precaching@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.1.5.tgz#9e0fecb5c567192f46783323fccea10bffc9f79e" + integrity sha512-yhm1kb6wgi141JeM5X7z42XJxCry53tbMLB3NgrxktrZbwbrJF8JILzYy+RFKC9tHC6u2bPmL789GPLT2NCDzw== + dependencies: + workbox-core "^6.1.5" + workbox-routing "^6.1.5" + workbox-strategies "^6.1.5" + +workbox-range-requests@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-5.1.4.tgz#7066a12c121df65bf76fdf2b0868016aa2bab859" integrity sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw== dependencies: workbox-core "^5.1.4" -workbox-routing@^5.1.3, workbox-routing@^5.1.4: +workbox-range-requests@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.1.5.tgz#047ccd12838bebe51a720256a4ca0cfa7197dfd3" + integrity sha512-iACChSapzB0yuIum3ascP/+cfBNuZi5DRrE+u4u5mCHigPlwfSWtlaY+y8p+a8EwcDTVTZVtnrGrRnF31SiLqQ== + dependencies: + workbox-core "^6.1.5" + +workbox-routing@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-5.1.4.tgz#3e8cd86bd3b6573488d1a2ce7385e547b547e970" integrity sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw== dependencies: workbox-core "^5.1.4" -workbox-strategies@^5.1.3, workbox-strategies@^5.1.4: +workbox-routing@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.1.5.tgz#15884d6152dba03faef83f0b23331846d8b6ef8e" + integrity sha512-uC/Ctz+4GXGL42h1WxUNKxqKRik/38uS0NZ6VY/EHqL2F1ObLFqMHUZ4ZYvyQsKdyI82cxusvhJZHOrY0a2fIQ== + dependencies: + workbox-core "^6.1.5" + +workbox-strategies@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-5.1.4.tgz#96b1418ccdfde5354612914964074d466c52d08c" integrity sha512-VVS57LpaJTdjW3RgZvPwX0NlhNmscR7OQ9bP+N/34cYMDzXLyA6kqWffP6QKXSkca1OFo/v6v7hW7zrrguo6EA== @@ -22051,7 +22124,14 @@ workbox-strategies@^5.1.3, workbox-strategies@^5.1.4: workbox-core "^5.1.4" workbox-routing "^5.1.4" -workbox-streams@^5.1.3, workbox-streams@^5.1.4: +workbox-strategies@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.1.5.tgz#2549a3e78f0eda371b760c4db21feb0d26143573" + integrity sha512-QhiOn9KT9YGBdbfWOmJT6pXZOIAxaVrs6J6AMYzRpkUegBTEcv36+ZhE/cfHoT0u2fxVtthHnskOQ/snEzaXQw== + dependencies: + workbox-core "^6.1.5" + +workbox-streams@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-5.1.4.tgz#05754e5e3667bdc078df2c9315b3f41210d8cac0" integrity sha512-xU8yuF1hI/XcVhJUAfbQLa1guQUhdLMPQJkdT0kn6HP5CwiPOGiXnSFq80rAG4b1kJUChQQIGPrq439FQUNVrw== @@ -22059,6 +22139,14 @@ workbox-streams@^5.1.3, workbox-streams@^5.1.4: workbox-core "^5.1.4" workbox-routing "^5.1.4" +workbox-streams@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.1.5.tgz#bb7678677275fc23c9627565a1f238e4ca350290" + integrity sha512-OI1kLvRHGFXV+soDvs6aEwfBwdAkvPB0mRryqdh3/K17qUj/1gRXc8QtpgU+83xqx/I/ar2bTCIj0KPzI/ChCQ== + dependencies: + workbox-core "^6.1.5" + workbox-routing "^6.1.5" + workbox-sw@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-5.1.4.tgz#2bb34c9f7381f90d84cef644816d45150011d3db" From 9209232112974a692bb146b66ace7445ec47100a Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Fri, 30 Jul 2021 23:59:00 +0200 Subject: [PATCH 10/12] chore: change pushNotification entity reference, remove client manifest shortcuts --- .../database/entities/pushNotification.entity.ts | 2 +- .../push-notifications/push-notifications.service.ts | 2 +- packages/client/public/manifest.json | 11 +---------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/api/src/modules/database/entities/pushNotification.entity.ts b/packages/api/src/modules/database/entities/pushNotification.entity.ts index 79f6ee2f..b3d3e3f3 100644 --- a/packages/api/src/modules/database/entities/pushNotification.entity.ts +++ b/packages/api/src/modules/database/entities/pushNotification.entity.ts @@ -22,6 +22,6 @@ export class PushNotificationEntity extends BaseEntity { }) public auth: string; - @ManyToOne(() => UserEntity, (user) => user.refreshTokens, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, (user) => user.pushNotifications, { onDelete: 'CASCADE' }) public user: Promise; } diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index 837be294..eed5d651 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -9,7 +9,7 @@ import { SubscribePushNotificationDto } from './dto/subscribe-push-notification. @Injectable() export class PushNotificationsService { - private logger: Logger = new Logger('PushNotifications'); + private readonly logger: Logger = new Logger(PushNotificationsService.name); constructor( private readonly config: Config, diff --git a/packages/client/public/manifest.json b/packages/client/public/manifest.json index 317af663..7dfabc74 100644 --- a/packages/client/public/manifest.json +++ b/packages/client/public/manifest.json @@ -23,14 +23,5 @@ "start_url": ".", "display": "standalone", "theme_color": "#257D69", - "background_color": "#22343C", - "shortcuts": [ - { - "name": "How's weather today?", - "short_name": "Today", - "description": "View weather information for today", - "url": "/", - "icons": [{ "src": "logo192.png", "sizes": "192x192" }] - } - ] + "background_color": "#22343C" } From 5911f18ebc840b86fd70cd76d3680d0caf5ae196 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Sat, 31 Jul 2021 01:38:08 +0200 Subject: [PATCH 11/12] chore: adjust push-notifications service logs --- .../entities/pushNotification.entity.ts | 2 +- .../dto/subscribe-push-notification.dto.ts | 2 +- .../push-notifications.controller.ts | 14 +++++++--- .../push-notifications.service.ts | 26 ++++++++++++------- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/api/src/modules/database/entities/pushNotification.entity.ts b/packages/api/src/modules/database/entities/pushNotification.entity.ts index b3d3e3f3..5d9fdafb 100644 --- a/packages/api/src/modules/database/entities/pushNotification.entity.ts +++ b/packages/api/src/modules/database/entities/pushNotification.entity.ts @@ -23,5 +23,5 @@ export class PushNotificationEntity extends BaseEntity { public auth: string; @ManyToOne(() => UserEntity, (user) => user.pushNotifications, { onDelete: 'CASCADE' }) - public user: Promise; + public user: UserEntity; } diff --git a/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts b/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts index e4d751cd..dd7049d3 100644 --- a/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts +++ b/packages/api/src/modules/push-notifications/dto/subscribe-push-notification.dto.ts @@ -8,5 +8,5 @@ export class SubscribePushNotificationDto { subscription: PushSubscription; @ValidateNested() - userPromise: Promise; + user: UserEntity; } diff --git a/packages/api/src/modules/push-notifications/push-notifications.controller.ts b/packages/api/src/modules/push-notifications/push-notifications.controller.ts index 2309edd3..b97e0162 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.controller.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Get, Post } from '@nestjs/common'; import { PushSubscription } from 'web-push'; import { TokenPayload } from '../../interfaces/token-types'; @@ -15,12 +15,20 @@ export class PushNotificationsController { private readonly authService: AuthService, ) {} + @Get() + async send(): Promise { + await this.pushNotificationsService.send({ + title: 'test title', + body: 'test body', + }); + } + @Post() async subscribe( @CookiePayload() { sub }: TokenPayload, @Body() subscription: PushSubscription, ): Promise { - const userPromise = this.authService.getUser(sub); - await this.pushNotificationsService.subscribe({ subscription, userPromise }); + const user = await this.authService.getUser(sub); + await this.pushNotificationsService.subscribe({ subscription, user }); } } diff --git a/packages/api/src/modules/push-notifications/push-notifications.service.ts b/packages/api/src/modules/push-notifications/push-notifications.service.ts index eed5d651..a28435b0 100644 --- a/packages/api/src/modules/push-notifications/push-notifications.service.ts +++ b/packages/api/src/modules/push-notifications/push-notifications.service.ts @@ -21,16 +21,20 @@ export class PushNotificationsService { if (publicVapidKey && privateVapidKey) { webPush.setVapidDetails(`mailto:${replyTo}`, publicVapidKey, privateVapidKey); + this.logger.log('Web push vapid details initialized'); } } - async subscribe({ subscription, userPromise }: SubscribePushNotificationDto): Promise { - this.logger.log('New subscriber'); + async subscribe({ subscription, user }: SubscribePushNotificationDto): Promise { + const { endpoint, keys } = subscription; + const { p256dh, auth } = keys; + + this.logger.log(`New subscriber (email: ${user.email}, endpoint: ${endpoint})`); await this.pushNotificationRepository.create({ - user: userPromise, - endpoint: subscription.endpoint, - p256dh: subscription.keys.p256dh, - auth: subscription.keys.auth, + user, + endpoint, + p256dh, + auth, }); } @@ -43,12 +47,16 @@ export class PushNotificationsService { } async send({ title, body, options, roles }: SendPushNotificationDto): Promise { - this.logger.log('Sending...'); + this.logger.log( + `Sending (title: "${title}", body: "${body}"${ + roles ? `, roles: [${JSON.stringify(roles)}]` : '' + }${options ? `, options: ${JSON.stringify(options)}` : ''})`, + ); const subscriptions = await this.getSubscriptions(roles); if (!subscriptions.length) { - this.logger.log('No subscriptions'); + this.logger.log(`No subscriptions for roles: [${roles ? JSON.stringify(roles) : ' '}]`); return; } @@ -69,7 +77,7 @@ export class PushNotificationsService { await Promise.all(sendNotificationsPromises); const sendTotal = sendNotificationsPromises.length; - this.logger.log(`Sent ${sendTotal} ${sendTotal > 1 ? 'notifications' : 'notification'}`); + this.logger.log(`${sendTotal} ${sendTotal > 1 ? 'notifications' : 'notification'} sent`); } catch (error) { this.logger.error(error); } From cbc0328098a9139bf2d179b31382f865c32e36f3 Mon Sep 17 00:00:00 2001 From: "jakub.jozwiak" Date: Sat, 31 Jul 2021 01:41:15 +0200 Subject: [PATCH 12/12] chore: bump yarn.lock --- yarn.lock | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index d34dae2b..8937b1b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4947,6 +4947,13 @@ resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de" integrity sha512-fWG42pMJOL4jKsDDZZREnXLjc3UE0R8LOJfARWYg6U966rxDT7TYejYzLnUF5cvSObGg34nd0+H2wHHU5Omdfw== +"@types/web-push@^3.3.2": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15" + integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw== + dependencies: + "@types/node" "*" + "@types/webpack-env@^1.16.0": version "1.16.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.2.tgz#8db514b059c1b2ae14ce9d7bb325296de6a9a0fa" @@ -5859,7 +5866,7 @@ asap@^2.0.0, asap@~2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -asn1.js@^5.2.0: +asn1.js@^5.2.0, asn1.js@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== @@ -11418,6 +11425,13 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http_ece@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75" + integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA== + dependencies: + urlsafe-base64 "~1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -13487,6 +13501,15 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" @@ -13495,6 +13518,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -20739,6 +20770,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" + integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY= + use-composed-ref@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.1.0.tgz#9220e4e94a97b7b02d7d27eaeab0b37034438bbc" @@ -21032,6 +21068,18 @@ web-namespaces@^1.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +web-push@^3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1" + integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g== + dependencies: + asn1.js "^5.3.0" + http_ece "1.1.0" + https-proxy-agent "^5.0.0" + jws "^4.0.0" + minimist "^1.2.5" + urlsafe-base64 "^1.0.0" + web-resource-inliner@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz#ac30db8096931f20a7c1b3ade54ff444e2e20f7b" @@ -21044,7 +21092,7 @@ web-resource-inliner@^5.0.0: node-fetch "^2.6.0" valid-data-url "^3.0.0" -web-vitals@^2.0.1: +web-vitals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.0.tgz#ebf5428875ab5bfc1056c2e80cd177001287de7b" integrity sha512-npEyJP8jHf3J71t1tRTEtz9FeKp8H2udWJUUq5ykfPhhstr//TUxiYhIEzLNwk4zv2ybAilMn7v7N6Mxmuitmg==