diff --git a/designer/client/index.tsx b/designer/client/index.tsx index d0d2d1c755..01ad2d56f0 100644 --- a/designer/client/index.tsx +++ b/designer/client/index.tsx @@ -3,9 +3,17 @@ import ReactDOM from "react-dom"; import { LandingChoice, NewConfig, ChooseExisting } from "./pages/LandingPage"; import "./styles/index.scss"; import { initI18n } from "./i18n"; -import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; +import { + BrowserRouter as Router, + Switch, + Route, + Redirect, + useLocation, +} from "react-router-dom"; import Designer from "./designer"; import { SaveError } from "./pages/ErrorPages"; +import { CookiesProvider, useCookies } from "react-cookie"; +import { ProgressPlugin } from "webpack"; initI18n(); @@ -13,30 +21,117 @@ function NoMatch() { return
404 Not found
; } +function UserChoice() { + let [cookies, setcookie] = useCookies(["user"]); + let [userState, updateUserState] = React.useState(cookies.user); + + let updateUser = (e: React.FormEvent) => { + setcookie("user", userState, { + path: "/", + sameSite: "strict", + }); + + return true; + }; + + // return ( + //
updateUser(e)}> + // + // updateUserState(e.target.value)} + // value={userState} + // /> + // + //
+ // ); + return
Logged in as: {cookies.user}
; +} + +function useQuery() { + const { search } = useLocation(); + + return React.useMemo(() => new URLSearchParams(search), [search]); +} + +function AuthProvider({ children }) { + let [cookies, setCookie] = useCookies(["user"]); + let query = useQuery(); + + if (query.get("token")) { + setCookie("user", query.get("token"), { + path: "/", + sameSite: "strict", + }); + } + + if (cookies.user || query.get("token")) return children; + + window.location.href = "/api/login"; +} + +function Auth() { + let [_, setcookie] = useCookies(["user"]); + let query = useQuery(); + + setcookie("user", query.get("token"), { + path: "/", + sameSite: "strict", + }); + return ; +} + +function Logout() { + let [_, __, removeCookie] = useCookies(["user"]); + + let doLogout = (e) => { + e.preventDefault(); + removeCookie("user"); + window.location.href = "/api/login"; + }; + + return ( + doLogout(e)} href="#"> + Logout + + ); +} + export class App extends React.Component { render() { return ( -
- - - - - - - - - - - - - - - - - - -
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
); } diff --git a/designer/package.json b/designer/package.json index fd24178738..fb934f22e1 100644 --- a/designer/package.json +++ b/designer/package.json @@ -60,6 +60,7 @@ "lodash": "^4.17.21", "moment-timezone": "^0.5.31", "nanoid": "^3.1.12", + "react-cookie": "^4.1.1", "react-helmet": "^6.1.0", "react-router-dom": "^5.2.0", "resolve": "^1.19.0", diff --git a/designer/server/config.ts b/designer/server/config.ts index 01984ee2d2..344aef6dff 100644 --- a/designer/server/config.ts +++ b/designer/server/config.ts @@ -11,6 +11,7 @@ export interface Config { previewUrl: string; publishUrl: string; formsApiUrl: string; + managementUrl: string; persistentBackend: "s3" | "blob" | "preview" | "api"; s3Bucket?: string; logLevel: "trace" | "info" | "debug" | "error"; @@ -39,6 +40,7 @@ const schema = joi.object({ previewUrl: joi.string(), publishUrl: joi.string(), formsApiUrl: joi.string(), + managementUrl: joi.string(), persistentBackend: joi .string() .valid("s3", "blob", "preview", "api") @@ -63,6 +65,7 @@ const config = { previewUrl: process.env.PREVIEW_URL || "http://localhost:3009", publishUrl: process.env.PUBLISH_URL || "http://localhost:3009", formsApiUrl: process.env.FORMS_API_URL || "http://localhost:4567", + managementUrl: process.env.MANAGEMENT_URL || "http://localhost:3030", persistentBackend: process.env.PERSISTENT_BACKEND || "preview", s3Bucket: process.env.S3_BUCKET, logLevel: process.env.LOG_LEVEL || "error", diff --git a/designer/server/lib/persistence/apiPersistenceService.ts b/designer/server/lib/persistence/apiPersistenceService.ts index 44bc1d051c..336b481f75 100644 --- a/designer/server/lib/persistence/apiPersistenceService.ts +++ b/designer/server/lib/persistence/apiPersistenceService.ts @@ -1,6 +1,7 @@ import type { PersistenceService } from "./persistenceService"; import Wreck from "@hapi/wreck"; import config from "../../config"; +import { FormConfiguration } from "@xgovformbuilder/model"; export class ApiPersistenceService implements PersistenceService { logger: any; @@ -12,22 +13,73 @@ export class ApiPersistenceService implements PersistenceService { }); } + async uploadConfigurationForUser( + id: string, + configuration: string, + user: string + ): Promise { + return Wreck.post(`${config.formsApiUrl}/publish`, { + payload: JSON.stringify({ id, configuration: JSON.parse(configuration) }), + headers: { + "x-api-key": user, + }, + }); + } + async copyConfiguration(configurationId: string, newName: string) { const configuration = await this.getConfiguration(configurationId); return this.uploadConfiguration(newName, configuration); } - async listAllConfigurations() { - const { payload } = await Wreck.get(`${config.formsApiUrl}/published`); - return JSON.parse(payload.toString()); + async copyConfigurationForUser( + configurationId: string, + newName: string, + user: string + ): Promise { + const configuration = await this.getConfigurationForUser( + configurationId, + user + ); + return this.uploadConfigurationForUser(newName, configuration, user); } async getConfiguration(id: string) { - console.log("Getting: ", id); const { payload } = await Wreck.get( `${config.formsApiUrl}/published/${id}` ); var configuration = JSON.parse(payload.toString()).values; return JSON.stringify(configuration); } + + async getConfigurationForUser(id: string, user: string): Promise { + const { payload } = await Wreck.get( + `${config.formsApiUrl}/published/${id}`, + { + headers: { + "x-api-key": user, + }, + } + ); + + var configuration = JSON.parse(payload.toString()).values; + return JSON.stringify(configuration); + } + + async listAllConfigurations() { + const { payload } = await Wreck.get(`${config.formsApiUrl}/published`); + return JSON.parse(payload.toString()); + } + + async listAllConfigurationsForUser( + user: string + ): Promise { + console.log("Getting forms for: ", user); + + const { payload } = await Wreck.get(`${config.formsApiUrl}/published`, { + headers: { + "x-api-key": user, + }, + }); + return JSON.parse(payload.toString()); + } } diff --git a/designer/server/lib/persistence/persistenceService.ts b/designer/server/lib/persistence/persistenceService.ts index 9e02366ffc..5690fd0d10 100644 --- a/designer/server/lib/persistence/persistenceService.ts +++ b/designer/server/lib/persistence/persistenceService.ts @@ -6,6 +6,18 @@ export interface PersistenceService { getConfiguration(id: string): Promise; uploadConfiguration(id: string, configuration: string): Promise; copyConfiguration(configurationId: string, newName: string): Promise; + listAllConfigurationsForUser(user: string): Promise; + getConfigurationForUser(id: string, user: string): Promise; + uploadConfigurationForUser( + id: string, + configuration: string, + user: string + ): Promise; + copyConfigurationForUser( + configurationId: string, + newName: string, + user: string + ): Promise; } export class StubPersistenceService implements PersistenceService { @@ -21,6 +33,7 @@ export class StubPersistenceService implements PersistenceService { getConfiguration(_id: string) { return Promise.resolve(""); } + copyConfiguration(_configurationId: string, _newName: string) { return Promise.resolve(""); } diff --git a/designer/server/plugins/designer.ts b/designer/server/plugins/designer.ts index ae9d4b87a9..e9a6eaffb1 100644 --- a/designer/server/plugins/designer.ts +++ b/designer/server/plugins/designer.ts @@ -52,6 +52,7 @@ export const designerPlugin = { }); server.route(newConfig.registerNewFormWithRunner); + server.route(api.loginRedirect); server.route(api.getFormWithId); server.route(api.putFormWithId); server.route(api.getAllPersistedConfigurations); diff --git a/designer/server/plugins/routes/api.ts b/designer/server/plugins/routes/api.ts index 01d128e662..6772d902cc 100644 --- a/designer/server/plugins/routes/api.ts +++ b/designer/server/plugins/routes/api.ts @@ -12,6 +12,16 @@ const getPublished = async function (id) { return payload.toString(); }; +export const loginRedirect: ServerRoute = { + method: "GET", + path: "/api/login", + options: { + handler: (_, h) => { + return h.redirect(config.managementUrl); + }, + }, +}; + export const getFormWithId: ServerRoute = { // GET DATA method: "GET", @@ -19,14 +29,14 @@ export const getFormWithId: ServerRoute = { options: { handler: async (request, h) => { const { id } = request.params; + const { persistenceService } = request.services([]); let formJson = newFormJson; try { - const response = await getPublished(id); - const { values } = JSON.parse(response); - - if (values) { - formJson = values; - } + const response = await persistenceService.getConfigurationForUser( + `${id}`, + request.state["user"] + ); + formJson = JSON.parse(response); } catch (error) { request.logger.error(error); } @@ -61,11 +71,22 @@ export const putFormWithId: ServerRoute = { throw new Error("Schema validation failed, reason: " + error.message); } - await persistenceService.uploadConfiguration( - `${id}`, - JSON.stringify(value) - ); - await publish(id, value); + + if (request.state["user"]) { + await persistenceService.uploadConfigurationForUser( + `${id}`, + JSON.stringify(value), + request.state["user"] + ); + } else { + await persistenceService.uploadConfiguration( + `${id}`, + JSON.stringify(value) + ); + } + + // Remove publishing for now + // await publish(id, value); return h.response({ ok: true }).code(204); } catch (err) { request.logger.error("Designer Server PUT /api/{id}/data error:", err); @@ -89,7 +110,16 @@ export const getAllPersistedConfigurations: ServerRoute = { handler: async (request, h): Promise => { const { persistenceService } = request.services([]); try { - const response = await persistenceService.listAllConfigurations(); + let response; + + if (request.state["user"]) { + response = await persistenceService.listAllConfigurationsForUser( + request.state["user"] + ); + } else { + response = await persistenceService.listAllConfigurations(); + } + return h.response(response).type("application/json"); } catch (error) { request.server.log(["error", "/configurations"], error); diff --git a/designer/server/plugins/routes/newConfig.ts b/designer/server/plugins/routes/newConfig.ts index 424aa11fb7..79ce352413 100644 --- a/designer/server/plugins/routes/newConfig.ts +++ b/designer/server/plugins/routes/newConfig.ts @@ -24,20 +24,47 @@ export const registerNewFormWithRunner: ServerRoute = { try { if (selected.Key === "New") { - if (config.persistentBackend !== "preview") { - await persistenceService.uploadConfiguration( - `${newName}.json`, - JSON.stringify(newFormJson) - ); + if ( + config.persistentBackend !== "preview" && + config.persistentBackend !== "api" + ) { + if (request.state["user"]) { + await persistenceService.uploadConfigurationForUser( + `${newName}`, + JSON.stringify(newFormJson), + request.state["user"] + ); + } else { + await persistenceService.uploadConfiguration( + `${newName}`, + JSON.stringify(newFormJson) + ); + } } - await publish(newName, newFormJson); + + // Remove publishing for now + // await publish(newName, newFormJson); } else { - await persistenceService.copyConfiguration( - `${selected.Key}`, - newName - ); - const copied = await persistenceService.getConfiguration(newName); - await publish(newName, copied); + let copied; + if (request.state["user"]) { + await persistenceService.copyConfigurationForUser( + `${selected.Key}`, + newName, + request.state["user"] + ); + copied = await persistenceService.getConfigurationForUser( + newName, + request.state["user"] + ); + } else { + await persistenceService.copyConfiguration( + `${selected.Key}`, + newName + ); + copied = await persistenceService.getConfiguration(newName); + } + // Remove publishing for now + // await publish(newName, copied); } } catch (e) { request.logger.error(e); diff --git a/runner/src/server/config.ts b/runner/src/server/config.ts index c9ef7e916f..1771163102 100644 --- a/runner/src/server/config.ts +++ b/runner/src/server/config.ts @@ -54,6 +54,7 @@ const schema = Joi.object({ serviceName: Joi.string().optional(), documentUploadApiUrl: Joi.string().default(DEFAULT_DOCUMENT_UPLOAD_API_URL), previewMode: Joi.boolean().optional(), + formsApiUrl: Joi.string().optional(), sslKey: Joi.string().optional(), sslCert: Joi.string().optional(), sessionTimeout: Joi.number().default(DEFAULT_SESSION_TTL), @@ -126,6 +127,7 @@ export function buildConfig() { serviceName: process.env.SERVICE_NAME, documentUploadApiUrl: process.env.DOCUMENT_UPLOAD_API_URL, previewMode: process.env.PREVIEW_MODE === "true", + formsApiUrl: process.env.FORM_API_URL, sslKey: process.env.SSL_KEY, sslCert: process.env.SSL_CERT, sessionTimeout: process.env.SESSION_TIMEOUT, diff --git a/runner/src/server/plugins/engine/configureEnginePlugin.ts b/runner/src/server/plugins/engine/configureEnginePlugin.ts index c9e1f1566b..cab31e0181 100644 --- a/runner/src/server/plugins/engine/configureEnginePlugin.ts +++ b/runner/src/server/plugins/engine/configureEnginePlugin.ts @@ -23,6 +23,7 @@ type ConfigureEnginePlugin = ( id: string; }[]; previewMode: boolean; + formsApiUrl: string; }; }; @@ -56,6 +57,11 @@ export const configureEnginePlugin: ConfigureEnginePlugin = ( return { plugin, - options: { modelOptions, configs, previewMode: config.previewMode }, + options: { + modelOptions, + configs, + previewMode: config.previewMode, + formsApiUrl: config.formsApiUrl, + }, }; }; diff --git a/runner/src/server/plugins/engine/plugin.ts b/runner/src/server/plugins/engine/plugin.ts index aa674f3002..8bcf401eca 100644 --- a/runner/src/server/plugins/engine/plugin.ts +++ b/runner/src/server/plugins/engine/plugin.ts @@ -7,8 +7,10 @@ import { HapiRequest, HapiResponseToolkit, HapiServer } from "server/types"; import { FormModel } from "./models"; import Boom from "boom"; import { PluginSpecificConfiguration } from "@hapi/hapi"; +import Wreck from "@hapi/wreck"; import { FormPayload } from "./types"; import { shouldLogin } from "server/plugins/auth"; +import config from "src/server/config"; configure([ // Configure Nunjucks to allow rendering of content that is revealed conditionally. @@ -24,6 +26,17 @@ function normalisePath(path: string) { return path.replace(/^\//, "").replace(/\/$/, ""); } +async function getForm( + id: string, + formsApiUrl: string, + modelOptions, + basePath: string +): Promise { + const { payload } = await Wreck.get(`${formsApiUrl}/published/${id}`); + var configuration = JSON.parse(payload.toString()).values; + return new FormModel(configuration, { ...modelOptions, basePath }); +} + function getStartPageRedirect( request: HapiRequest, h: HapiResponseToolkit, @@ -47,6 +60,7 @@ type PluginOptions = { modelOptions: any; configs: any[]; previewMode: boolean; + formsApiUrl: string; }; export const plugin = { @@ -64,93 +78,93 @@ export const plugin = { }); }); - if (previewMode) { - /** - * The following endpoints are used from the designer for operating in 'preview' mode. - * I.E. Designs saved in the designer can be accessed in the runner for viewing. - * The designer also uses these endpoints as a persistence mechanism for storing and retrieving data - * for it's own purposes so if you're changing these endpoints you likely need to go and amend - * the designer too! - */ - server.route({ - method: "post", - path: "/publish", - handler: (request: HapiRequest, h: HapiResponseToolkit) => { - const payload = request.payload as FormPayload; - const { id, configuration } = payload; - - const parsedConfiguration = - typeof configuration === "string" - ? JSON.parse(configuration) - : configuration; - forms[id] = new FormModel(parsedConfiguration, { - ...modelOptions, - basePath: id, - }); - return h.response({}).code(204); - }, - }); + // if (previewMode) { + // /** + // * The following endpoints are used from the designer for operating in 'preview' mode. + // * I.E. Designs saved in the designer can be accessed in the runner for viewing. + // * The designer also uses these endpoints as a persistence mechanism for storing and retrieving data + // * for it's own purposes so if you're changing these endpoints you likely need to go and amend + // * the designer too! + // */ + // server.route({ + // method: "post", + // path: "/publish", + // handler: (request: HapiRequest, h: HapiResponseToolkit) => { + // const payload = request.payload as FormPayload; + // const { id, configuration } = payload; - server.route({ - method: "get", - path: "/published/{id}", - handler: (request: HapiRequest, h: HapiResponseToolkit) => { - const { id } = request.params; - if (forms[id]) { - const { values } = forms[id]; - return h.response(JSON.stringify({ id, values })).code(200); - } else { - return h.response({}).code(204); - } - }, - }); + // const parsedConfiguration = + // typeof configuration === "string" + // ? JSON.parse(configuration) + // : configuration; + // forms[id] = new FormModel(parsedConfiguration, { + // ...modelOptions, + // basePath: id, + // }); + // return h.response({}).code(204); + // }, + // }); - server.route({ - method: "get", - path: "/published", - handler: (_request: HapiRequest, h: HapiResponseToolkit) => { - return h - .response( - JSON.stringify( - Object.keys(forms).map( - (key) => - new FormConfiguration( - key, - forms[key].name, - undefined, - forms[key].def.feedback?.feedbackForm - ) - ) - ) - ) - .code(200); - }, - }); - } + // server.route({ + // method: "get", + // path: "/published/{id}", + // handler: (request: HapiRequest, h: HapiResponseToolkit) => { + // const { id } = request.params; + // if (forms[id]) { + // const { values } = forms[id]; + // return h.response(JSON.stringify({ id, values })).code(200); + // } else { + // return h.response({}).code(204); + // } + // }, + // }); - server.route({ - method: "get", - path: "/", - handler: (request: HapiRequest, h: HapiResponseToolkit) => { - const keys = Object.keys(forms); - let id = ""; - if (keys.length === 1) { - id = keys[0]; - } - const model = forms[id]; - if (model) { - return getStartPageRedirect(request, h, id, model); - } - throw Boom.notFound("No default form found"); - }, - }); + // server.route({ + // method: "get", + // path: "/published", + // handler: (_request: HapiRequest, h: HapiResponseToolkit) => { + // return h + // .response( + // JSON.stringify( + // Object.keys(forms).map( + // (key) => + // new FormConfiguration( + // key, + // forms[key].name, + // undefined, + // forms[key].def.feedback?.feedbackForm + // ) + // ) + // ) + // ) + // .code(200); + // }, + // }); + // } + + // server.route({ + // method: "get", + // path: "/", + // handler: (request: HapiRequest, h: HapiResponseToolkit) => { + // const keys = Object.keys(forms); + // let id = ""; + // if (keys.length === 1) { + // id = keys[0]; + // } + // const model = forms[id]; + // if (model) { + // return getStartPageRedirect(request, h, id, model); + // } + // throw Boom.notFound("No default form found"); + // }, + // }); server.route({ method: "get", path: "/{id}", - handler: (request: HapiRequest, h: HapiResponseToolkit) => { + handler: async (request: HapiRequest, h: HapiResponseToolkit) => { const { id } = request.params; - const model = forms[id]; + const model = await getForm(id, options.formsApiUrl, modelOptions, id); if (model) { return getStartPageRedirect(request, h, id, model); } @@ -161,9 +175,9 @@ export const plugin = { server.route({ method: "get", path: "/{id}/{path*}", - handler: (request: HapiRequest, h: HapiResponseToolkit) => { + handler: async (request: HapiRequest, h: HapiResponseToolkit) => { const { path, id } = request.params; - const model = forms[id]; + const model = await getForm(id, options.formsApiUrl, modelOptions, id); const page = model?.pages.find( (page) => normalisePath(page.path) === normalisePath(path) ); @@ -196,7 +210,7 @@ export const plugin = { h: HapiResponseToolkit ) => { const { path, id } = request.params; - const model = forms[id]; + const model = await getForm(id, options.formsApiUrl, modelOptions, id); if (model) { const page = model.pages.find( diff --git a/yarn.lock b/yarn.lock index 85eb50daca..6579898965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3541,6 +3541,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.3.3": + version: 0.3.3 + resolution: "@types/cookie@npm:0.3.3" + checksum: 35d603d5e620ed8a0c4dedcf6b6e19e4981a4c44d40402de39e53f041ce74bf66b40272a355db37271b9557709fd3e6dfc8fe6ea4031f59b8b16cf0606ea3797 + languageName: node + linkType: hard + "@types/cookie@npm:^0.4.0": version: 0.4.0 resolution: "@types/cookie@npm:0.4.0" @@ -3715,6 +3722,16 @@ __metadata: languageName: node linkType: hard +"@types/hoist-non-react-statics@npm:^3.0.1": + version: 3.3.1 + resolution: "@types/hoist-non-react-statics@npm:3.3.1" + dependencies: + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + checksum: 16ab4c45d4920fa378c8be76554b10061247fc04d2c8af11bdb7d520b3967e9c06d7ad5efd9b0f1657fbc4d095f62c6e1325f03b9141eb1ef2c8095b96fd42f8 + languageName: node + linkType: hard + "@types/html-minifier-terser@npm:^5.0.0": version: 5.1.1 resolution: "@types/html-minifier-terser@npm:5.1.1" @@ -4024,6 +4041,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:*": + version: 17.0.38 + resolution: "@types/react@npm:17.0.38" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: b04dcee8a5e530ec22d9d624c76ad198130b29751b28d66678def14d2def8be9c6fc1e3515d4700f23feb765a8eae7e278f27eaf841b006670a41b421c0a6866 + languageName: node + linkType: hard + "@types/react@npm:^16, @types/react@npm:^16.9.52": version: 16.14.2 resolution: "@types/react@npm:16.14.2" @@ -4052,6 +4080,13 @@ __metadata: languageName: node linkType: hard +"@types/scheduler@npm:*": + version: 0.16.2 + resolution: "@types/scheduler@npm:0.16.2" + checksum: e78d1bb50ca9321a76826c0f5a5ed7f1b6e995597127993cad658bd18d7f7de4324cd7efdfd0140079ee212a05774bbbd95eecfdf72370d14023acb99b841a30 + languageName: node + linkType: hard + "@types/selenium-standalone@npm:^7.0.0": version: 7.0.0 resolution: "@types/selenium-standalone@npm:7.0.0" @@ -4960,6 +4995,7 @@ __metadata: postcss-loader: ^4.1.0 prismjs: 1.23.0 react: 16.13.1 + react-cookie: ^4.1.1 react-dom: 16.13.1 react-helmet: ^6.1.0 react-router-dom: ^5.2.0 @@ -8158,7 +8194,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.1": +"cookie@npm:^0.4.0, cookie@npm:^0.4.1": version: 0.4.1 resolution: "cookie@npm:0.4.1" checksum: b8e0928e3e7aba013087974b33a6eec730b0a68b7ec00fc3c089a56ba2883bcf671252fc2ed64775aa1ca64796b6e1f6fdddba25a66808aef77614d235fd3e06 @@ -12821,7 +12857,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.1.0": +"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -19318,6 +19354,19 @@ fsevents@^1.2.7: languageName: node linkType: hard +"react-cookie@npm:^4.1.1": + version: 4.1.1 + resolution: "react-cookie@npm:4.1.1" + dependencies: + "@types/hoist-non-react-statics": ^3.0.1 + hoist-non-react-statics: ^3.0.0 + universal-cookie: ^4.0.0 + peerDependencies: + react: ">= 16.3.0" + checksum: 0a011b7cbe8d72e5d3482708abe9a00616c18b2c6c6f4f911373d76f1307d6c3b2a5a096e855181009b3d6461ae587f27483820d511fdaaf2821d9daf54b1103 + languageName: node + linkType: hard + "react-dom@npm:16.13.1": version: 16.13.1 resolution: "react-dom@npm:16.13.1" @@ -23147,6 +23196,16 @@ resolve@^1.1.6: languageName: node linkType: hard +"universal-cookie@npm:^4.0.0": + version: 4.0.4 + resolution: "universal-cookie@npm:4.0.4" + dependencies: + "@types/cookie": ^0.3.3 + cookie: ^0.4.0 + checksum: 182b97a2dd6a368f1940f8dd2d0ec3908f1add6f651a6bd30dac1a8083dd7f64c35e0a7fd4b3c923004f4fe85e838d2996bcd0057061a2f494782f65c2a105fc + languageName: node + linkType: hard + "universalify@npm:^0.1.0, universalify@npm:^0.1.2": version: 0.1.2 resolution: "universalify@npm:0.1.2"