diff --git a/.eslintrc.js b/.eslintrc.js index 39e1de8..0184e79 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -57,6 +57,5 @@ module.exports = { __DEV__: true, __LOCAL__: false, __TEST__: false, - __SENDGRID_API_KEY__: false, }, } diff --git a/jest/config.js b/jest/config.js index 92f5a54..3ac0720 100644 --- a/jest/config.js +++ b/jest/config.js @@ -23,7 +23,6 @@ module.exports = { __SERVER__: false, __LOCAL__: false, __TEST__: true, - __SENDGRID_API_KEY__: "", localStorage: { getItem: () => null, setItem: () => null, removeItem: () => null }, }, maxConcurrency: 50, diff --git a/package.json b/package.json index 4d696ff..723ccfc 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,11 @@ "@reduxjs/toolkit": "^1.6.0", "@sendgrid/mail": "^7.6.0", "@types/cookie-parser": "^1.4.2", + "@types/detect-node": "^2.0.0", "@types/js-cookie": "^3.0.1", "@types/lodash": "^4.14.177", + "@types/serviceworker": "^0.0.32", + "@types/web-push": "^3.3.2", "autoprefixer": "^10.2.6", "axios": "^0.21.1", "bcrypt": "^5.0.1", @@ -86,6 +89,7 @@ "cookie-parser": "^1.4.6", "core-js": "^3.15.2", "cross-env": "^7.0.3", + "detect-node": "^2.1.0", "express": "^4.17.1", "fs": "^0.0.1-security", "google-auth-library": "^7.10.1", @@ -114,7 +118,8 @@ "redux-devtools-extension": "^2.13.9", "redux-thunk": "^2.3.0", "serialize-javascript": "^6.0.0", - "serve-favicon": "^2.5.0" + "serve-favicon": "^2.5.0", + "web-push": "^3.4.5" }, "devDependencies": { "@babel/core": "^7.14.6", diff --git a/src/components/Notifications/index.tsx b/src/components/Notifications/index.tsx index bb381a8..02b8ce1 100644 --- a/src/components/Notifications/index.tsx +++ b/src/components/Notifications/index.tsx @@ -1,20 +1,30 @@ import _ from "lodash" -import React, { memo, useCallback, useState } from "react" -import { AppDispatch } from "../../store" +import React, { memo, useCallback, useEffect, useRef, useState } from "react" +import isNode from "detect-node" +import { shallowEqual, useSelector } from "react-redux" +import { AppDispatch, AppState } from "../../store" import { fetchVolunteerNotifsSet } from "../../store/volunteerNotifsSet" -import { VolunteerNotifs } from "../../services/volunteers" import styles from "./styles.module.scss" import { logoutUser } from "../../store/auth" import { unsetJWT } from "../../services/auth" +import { VolunteerNotifs } from "../../services/volunteers" interface Props { dispatch: AppDispatch jwt: string - // eslint-disable-next-line react/require-default-props - volunteerNotifs?: VolunteerNotifs } +let prevNotifs: VolunteerNotifs | undefined + +const Notifications = ({ dispatch, jwt }: Props): JSX.Element | null => { + const volunteerNotifs = useSelector((state: AppState) => { + const notifs = state.volunteerNotifsSet.entity + if (notifs) { + prevNotifs = notifs + return notifs + } + return prevNotifs + }, shallowEqual) -const Notifications = ({ dispatch, jwt, volunteerNotifs }: Props): JSX.Element => { const hidden = volunteerNotifs?.hiddenNotifs || [] const notifs: JSX.Element[] = [] @@ -78,41 +88,46 @@ const Notifications = ({ dispatch, jwt, volunteerNotifs }: Props): JSX.Element =
Si les conditions sanitaires te le permettent, souhaites-tu être bénévole à PeL 2022 ?
- {" "} - -
- {" "} - Oui -
- {" "} - Non -
- {" "} - Je ne sais pas encore -
+ + + + {participation === "peut-etre" ? (
On te le reproposera dans quelques temps. @@ -149,6 +164,212 @@ Rejoindre les 86 bénévoles déjà présents sur le serveur se fait en cliquant Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique. */ + const [acceptsNotifs, setAcceptsNotifs] = useState("") + const [notifMessage, setNotifMessage] = useState("") + + const mounted = useRef(false) + useEffect(() => { + if (mounted.current) { + return + } + mounted.current = true + if (!isNode) { + if (volunteerNotifs?.acceptsNotifs === "oui") { + navigator.serviceWorker.ready.then((registration) => + registration.pushManager.getSubscription().then((existedSubscription) => { + const doesAcceptNotifs = + _.isEqual( + JSON.parse(JSON.stringify(existedSubscription)), + JSON.parse(volunteerNotifs?.pushNotifSubscription) + ) && volunteerNotifs?.acceptsNotifs === "oui" + setAcceptsNotifs(doesAcceptNotifs ? "oui" : "non") + }) + ) + } else { + setAcceptsNotifs("non") + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const onChangePushNotifs = useCallback( + async (event: React.ChangeEvent): Promise => { + event.preventDefault() + if (isNode) { + return + } + + if (!("serviceWorker" in navigator)) { + return + } + + const isChecked = event.target.value === "oui" + if (!isChecked) { + setNotifMessage("") + setAcceptsNotifs("non") + dispatch( + fetchVolunteerNotifsSet(jwt, 0, { + acceptsNotifs: "non", + }) + ) + return + } + + const registration = await navigator.serviceWorker.ready + + if (!registration.pushManager) { + setNotifMessage( + "Il y a un problème avec le push manager. Il faudrait utiliser un navigateur plus récent !" + ) + return + } + + const convertedVapidKey = urlBase64ToUint8Array( + process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY + ) + + function urlBase64ToUint8Array(base64String?: string) { + if (!base64String) { + return "" + } + const padding = "=".repeat((4 - (base64String.length % 4)) % 4) + // eslint-disable-next-line + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/") + + const rawData = atob(base64) + const outputArray = new Uint8Array(rawData.length) + + for (let i = 0; i < rawData.length; i += 1) { + outputArray[i] = rawData.charCodeAt(i) + } + return outputArray + } + + if (!convertedVapidKey) { + console.error("No convertedVapidKey available") + } + + try { + const existedSubscription = await registration.pushManager.getSubscription() + + if (existedSubscription === null) { + // No subscription detected, make a request + try { + const newSubscription = await registration.pushManager.subscribe({ + applicationServerKey: convertedVapidKey, + userVisibleOnly: true, + }) + // New subscription added + if ( + volunteerNotifs?.acceptsNotifs === "oui" && + !subscriptionEqualsSave( + newSubscription, + volunteerNotifs?.pushNotifSubscription + ) + ) { + setNotifMessage( + "Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera." + ) + } else { + setNotifMessage("C'est enregistré !") + } + + setAcceptsNotifs("oui") + dispatch( + fetchVolunteerNotifsSet(jwt, 0, { + pushNotifSubscription: JSON.stringify(newSubscription), + acceptsNotifs: "oui", + }) + ) + } catch (_e) { + if (Notification.permission !== "granted") { + setNotifMessage( + "Mince tu as bloqué toutes notifications pour le site des bénévoles, l'exact opposé de ce qu'il fallait :) Pour annuler ça, les instructions sont ici : https://support.pushcrew.com/support/solutions/articles/9000098467-how-to-unblock-notifications-from-a-website-that-you-once-blocked-" + ) + } else { + setNotifMessage( + "Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !" + ) + } + } + } else { + // Existed subscription detected + if ( + volunteerNotifs?.acceptsNotifs === "oui" && + !subscriptionEqualsSave( + existedSubscription, + volunteerNotifs?.pushNotifSubscription + ) + ) { + setNotifMessage( + "Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera." + ) + } else { + setNotifMessage("C'est enregistré !") + } + + setAcceptsNotifs("oui") + dispatch( + fetchVolunteerNotifsSet(jwt, 0, { + pushNotifSubscription: JSON.stringify(existedSubscription), + acceptsNotifs: "oui", + }) + ) + } + } catch (_e) { + setNotifMessage( + "Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !" + ) + } + }, + [dispatch, jwt, volunteerNotifs] + ) + + function subscriptionEqualsSave(toCheck: PushSubscription, save: string | undefined): boolean { + if (!save) { + return !toCheck + } + return _.isEqual(JSON.parse(JSON.stringify(toCheck)), JSON.parse(save)) + } + + if (notifs.length === 0) { + notifs.push( +
+
+
+
+ +
{notifMessage}
+
+
+
+
+ ) + } + const onClick = useCallback( (event: React.SyntheticEvent): void => { event.preventDefault() @@ -166,6 +387,10 @@ Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique.
) + if (volunteerNotifs === undefined) { + return null + } + return
{notifs.map((t) => t).reduce((prev, curr) => [prev, curr])}
} diff --git a/src/components/Notifications/styles.module.scss b/src/components/Notifications/styles.module.scss index 463c191..6dd26fd 100755 --- a/src/components/Notifications/styles.module.scss +++ b/src/components/Notifications/styles.module.scss @@ -7,6 +7,8 @@ .notificationsContent { @include page-content-wrapper; + + background-color: #ece3df; } .notifIntro { @@ -19,6 +21,7 @@ label { display: block; margin-left: 5px; + margin-top: 7px; } select, input { @@ -34,6 +37,12 @@ text-align: center; } +.message { + margin-top: 10px; + color: rgb(11, 138, 0); + text-align: center; +} + .error { margin-top: 10px; color: rgb(255, 0, 0); diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 7a9583a..0715a2e 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -12,20 +12,14 @@ export type Props = RouteComponentProps const HomePage: FC = (): JSX.Element => { const dispatch = useDispatch() - const readyStatus = useSelector((state: AppState) => state.volunteerNotifsSet.readyStatus) - const volunteerNotifs = useSelector( - (state: AppState) => state.volunteerNotifsSet.entity, - shallowEqual - ) const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual) const jwt = useSelector((state: AppState) => state.auth.jwt, shallowEqual) - if (!readyStatus || readyStatus === "idle" || readyStatus === "request") - return

Loading...

+ if (jwt === undefined) return

Loading...

if (jwt) { - return + return } return (
diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index b3bc02c..d777ad8 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -8,7 +8,8 @@ import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadshee const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") -const REMOTE_UPDATE_DELAY = 20000 +const REMOTE_UPDATE_DELAY = 40000 +const DELAY_AFTER_QUERY = 1000 export type ElementWithId = { id: number } & ElementNoId @@ -26,14 +27,6 @@ export const sheetNames = new SheetNames() // eslint-disable-next-line @typescript-eslint/ban-types type SheetList = { [sheetName in keyof SheetNames]?: Sheet> } const sheetList: SheetList = {} -setInterval( - () => - // eslint-disable-next-line @typescript-eslint/ban-types - Object.values(sheetList).forEach((sheet: Sheet>) => - sheet.dbUpdate() - ), - REMOTE_UPDATE_DELAY -) export function getSheet< // eslint-disable-next-line @typescript-eslint/ban-types @@ -48,7 +41,14 @@ export function getSheet< sheetList[sheetName] = new Sheet(sheetName, specimen, translation) } - return sheetList[sheetName] as Sheet + const sheet = sheetList[sheetName] as Sheet + + setTimeout( + () => setInterval(() => sheet.dbUpdate(), REMOTE_UPDATE_DELAY), + 1000 * Object.values(sheetList).length + ) + + return sheet } export class Sheet< @@ -83,7 +83,7 @@ export class Sheet< (englishProp: string) => (specimen as any)[englishProp] ) as Element - this.dbLoad() + setTimeout(() => this.dbLoad(), 100 * Object.values(sheetList).length) } async getList(): Promise { @@ -198,6 +198,8 @@ export class Sheet< if (!row) { // eslint-disable-next-line no-await-in-loop await sheet.addRow(stringifiedRow) + // eslint-disable-next-line no-await-in-loop + await delayDBAccess() } else { const keys = Object.keys(stringifiedRow) const sameCells = _.every(keys, (key: keyof Element) => { @@ -211,6 +213,8 @@ export class Sheet< }) // eslint-disable-next-line no-await-in-loop await row.save() + // eslint-disable-next-line no-await-in-loop + await delayDBAccess() } } @@ -222,6 +226,8 @@ export class Sheet< if (rows[rowToDelete]) { // eslint-disable-next-line no-await-in-loop await rows[rowToDelete].delete() + // eslint-disable-next-line no-await-in-loop + await delayDBAccess() } } } @@ -233,6 +239,7 @@ export class Sheet< // Load sheet into an array of objects const rows = (await sheet.getRows()) as StringifiedElement[] + await delayDBAccess() const elements: Element[] = [] if (!rows[0]) { throw new Error(`No column types defined in sheet ${this.name}`) @@ -495,3 +502,7 @@ function parseDate(value: string): Date { } return new Date(+matchDate[1], +matchDate[2] - 1, +matchDate[3]) } + +async function delayDBAccess(): Promise { + return new Promise((resolve) => setTimeout(resolve, DELAY_AFTER_QUERY)) +} diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index a5a0fbd..159498d 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -112,6 +112,8 @@ export const volunteerNotifsSet = expressAccessor.set( adult: newVolunteer.adult, active: newVolunteer.active, hiddenNotifs: newVolunteer.hiddenNotifs, + pushNotifSubscription: newVolunteer.pushNotifSubscription, + acceptsNotifs: newVolunteer.acceptsNotifs, } as VolunteerNotifs, } } diff --git a/src/server/index.ts b/src/server/index.ts index c2522d5..d9cadec 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -27,6 +27,7 @@ import { volunteerForgot, } from "./gsheets/volunteers" import config from "../config" +import notificationsSubscribe from "./notificationsSubscribe" const app = express() @@ -70,6 +71,9 @@ app.post("/VolunteerSet", secure as RequestHandler, volunteerSet) // UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet) app.post("/VolunteerNotifsSet", secure as RequestHandler, volunteerNotifsSet) +// Push notification subscription +app.post("/notifications/subscribe", notificationsSubscribe) + // Use React server-side rendering middleware app.get("*", ssr) diff --git a/src/server/notificationsSubscribe.ts b/src/server/notificationsSubscribe.ts new file mode 100644 index 0000000..2c95c64 --- /dev/null +++ b/src/server/notificationsSubscribe.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from "express" +import webpush from "web-push" + +const publicKey = process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY +const privateKey = process.env.FORCE_ORANGE_PRIVATE_VAPID_KEY +if (publicKey && privateKey) { + webpush.setVapidDetails("mailto: contact@parisestludique.fr", publicKey, privateKey) +} + +export default function notificationsSubscribe( + request: Request, + response: Response, + _next: NextFunction +): void { + const subscription = request.body + + console.log(subscription) + + const payload = JSON.stringify({ + title: "Hello!", + body: "It works.", + }) + webpush + .sendNotification(subscription, payload) + .then((result) => console.log(result)) + .catch((e) => console.log(e.stack)) + + response.status(200).json({ success: true }) +} diff --git a/src/server/renderHtml.ts b/src/server/renderHtml.ts index dff35ab..85cc6f8 100755 --- a/src/server/renderHtml.ts +++ b/src/server/renderHtml.ts @@ -31,6 +31,37 @@ export default ( ${extractor.getStyleTags()} + +
${htmlContent}
diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index 76592cf..9956420 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -28,6 +28,10 @@ export class Volunteer { password1 = "" password2 = "" + + pushNotifSubscription = "" + + acceptsNotifs = "" } export const translationVolunteer: { [k in keyof Volunteer]: string } = { @@ -45,6 +49,8 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = { created: "creation", password1: "passe1", password2: "passe2", + pushNotifSubscription: "pushNotifSubscription", + acceptsNotifs: "accepteLesNotifs", } const elementName = "Volunteer" @@ -64,6 +70,8 @@ export const volunteerExample: Volunteer = { created: new Date(0), password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + pushNotifSubscription: "", + acceptsNotifs: "", } export const emailRegexp = @@ -97,6 +105,8 @@ export interface VolunteerNotifs { adult: number active: string hiddenNotifs: number[] + pushNotifSubscription: string + acceptsNotifs: string } export const volunteerNotifsSet = serviceAccessors.securedCustomPost<[number, Partial]>("NotifsSet") diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 5b6908b..48d8021 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -3,7 +3,6 @@ declare const __SERVER__: boolean declare const __DEV__: boolean declare const __LOCAL__: boolean declare const __TEST__: boolean -declare const __SENDGRID_API_KEY__: string declare module "*.svg" declare module "*.gif" @@ -21,10 +20,15 @@ declare namespace NodeJS { __DEV__: boolean __LOCAL__: boolean __TEST__: boolean - __SENDGRID_API_KEY__: string $RefreshReg$: () => void $RefreshSig$$: () => void } + + interface ProcessEnv { + SENDGRID_API_KEY?: string + FORCE_ORANGE_PUBLIC_VAPID_KEY?: string + FORCE_ORANGE_PRIVATE_VAPID_KEY?: string + } } interface Window { diff --git a/webpack/base.config.ts b/webpack/base.config.ts index 42f5670..a7962ba 100644 --- a/webpack/base.config.ts +++ b/webpack/base.config.ts @@ -125,4 +125,28 @@ const config = (isWeb = false): Configuration => ({ }, }) +export function getClientEnvironment(allowedKeys: string[]): any { + const raw = Object.keys(process.env) + // Custom regex to allow only a certain category of variables available to the application + .filter((key) => allowedKeys.indexOf(key) >= 0) + .reduce( + (env: any, key: string) => { + env[key] = process.env[key] + return env + }, + { + NODE_ENV: process.env.NODE_ENV || "development", + } + ) + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = { + "process.env": Object.keys(raw).reduce((env: any, key: string) => { + env[key] = JSON.stringify(raw[key]) + return env + }, {}), + } + + return stringified +} + export default config diff --git a/webpack/client.config.ts b/webpack/client.config.ts index b9ae631..9aff5e9 100755 --- a/webpack/client.config.ts +++ b/webpack/client.config.ts @@ -7,7 +7,7 @@ import CompressionPlugin from "compression-webpack-plugin" import ImageMinimizerPlugin from "image-minimizer-webpack-plugin" import merge from "webpack-merge" -import baseConfig, { isDev } from "./base.config" +import baseConfig, { isDev, getClientEnvironment } from "./base.config" const getPlugins = () => { let plugins = [ @@ -16,6 +16,7 @@ const getPlugins = () => { filename: isDev ? "[name].css" : "[name].[contenthash].css", chunkFilename: isDev ? "[id].css" : "[id].[contenthash].css", }), + new webpack.DefinePlugin(getClientEnvironment(["FORCE_ORANGE_PUBLIC_VAPID_KEY"])), ] if (isDev) diff --git a/yarn.lock b/yarn.lock index 09f3529..c3ac9c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1590,6 +1590,11 @@ dependencies: postcss "5 - 7" +"@types/detect-node@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/detect-node/-/detect-node-2.0.0.tgz#696e024ddd105c72bbc6a2e3f97902a2886f2c3f" + integrity sha512-+BozjlbPTACYITf1PWf62HLtDV79HbmZosUN1mv1gGrnjDCRwBXkDKka1sf6YQJvspmfPXVcy+X6tFW62KteeQ== + "@types/eslint-scope@^3.7.0": version "3.7.1" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e" @@ -1956,6 +1961,11 @@ "@types/mime" "^1" "@types/node" "*" +"@types/serviceworker@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/serviceworker/-/serviceworker-0.0.32.tgz#c262fed4e394f7f3e3787a597f8ecb5c12161626" + integrity sha512-3B1uuyZQ86m9C3BBhNfl4mJs+Whdm1ozPyeLm1Z5hX4cKH0JLPfD9CBFEfdXmCMeNdbcJGfBZXYNXzlrpVrwQg== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -1997,6 +2007,13 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@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-bundle-analyzer@^4.4.1": version "4.4.1" resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz#bcc2501be10c8cdd1d170bc6b7847b3321f20440" @@ -2600,6 +2617,16 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +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== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -2909,6 +2936,11 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bn.js@^4.0.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -3987,6 +4019,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -5747,6 +5784,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-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -7593,6 +7637,11 @@ mini-css-extract-plugin@^2.1.0: dependencies: schema-utils "^4.0.0" +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -10844,6 +10893,11 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= +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= + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -10952,6 +11006,18 @@ watchpack@^2.2.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +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" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"