diff --git a/packages/app-root/.eslintrc.json b/packages/app-root/.eslintrc.json new file mode 100644 index 0000000000..5522f86278 --- /dev/null +++ b/packages/app-root/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "extends": [ + "next/core-web-vitals", + "plugin:jsx-a11y/recommended", + "plugin:@next/next/recommended" + ], + "rules": { + "consistent-return": "error" + }, + "overrides": [ + { + "files": [ + "src/**/*.stories.js" + ], + "rules": { + "import/no-anonymous-default-export": "off" + } + } + ] +} diff --git a/packages/app-root/next.config.mjs b/packages/app-root/next.config.mjs index a2a6e99052..026a6646f5 100644 --- a/packages/app-root/next.config.mjs +++ b/packages/app-root/next.config.mjs @@ -4,6 +4,10 @@ const bundleAnalyzer = withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true', }) -const nextConfig = {} +const nextConfig = { + experimental: { + optimizePackageImports: ['@zooniverse/react-components', 'grommet', 'grommet-icons'], + } +} export default bundleAnalyzer(nextConfig) diff --git a/packages/app-root/package.json b/packages/app-root/package.json index 7f7bc2adea..370d1c37db 100644 --- a/packages/app-root/package.json +++ b/packages/app-root/package.json @@ -6,9 +6,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "APP_ENV=${APP_ENV:-development} PANOPTES_ENV=${PANOPTES_ENV:-staging} node server/server.js", "build": "next build", - "start": "next start", + "start": "NODE_ENV=${NODE_ENV:-production} PANOPTES_ENV=${PANOPTES_ENV:-production} node server/server.js", "lint": "next lint" }, "type": "module", @@ -17,17 +17,24 @@ "@zooniverse/grommet-theme": "~3.1.1", "@zooniverse/panoptes-js": "~0.4.1", "@zooniverse/react-components": "~1.6.1", + "express": "~4.18.2", "grommet": "~2.33.2", "grommet-icons": "~4.11.0", + "newrelic": "~11.2.0", "next": "~13.5.5", + "panoptes-client": "~5.5.6", "react": "~18.2.0", "react-dom": "~18.2.0", - "styled-components": "~5.3.10" + "styled-components": "~5.3.10", + "swr": "~2.2.4" }, "engines": { "node": ">=20.5" }, "devDependencies": { - "@next/bundle-analyzer": "~13.5.4" + "@next/bundle-analyzer": "~13.5.5", + "eslint-config-next": "~13.5.5", + "eslint-plugin-jsx-a11y": "~6.7.0", + "selfsigned": "~2.1.1" } } diff --git a/packages/app-root/server/server.js b/packages/app-root/server/server.js new file mode 100644 index 0000000000..e69efba623 --- /dev/null +++ b/packages/app-root/server/server.js @@ -0,0 +1,53 @@ +if (process.env.NEWRELIC_LICENSE_KEY) { + await import('newrelic') +} + +import express from 'express' +import next from 'next' + +const port = parseInt(process.env.PORT, 10) || 3000 +const dev = process.env.NODE_ENV !== 'production' + +const APP_ENV = process.env.APP_ENV || 'development' + +const hostnames = { + development: 'local.zooniverse.org', + branch: 'fe-project-branch.preview.zooniverse.org', + staging: 'frontend.preview.zooniverse.org', + production : 'www.zooniverse.org' +} +const hostname = hostnames[APP_ENV] + +const app = next({ dev, hostname, port }) +const handle = app.getRequestHandler() + +app.prepare().then(async () => { + const server = express() + + server.get('*', (req, res) => { + return handle(req, res) + }) + + let selfsigned + try { + selfsigned = await import('selfsigned') + } catch (error) { + console.error(error) + } + if (APP_ENV === 'development' && selfsigned) { + const https = await import('https') + + const attrs = [{ name: 'commonName', value: hostname }]; + const { cert, private: key } = selfsigned.generate(attrs, { days: 365 }) + return https.createServer({ cert, key }, server) + .listen(port, err => { + if (err) throw err + console.log(`> Ready on https://${hostname}:${port}`) + }) + } else { + return server.listen(port, err => { + if (err) throw err + console.log(`> Ready on http://${hostname}:${port}`) + }) + } +}) diff --git a/packages/app-root/src/app/about/page.js b/packages/app-root/src/app/about/page.js index 197074ea7e..ad09698f91 100644 --- a/packages/app-root/src/app/about/page.js +++ b/packages/app-root/src/app/about/page.js @@ -1,5 +1,8 @@ export default function AboutPage() { return ( +
+

This is the section header.

+

This is lib-content-pages

diff --git a/packages/app-root/src/app/projects/page.js b/packages/app-root/src/app/projects/page.js index f4575f3406..f7fc4be5b3 100644 --- a/packages/app-root/src/app/projects/page.js +++ b/packages/app-root/src/app/projects/page.js @@ -1,5 +1,8 @@ export default function ProjectPage() { return ( +
+

This is the project header.

+

This is lib-project

diff --git a/packages/app-root/src/components/PageContextProviders.js b/packages/app-root/src/components/PageContextProviders.js new file mode 100644 index 0000000000..a0dfffddc7 --- /dev/null +++ b/packages/app-root/src/components/PageContextProviders.js @@ -0,0 +1,42 @@ +'use client' + +import zooTheme from '@zooniverse/grommet-theme' +import { Grommet } from 'grommet' +import { createGlobalStyle } from 'styled-components' + +import { PanoptesAuthContext } from '../contexts' +import { useAdminMode, usePanoptesUser } from '../hooks' + +const GlobalStyle = createGlobalStyle` + body { + margin: 0; + } +` + +/** + Context for every page: + - global page styles. + - Zooniverse Grommet theme. + - Panoptes auth (user account and admin mode.) +*/ +export default function PageContextProviders({ children }) { + const { data: user, error, isLoading } = usePanoptesUser() + const { adminMode, toggleAdmin } = useAdminMode(user) + const authContext = { adminMode, error, isLoading, toggleAdmin, user } + + return ( + + + + {children} + + + ) + +} \ No newline at end of file diff --git a/packages/app-root/src/components/PageFooter.js b/packages/app-root/src/components/PageFooter.js new file mode 100644 index 0000000000..934dd72a4c --- /dev/null +++ b/packages/app-root/src/components/PageFooter.js @@ -0,0 +1,15 @@ +'use client' +import { AdminCheckbox, ZooFooter } from '@zooniverse/react-components' +import { useContext } from 'react' + +import { PanoptesAuthContext } from '../contexts' + +export default function PageFooter() { + const { adminMode, toggleAdmin, user } = useContext(PanoptesAuthContext) + + return ( + : null} + /> + ) +} \ No newline at end of file diff --git a/packages/app-root/src/components/PageHeader.js b/packages/app-root/src/components/PageHeader.js new file mode 100644 index 0000000000..82e1119a13 --- /dev/null +++ b/packages/app-root/src/components/PageHeader.js @@ -0,0 +1,27 @@ +'use client' +import { ZooHeader } from '@zooniverse/react-components' +import { useContext } from 'react' + +import { + useUnreadMessages, + useUnreadNotifications +} from '../hooks' + +import { PanoptesAuthContext } from '../contexts' + +export default function PageHeader() { + const { adminMode, user } = useContext(PanoptesAuthContext) + const { data: unreadMessages }= useUnreadMessages(user) + const { data: unreadNotifications }= useUnreadNotifications(user) + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/packages/app-root/src/components/RootLayout.js b/packages/app-root/src/components/RootLayout.js index 6d7d3e1471..8a30856aa6 100644 --- a/packages/app-root/src/components/RootLayout.js +++ b/packages/app-root/src/components/RootLayout.js @@ -1,37 +1,15 @@ -'use client' -/** - * Note that all child components are now client components. - * If we want children of RootLayout to be server components - * a ZooHeaderContainer and ZooFooterContainer could be created instead. - */ - -import { createGlobalStyle } from 'styled-components' -import { Grommet } from 'grommet' -import zooTheme from '@zooniverse/grommet-theme' -import ZooHeader from '@zooniverse/react-components/ZooHeader' -import ZooFooter from '@zooniverse/react-components/ZooFooter' - -const GlobalStyle = createGlobalStyle` - body { - margin: 0; - } -` +import PageContextProviders from './PageContextProviders.js' +import PageHeader from './PageHeader.js' +import PageFooter from './PageFooter.js' export default function RootLayout({ children }) { return ( - - - + + {children} - - + + ) } diff --git a/packages/app-root/src/contexts/PanoptesAuthContext.js b/packages/app-root/src/contexts/PanoptesAuthContext.js new file mode 100644 index 0000000000..c39b6e57f6 --- /dev/null +++ b/packages/app-root/src/contexts/PanoptesAuthContext.js @@ -0,0 +1,5 @@ +import { createContext } from 'react' + +const PanoptesAuthContext = createContext({}) + +export default PanoptesAuthContext diff --git a/packages/app-root/src/contexts/index.js b/packages/app-root/src/contexts/index.js new file mode 100644 index 0000000000..5c21818a37 --- /dev/null +++ b/packages/app-root/src/contexts/index.js @@ -0,0 +1 @@ +export { default as PanoptesAuthContext } from './PanoptesAuthContext.js' diff --git a/packages/app-root/src/helpers/fetchPanoptesUser.js b/packages/app-root/src/helpers/fetchPanoptesUser.js new file mode 100644 index 0000000000..caa89f3676 --- /dev/null +++ b/packages/app-root/src/helpers/fetchPanoptesUser.js @@ -0,0 +1,38 @@ +import auth from 'panoptes-client/lib/auth' +import { auth as authHelpers } from '@zooniverse/panoptes-js' + +/** + Get a Panoptes user from a Panoptes JSON Web Token (JWT), if we have one, or from + the Panoptes API otherwise. +*/ +export default async function fetchPanoptesUser({ user: storedUser }) { + try { + const jwt = await auth.checkBearerToken() + /* + `crypto.subtle` is needed to decrypt the Panoptes JWT. + It will only exist for https:// URLs. + */ + const isSecure = crypto?.subtle + if (jwt && isSecure) { + /* + avatar_src isn't encoded in the Panoptes JWT, so we need to add it. + https://github.com/zooniverse/panoptes/issues/4217 + */ + const { user, error } = await authHelpers.decodeJWT(jwt) + if (user) { + const { admin, display_name, id, login } = user + return { + avatar_src: storedUser.avatar_src, + ...user + } + } + if (error) { + throw error + } + } + } catch (error) { + console.log(error) + } + const { admin, avatar_src, display_name, id, login } = await auth.checkCurrent() + return { admin, avatar_src, display_name, id, login } +} diff --git a/packages/app-root/src/helpers/index.js b/packages/app-root/src/helpers/index.js new file mode 100644 index 0000000000..025ab766d9 --- /dev/null +++ b/packages/app-root/src/helpers/index.js @@ -0,0 +1 @@ +export { default as fetchPanoptesUser } from './fetchPanoptesUser.js' diff --git a/packages/app-root/src/hooks/index.js b/packages/app-root/src/hooks/index.js new file mode 100644 index 0000000000..e6419b719c --- /dev/null +++ b/packages/app-root/src/hooks/index.js @@ -0,0 +1,4 @@ +export { default as useAdminMode } from './useAdminMode.js' +export { default as usePanoptesUser } from './usePanoptesUser.js' +export { default as useUnreadMessages } from './useUnreadMessages.js' +export { default as useUnreadNotifications } from './useUnreadNotifications.js' diff --git a/packages/app-root/src/hooks/useAdminMode.js b/packages/app-root/src/hooks/useAdminMode.js new file mode 100644 index 0000000000..627cc485d3 --- /dev/null +++ b/packages/app-root/src/hooks/useAdminMode.js @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react' + +const isBrowser = typeof window !== 'undefined' +const localStorage = isBrowser ? window.localStorage : null +const storedAdminFlag = !!localStorage?.getItem('adminFlag') +const adminBorderImage = 'repeating-linear-gradient(45deg,#000,#000 25px,#ff0 25px,#ff0 50px) 5' + +export default function useAdminMode(user) { + const [adminState, setAdminState] = useState(storedAdminFlag) + const adminMode = user?.admin && adminState + + useEffect(function onUserChange() { + const isAdmin = user?.admin + if (isAdmin) { + const adminFlag = !!localStorage?.getItem('adminFlag') + setAdminState(adminFlag) + } else { + localStorage?.removeItem('adminFlag') + } + }, [user?.admin]) + + useEffect(function onAdminChange() { + if (adminMode) { + document.body.style.border = '5px solid' + document.body.style.borderImage = adminBorderImage + } + return () => { + document.body.style.border = '' + document.body.style.borderImage = '' + } + }, [adminMode]) + + function toggleAdmin() { + let newAdminState = !adminState + setAdminState(newAdminState) + if (newAdminState) { + localStorage?.setItem('adminFlag', true) + } else { + localStorage?.removeItem('adminFlag') + } + } + + return { adminMode, toggleAdmin } +} \ No newline at end of file diff --git a/packages/app-root/src/hooks/usePanoptesUser.js b/packages/app-root/src/hooks/usePanoptesUser.js new file mode 100644 index 0000000000..4203507a95 --- /dev/null +++ b/packages/app-root/src/hooks/usePanoptesUser.js @@ -0,0 +1,66 @@ +import auth from 'panoptes-client/lib/auth' +import { useEffect } from 'react' +import useSWR from 'swr' + +import { fetchPanoptesUser } from '../helpers' + +const isBrowser = typeof window !== 'undefined' + +const SWROptions = { + revalidateIfStale: true, + revalidateOnMount: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + refreshInterval: 0 +} + +if (isBrowser) { + auth.checkCurrent() +} + +const localStorage = isBrowser ? window.localStorage : null +const storedUserJSON = localStorage?.getItem('panoptes-user') +let storedUser = storedUserJSON && JSON.parse(storedUserJSON) +/* + Null users crash the ZooHeader component. + Set them to undefined for now. +*/ +if (storedUser === null) { + storedUser = undefined +} + +export default function usePanoptesUser() { + const key = { + user: storedUser, + endpoint: '/me' + } + + /* + `useSWR` here will always return the same stale user object. + See https://github.com/zooniverse/panoptes-javascript-client/issues/207 + */ + const { data, error, isLoading } = useSWR(key, fetchPanoptesUser, SWROptions) + if (data) { + storedUser = data + } + + useEffect(function subscribeToAuthChanges() { + auth.listen('change', auth.checkCurrent) + + return function () { + auth.stopListening('change', auth.checkCurrent) + } + }, []) + + useEffect(function persistUserInStorage() { + if (data) { + localStorage?.setItem('panoptes-user', JSON.stringify(data)) + } + + return () => { + localStorage?.removeItem('panoptes-user') + } + }, [data]) + + return { data: storedUser, error, isLoading } +} diff --git a/packages/app-root/src/hooks/useUnreadMessages.js b/packages/app-root/src/hooks/useUnreadMessages.js new file mode 100644 index 0000000000..b92af111f3 --- /dev/null +++ b/packages/app-root/src/hooks/useUnreadMessages.js @@ -0,0 +1,55 @@ +import { talkAPI } from '@zooniverse/panoptes-js' +import auth from 'panoptes-client/lib/auth' +import useSWR from 'swr' + +const SWROptions = { + revalidateIfStale: true, + revalidateOnMount: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + refreshInterval: 0 +} + +async function fetchUnreadMessageCount({ endpoint = '/conversations' }) { + const token = await auth.checkBearerToken() + const authorization = `Bearer ${token}` + if (!authorization) return undefined + + let unreadConversationsIds = [] + + async function getConversations (page = 1) { + const query = { + unread: true, + page: page + } + + const response = await talkAPI.get(endpoint, query, { authorization }) + const { meta, conversations } = response?.body || {} + + if (conversations && conversations.length) { + unreadConversationsIds = unreadConversationsIds.concat( + conversations.map(conversation => conversation.id) + ) + } + + if (meta?.next_page) { + return getConversations(meta.next_page) + } + + return unreadConversationsIds + } + + await getConversations(1) + return unreadConversationsIds.length +} + +export default function useUnreadMessages(user) { + let key = null + if (user) { + key = { + user, + endpoint: '/conversations' + } + } + return useSWR(key, fetchUnreadMessageCount, SWROptions) +} diff --git a/packages/app-root/src/hooks/useUnreadNotifications.js b/packages/app-root/src/hooks/useUnreadNotifications.js new file mode 100644 index 0000000000..ab098060da --- /dev/null +++ b/packages/app-root/src/hooks/useUnreadNotifications.js @@ -0,0 +1,36 @@ +import { talkAPI } from '@zooniverse/panoptes-js' +import auth from 'panoptes-client/lib/auth' +import useSWR from 'swr' + +const SWROptions = { + revalidateIfStale: true, + revalidateOnMount: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + refreshInterval: 0 +} + +async function fetchUnreadNotificationsCount({ endpoint = '/notifications' }) { + const token = await auth.checkBearerToken() + const authorization = `Bearer ${token}` + if (!authorization) return undefined + + const query = { + delivered: false, + page_size: 1 + } + + const response = await talkAPI.get(endpoint, query, { authorization }) + return response?.body?.meta?.notifications?.count +} + +export default function useUnreadNotifications(user) { + let key = null + if (user) { + key = { + user, + endpoint: '/notifications' + } + } + return useSWR(key, fetchUnreadNotificationsCount, SWROptions) +} diff --git a/yarn.lock b/yarn.lock index a6918894d0..6edc4d73fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1983,7 +1983,7 @@ resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== -"@grpc/grpc-js@^1.9.4": +"@grpc/grpc-js@^1.8.10", "@grpc/grpc-js@^1.9.4": version "1.9.7" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.7.tgz#7d0e29bc162287bee2523901c9bc9320d8402397" integrity sha512-yMaA/cIsRhGzW3ymCNpdlPcInXcovztlgu/rirThj2b87u3RzWUszliOqZ/pldy7yhmJPS8uwog+kZSTa4A0PQ== @@ -2260,12 +2260,12 @@ pump "^3.0.0" tar-fs "^2.1.1" -"@newrelic/aws-sdk@^7.0.2": +"@newrelic/aws-sdk@^7.0.0", "@newrelic/aws-sdk@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@newrelic/aws-sdk/-/aws-sdk-7.0.2.tgz#e93f1796c89be8323a75f3d7ec45b1bdd5a29292" integrity sha512-nT19hzId0MbjR3v1ks5YetvNfrwIEgMfeai+T2pQkuWkjCsYm3z+OybLOYMCN66gueqOOqGTq60qhM4dFu5s5w== -"@newrelic/koa@^8.0.1": +"@newrelic/koa@^8.0.0", "@newrelic/koa@^8.0.1": version "8.0.1" resolved "https://registry.yarnpkg.com/@newrelic/koa/-/koa-8.0.1.tgz#26c1c6a69b15ad4b64a148b6be537ec2ca734206" integrity sha512-GyeZGKPllpUu6gWXRwVP/FlvE9+tU2lOprRiTdoXNM8jdVGL02IfHnvAzrIANoZoUdf3+Vev8NNeCup2Eojcvg== @@ -2307,12 +2307,17 @@ uuid "^9.0.0" ws "^7.5.9" +"@newrelic/superagent@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@newrelic/superagent/-/superagent-7.0.0.tgz" + integrity sha512-fNB4NC+pJYYrFZRLcXaTb4Z7XFEfHi7fVQ3O9Qh10m/9CBM2W+Qc/6yyK9M1liRfgUGo5NOILRdjA23SS7720A== + "@newrelic/superagent@^7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@newrelic/superagent/-/superagent-7.0.1.tgz#8d5bb92579cf0b291e1298f480c4939a3d70ec09" integrity sha512-QZlW0VxHSVOXcMAtlkg+Mth0Nz3vFku8rfzTEmoI/pXcckHXGEYuiVUhhboCTD3xTKVgnZRUp9BWF6SOggGUSw== -"@next/bundle-analyzer@~13.5.4": +"@next/bundle-analyzer@~13.5.5": version "13.5.5" resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-13.5.5.tgz#301edbfe05ff910ce3c9ba691ea2a6257e0032cb" integrity sha512-v69BJm8ONM/e6l39Ao0ar8TwZyFnhI5s6id8LGayNq/3JaqkbzW97bIcBkTI0H9RiX3zZNIiaIyMgdKcbJqvsw== @@ -9254,7 +9259,7 @@ execa@^5.0.0, execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -express@^4.17.1, express@^4.17.3: +express@^4.17.1, express@^4.17.3, express@~4.18.2: version "4.18.2" resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -12972,6 +12977,33 @@ newrelic@^11.0.0, newrelic@~11.4.0: "@newrelic/native-metrics" "^10.0.0" "@prisma/prisma-fmt-wasm" "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" +newrelic@~11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/newrelic/-/newrelic-11.2.0.tgz#eded32c7b7d97cae36e45396e8926a201e441793" + integrity sha512-gkt6c5nphsKTRBmKd0H12xELwnhdV9Xph5CL8IXT7nj0C1gL/xxfuTrwj6g+JqDvVz983iNNfdfXBEhIUJC4nQ== + dependencies: + "@grpc/grpc-js" "^1.8.10" + "@grpc/proto-loader" "^0.7.5" + "@newrelic/aws-sdk" "^7.0.0" + "@newrelic/koa" "^8.0.0" + "@newrelic/security-agent" "0.3.0" + "@newrelic/superagent" "^7.0.0" + "@tyriar/fibonacci-heap" "^2.0.7" + concat-stream "^2.0.0" + https-proxy-agent "^7.0.1" + import-in-the-middle "^1.4.2" + json-bigint "^1.0.0" + json-stringify-safe "^5.0.0" + module-details-from-path "^1.0.3" + readable-stream "^3.6.1" + require-in-the-middle "^7.2.0" + semver "^7.5.2" + winston-transport "^4.5.0" + optionalDependencies: + "@contrast/fn-inspect" "^3.3.0" + "@newrelic/native-metrics" "^10.0.0" + "@prisma/prisma-fmt-wasm" "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085" + next-absolute-url@~1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/next-absolute-url/-/next-absolute-url-1.2.2.tgz" @@ -13814,7 +13846,7 @@ pako@~1.0.5: resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== -panoptes-client@~5.5.1: +panoptes-client@~5.5.1, panoptes-client@~5.5.6: version "5.5.6" resolved "https://registry.npmjs.org/panoptes-client/-/panoptes-client-5.5.6.tgz" integrity sha512-TvcKIS7ggrfuh8dA+9ORgHw53lWCoRjyIZWtSjOGOlIIBB2QF+3dPEgyDUltQ6Kpo49TV7PRAYNczJI3GGn07w== @@ -15590,6 +15622,13 @@ selfsigned@^2.1.1, selfsigned@~2.4.1: "@types/node-forge" "^1.3.0" node-forge "^1" +selfsigned@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.2.tgz#9f9a4b0d472a5f29f892eb52358056c61a7387e3" + integrity sha512-xc6ZKMc9owNuU3uEPuW45RnSPylOlRK5Brj8oWf/2+BQV2gD1c+/eJaHFCcTG8w8kRkEfb5mzn/yIpie6gJ1tA== + dependencies: + node-forge "^1" + "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" @@ -16401,7 +16440,7 @@ swc-loader@^0.2.3: resolved "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.3.tgz" integrity sha512-D1p6XXURfSPleZZA/Lipb3A8pZ17fP4NObZvFCDjK/OKljroqDpPmsBdTraWhVBqUNpcWBQY1imWdoPScRlQ7A== -swr@~2.2.0: +swr@~2.2.0, swr@~2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07" integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==