From 29889f80e58de98d1d09fadf999e573ac40caa2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 29 Nov 2023 08:55:34 +0000 Subject: [PATCH] fix: Restrict write access to `flows.data` column via GraphQL API (#2488) --- api.planx.uk/helpers.ts | 93 ++- .../modules/flows/copyFlow/controller.ts | 4 +- .../modules/flows/copyFlow/copyFlow.test.ts | 57 ++ .../modules/flows/findReplace/controller.ts | 11 +- .../flows/findReplace/findReplace.test.ts | 600 ++++++++++++------ .../modules/flows/findReplace/service.ts | 8 +- api.planx.uk/package.json | 4 + api.planx.uk/pnpm-lock.yaml | 254 +++++++- .../src/pages/FlowEditor/lib/store/editor.ts | 6 +- hasura.planx.uk/metadata/tables.yaml | 20 +- .../down.sql | 1 + .../up.sql | 1 + 12 files changed, 795 insertions(+), 264 deletions(-) create mode 100644 hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/down.sql create mode 100644 hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/up.sql diff --git a/api.planx.uk/helpers.ts b/api.planx.uk/helpers.ts index 585c922c63..83b82b6ab0 100644 --- a/api.planx.uk/helpers.ts +++ b/api.planx.uk/helpers.ts @@ -2,7 +2,7 @@ import { gql } from "graphql-request"; import { capitalize } from "lodash"; import { Flow, Node } from "./types"; import { ComponentType, FlowGraph } from "@opensystemslab/planx-core/types"; -import { $public, getClient } from "./client"; +import { $api, $public, getClient } from "./client"; // Get a flow's data (unflattened, without external portal nodes) const getFlowData = async (id: string): Promise => { @@ -23,6 +23,11 @@ const getFlowData = async (id: string): Promise => { return flow; }; +interface InsertFlow { + flow: { + id: string; + }; +} // Insert a new flow into the `flows` table const insertFlow = async ( teamId: number, @@ -32,40 +37,64 @@ const insertFlow = async ( copiedFrom?: Flow["id"], ) => { const { client: $client } = getClient(); - const data = await $client.request<{ flow: { id: string } }>( - gql` - mutation InsertFlow( - $team_id: Int! - $slug: String! - $data: jsonb = {} - $creator_id: Int - $copied_from: uuid - ) { - flow: insert_flows_one( - object: { - team_id: $team_id - slug: $slug - data: $data - version: 1 - creator_id: $creator_id - copied_from: $copied_from - } + try { + const { + flow: { id }, + } = await $client.request( + gql` + mutation InsertFlow( + $team_id: Int! + $slug: String! + $creator_id: Int + $copied_from: uuid ) { - id + flow: insert_flows_one( + object: { + team_id: $team_id + slug: $slug + version: 1 + creator_id: $creator_id + copied_from: $copied_from + } + ) { + id + } } - } - `, - { - team_id: teamId, - slug: slug, - data: flowData, - creator_id: creatorId, - copied_from: copiedFrom, - }, - ); + `, + { + team_id: teamId, + slug: slug, + creator_id: creatorId, + copied_from: copiedFrom, + }, + ); + + // Populate flow data using API role now that we know user has permission to insert flow + // Access to flow.data column is restricted to limit unsafe content that could be inserted + await $api.client.request( + gql` + mutation UpdateFlowData($data: jsonb = {}, $id: uuid!) { + flow: update_flows_by_pk( + pk_columns: { id: $id } + _set: { data: $data } + ) { + id + } + } + `, + { + id: id, + data: flowData, + }, + ); - if (data) await createAssociatedOperation(data?.flow?.id); - return data?.flow; + await createAssociatedOperation(id); + return { id }; + } catch (error) { + throw Error( + `User ${creatorId} failed to insert flow to teamId ${teamId}. Please check permissions.`, + ); + } }; // Add a row to `operations` for an inserted flow, otherwise ShareDB throws a silent error when opening the flow in the UI diff --git a/api.planx.uk/modules/flows/copyFlow/controller.ts b/api.planx.uk/modules/flows/copyFlow/controller.ts index c3bfcfc82a..0f8918995c 100644 --- a/api.planx.uk/modules/flows/copyFlow/controller.ts +++ b/api.planx.uk/modules/flows/copyFlow/controller.ts @@ -27,7 +27,7 @@ export type CopyFlowController = ValidatedRequestHandler< >; export const copyFlowController: CopyFlowController = async ( - req, + _req, res, next, ) => { @@ -48,7 +48,7 @@ export const copyFlowController: CopyFlowController = async ( }); } catch (error) { return next( - new ServerError({ message: "Failed to copy flow", cause: error }), + new ServerError({ message: `Failed to copy flow. Error: ${error}` }), ); } }; diff --git a/api.planx.uk/modules/flows/copyFlow/copyFlow.test.ts b/api.planx.uk/modules/flows/copyFlow/copyFlow.test.ts index 30bef2e79e..8e376038c2 100644 --- a/api.planx.uk/modules/flows/copyFlow/copyFlow.test.ts +++ b/api.planx.uk/modules/flows/copyFlow/copyFlow.test.ts @@ -26,6 +26,16 @@ beforeEach(() => { }, }); + queryMock.mockQuery({ + name: "UpdateFlowData", + matchOnVariables: false, + data: { + flow: { + id: 2, + }, + }, + }); + queryMock.mockQuery({ name: "InsertOperation", matchOnVariables: false, @@ -85,6 +95,53 @@ it("returns an error if required replacement characters are not provided in the }); }); +it("returns an error if the operation to insert a new flow fails", async () => { + queryMock.reset(); + + const body = { + insert: true, + replaceValue: "T3ST1", + }; + + queryMock.mockQuery({ + name: "GetFlowData", + matchOnVariables: false, + data: { + flow: { + data: mockFlowData, + }, + }, + }); + + queryMock.mockQuery({ + name: "InsertFlow", + matchOnVariables: false, + data: { + flow: { + id: 2, + }, + }, + graphqlErrors: [ + { + message: "Something went wrong", + }, + ], + variables: { + id: "3", + }, + }); + + await supertest(app) + .post("/flows/3/copy") + .send(body) + .set(auth) + .expect(500) + .then((res) => { + expect(res.body.error).toMatch(/failed to insert flow/); + expect(res.body.error).toMatch(/Please check permissions/); + }); +}); + it("returns copied unique flow data without inserting a new record", async () => { const body = { insert: false, diff --git a/api.planx.uk/modules/flows/findReplace/controller.ts b/api.planx.uk/modules/flows/findReplace/controller.ts index c64387dc64..ea0e1e3d44 100644 --- a/api.planx.uk/modules/flows/findReplace/controller.ts +++ b/api.planx.uk/modules/flows/findReplace/controller.ts @@ -4,6 +4,12 @@ import { z } from "zod"; import { ServerError } from "../../../errors"; import { findAndReplaceInFlow } from "./service"; import { FlowGraph } from "@opensystemslab/planx-core/types"; +import { JSDOM } from "jsdom"; +import createDOMPurify from "dompurify"; + +// Setup JSDOM and DOMPurify +const window = new JSDOM("").window; +const DOMPurify = createDOMPurify(window); interface FindAndReplaceResponse { message: string; @@ -17,7 +23,10 @@ export const findAndReplaceSchema = z.object({ }), query: z.object({ find: z.string(), - replace: z.string().optional(), + replace: z + .string() + .optional() + .transform((val) => val && DOMPurify.sanitize(val)), }), }); diff --git a/api.planx.uk/modules/flows/findReplace/findReplace.test.ts b/api.planx.uk/modules/flows/findReplace/findReplace.test.ts index 85202976d6..918aa1db1a 100644 --- a/api.planx.uk/modules/flows/findReplace/findReplace.test.ts +++ b/api.planx.uk/modules/flows/findReplace/findReplace.test.ts @@ -5,234 +5,420 @@ import { authHeader } from "../../../tests/mockJWT"; import app from "../../../server"; import { Flow } from "../../../types"; -beforeEach(() => { - queryMock.mockQuery({ - name: "GetFlowData", - matchOnVariables: false, - data: { - flow: { - data: mockFlowData, - slug: "test", - }, - }, +const auth = authHeader({ role: "platformAdmin" }); + +describe("authentication", () => { + it("requires a user to be logged in", async () => { + await supertest(app).post("/flows/1/search").expect(401); }); - queryMock.mockQuery({ - name: "UpdateFlow", - matchOnVariables: false, - data: { - flow: { - data: replacedFlowData, - slug: "test", - }, - }, + it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .post("/flows/1/search") + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + + it("throws an error if missing query parameter `find`", async () => { + await supertest(app) + .post("/flows/1/search") + .set(auth) + .expect(400) + .then((res) => { + expect(res.body).toHaveProperty("issues"); + expect(res.body).toHaveProperty("name", "ZodError"); + }); }); }); -const auth = authHeader({ role: "platformAdmin" }); +describe("string replacement", () => { + const mockFlowData: Flow["data"] = { + _root: { + edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], + }, + "6dwuQp5xjA": { + data: { + text: "No", + }, + type: 200, + }, + "8AWcYxZgBw": { + data: { + fn: "property.constraints.planning", + text: "Is it monument inside a portal?", + }, + type: 100, + edges: ["AJnWX6O1xt", "6dwuQp5xjA"], + }, + AJnWX6O1xt: { + data: { + val: "designated.monument", + text: "Yes", + }, + type: 200, + }, + Hfh8KuSzUq: { + data: { + val: "designated.monument", + text: "Yes", + }, + type: 200, + }, + RRQwM2zAgy: { + data: { + fn: "property.constraints.planning", + text: "Is it a monument", + }, + type: 100, + edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], + }, + ft26KlH7Oy: { + data: { + text: "No", + }, + type: 200, + }, + vcTgmVQAre: { + data: { + text: "internal-portal-test", + }, + type: 300, + edges: ["8AWcYxZgBw"], + }, + QsEdip17H5: { + type: 310, + data: { + flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", + }, + }, + }; -it("requires a user to be logged in", async () => { - await supertest(app).post("/flows/1/search").expect(401); -}); + const replacedFlowData: Flow["data"] = { + _root: { + edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], + }, + "6dwuQp5xjA": { + data: { + text: "No", + }, + type: 200, + }, + "8AWcYxZgBw": { + data: { + fn: "property.constraints.planning", + text: "Is it monument inside a portal?", + }, + type: 100, + edges: ["AJnWX6O1xt", "6dwuQp5xjA"], + }, + AJnWX6O1xt: { + data: { + val: "monument", + text: "Yes", + }, + type: 200, + }, + Hfh8KuSzUq: { + data: { + val: "monument", + text: "Yes", + }, + type: 200, + }, + RRQwM2zAgy: { + data: { + fn: "property.constraints.planning", + text: "Is it a monument", + }, + type: 100, + edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], + }, + ft26KlH7Oy: { + data: { + text: "No", + }, + type: 200, + }, + vcTgmVQAre: { + data: { + text: "internal-portal-test", + }, + type: 300, + edges: ["8AWcYxZgBw"], + }, + QsEdip17H5: { + type: 310, + data: { + flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", + }, + }, + }; -it("requires a user to have the 'platformAdmin' role", async () => { - await supertest(app) - .post("/flows/1/search") - .set(authHeader({ role: "teamEditor" })) - .expect(403); -}); + beforeEach(() => { + queryMock.mockQuery({ + name: "GetFlowData", + matchOnVariables: false, + data: { + flow: { + data: mockFlowData, + slug: "test", + }, + }, + }); -it("throws an error if missing query parameter `find`", async () => { - await supertest(app) - .post("/flows/1/search") - .set(auth) - .expect(400) - .then((res) => { - expect(res.body).toHaveProperty("issues"); - expect(res.body).toHaveProperty("name", "ZodError"); + queryMock.mockQuery({ + name: "UpdateFlow", + matchOnVariables: false, + data: { + flow: { + data: replacedFlowData, + slug: "test", + }, + }, }); -}); + }); -it("finds matches", async () => { - await supertest(app) - .post("/flows/1/search?find=designated.monument") - .set(auth) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ - message: `Found 2 matches of "designated.monument" in this flow`, - matches: { - AJnWX6O1xt: { - data: { - val: "designated.monument", + it("finds matches", async () => { + await supertest(app) + .post("/flows/1/search?find=designated.monument") + .set(auth) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + message: `Found 2 matches of "designated.monument" in this flow`, + matches: { + AJnWX6O1xt: { + data: { + val: "designated.monument", + }, + }, + Hfh8KuSzUq: { + data: { + val: "designated.monument", + }, }, }, - Hfh8KuSzUq: { - data: { - val: "designated.monument", + }); + }); + }); + + it("does not replace if no matches are found", async () => { + await supertest(app) + .post("/flows/1/search?find=bananas&replace=monument") + .set(auth) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + message: `Didn't find "bananas" in this flow, nothing to replace`, + matches: null, + }); + }); + }); + + it("updates flow data and returns matches if there are matches", async () => { + await supertest(app) + .post("/flows/1/search?find=designated.monument&replace=monument") + .set(auth) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + message: `Found 2 matches of "designated.monument" and replaced with "monument"`, + matches: { + AJnWX6O1xt: { + data: { + val: "designated.monument", + }, + }, + Hfh8KuSzUq: { + data: { + val: "designated.monument", + }, }, }, - }, + updatedFlow: replacedFlowData, + }); }); - }); + }); }); -it("does not replace if no matches are found", async () => { - await supertest(app) - .post("/flows/1/search?find=bananas&replace=monument") - .set(auth) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ - message: `Didn't find "bananas" in this flow, nothing to replace`, - matches: null, - }); +describe("HTML replacement", () => { + const originalHTML = ``; + const unsafeHTML = ""; + const safeHTML = ''; + + const mockFlowData: Flow["data"] = { + _root: { + edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], + }, + "6dwuQp5xjA": { + data: { + text: "No", + }, + type: 200, + }, + "8AWcYxZgBw": { + data: { + fn: "property.constraints.planning", + text: "Is it monument inside a portal?", + }, + type: 100, + edges: ["AJnWX6O1xt", "6dwuQp5xjA"], + }, + AJnWX6O1xt: { + data: { + val: "designated.monument", + text: "Yes", + description: originalHTML, + }, + type: 200, + }, + Hfh8KuSzUq: { + data: { + val: "designated.monument", + text: "Yes", + description: originalHTML, + }, + type: 200, + }, + RRQwM2zAgy: { + data: { + fn: "property.constraints.planning", + text: "Is it a monument", + }, + type: 100, + edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], + }, + ft26KlH7Oy: { + data: { + text: "No", + }, + type: 200, + }, + vcTgmVQAre: { + data: { + text: "internal-portal-test", + }, + type: 300, + edges: ["8AWcYxZgBw"], + }, + QsEdip17H5: { + type: 310, + data: { + flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", + }, + }, + }; + + const replacedFlowData: Flow["data"] = { + _root: { + edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], + }, + "6dwuQp5xjA": { + data: { + text: "No", + }, + type: 200, + }, + "8AWcYxZgBw": { + data: { + fn: "property.constraints.planning", + text: "Is it monument inside a portal?", + }, + type: 100, + edges: ["AJnWX6O1xt", "6dwuQp5xjA"], + }, + AJnWX6O1xt: { + data: { + val: "monument", + text: "Yes", + description: safeHTML, + }, + type: 200, + }, + Hfh8KuSzUq: { + data: { + val: "monument", + text: "Yes", + description: safeHTML, + }, + type: 200, + }, + RRQwM2zAgy: { + data: { + fn: "property.constraints.planning", + text: "Is it a monument", + }, + type: 100, + edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], + }, + ft26KlH7Oy: { + data: { + text: "No", + }, + type: 200, + }, + vcTgmVQAre: { + data: { + text: "internal-portal-test", + }, + type: 300, + edges: ["8AWcYxZgBw"], + }, + QsEdip17H5: { + type: 310, + data: { + flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", + }, + }, + }; + + beforeEach(() => { + queryMock.mockQuery({ + name: "GetFlowData", + matchOnVariables: false, + data: { + flow: { + data: mockFlowData, + slug: "test", + }, + }, }); -}); -it("updates flow data and returns matches if there are matches", async () => { - await supertest(app) - .post("/flows/1/search?find=designated.monument&replace=monument") - .set(auth) - .expect(200) - .then((res) => { - expect(res.body).toEqual({ - message: `Found 2 matches of "designated.monument" and replaced with "monument"`, - matches: { - AJnWX6O1xt: { - data: { - val: "designated.monument", + queryMock.mockQuery({ + name: "UpdateFlow", + matchOnVariables: false, + data: { + flow: { + data: replacedFlowData, + slug: "test", + }, + }, + }); + }); + + it("sanitises unsafe replace values", async () => { + await supertest(app) + .post(`/flows/2/search?find=${originalHTML}&replace=${unsafeHTML}`) + .set(auth) + .expect(200) + .then((res) => { + expect(res.body).toEqual({ + message: + 'Found 2 matches of "" and replaced with ""', + matches: { + AJnWX6O1xt: { + data: { + description: '', + }, }, - }, - Hfh8KuSzUq: { - data: { - val: "designated.monument", + Hfh8KuSzUq: { + data: { + description: '', + }, }, }, - }, - updatedFlow: replacedFlowData, + updatedFlow: replacedFlowData, + }); }); - }); + }); }); - -const mockFlowData: Flow["data"] = { - _root: { - edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], - }, - "6dwuQp5xjA": { - data: { - text: "No", - }, - type: 200, - }, - "8AWcYxZgBw": { - data: { - fn: "property.constraints.planning", - text: "Is it monument inside a portal?", - }, - type: 100, - edges: ["AJnWX6O1xt", "6dwuQp5xjA"], - }, - AJnWX6O1xt: { - data: { - val: "designated.monument", - text: "Yes", - }, - type: 200, - }, - Hfh8KuSzUq: { - data: { - val: "designated.monument", - text: "Yes", - }, - type: 200, - }, - RRQwM2zAgy: { - data: { - fn: "property.constraints.planning", - text: "Is it a monument", - }, - type: 100, - edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], - }, - ft26KlH7Oy: { - data: { - text: "No", - }, - type: 200, - }, - vcTgmVQAre: { - data: { - text: "internal-portal-test", - }, - type: 300, - edges: ["8AWcYxZgBw"], - }, - QsEdip17H5: { - type: 310, - data: { - flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", - }, - }, -}; - -const replacedFlowData: Flow["data"] = { - _root: { - edges: ["RRQwM2zAgy", "vcTgmVQAre", "QsEdip17H5"], - }, - "6dwuQp5xjA": { - data: { - text: "No", - }, - type: 200, - }, - "8AWcYxZgBw": { - data: { - fn: "property.constraints.planning", - text: "Is it monument inside a portal?", - }, - type: 100, - edges: ["AJnWX6O1xt", "6dwuQp5xjA"], - }, - AJnWX6O1xt: { - data: { - val: "monument", - text: "Yes", - }, - type: 200, - }, - Hfh8KuSzUq: { - data: { - val: "monument", - text: "Yes", - }, - type: 200, - }, - RRQwM2zAgy: { - data: { - fn: "property.constraints.planning", - text: "Is it a monument", - }, - type: 100, - edges: ["Hfh8KuSzUq", "ft26KlH7Oy"], - }, - ft26KlH7Oy: { - data: { - text: "No", - }, - type: 200, - }, - vcTgmVQAre: { - data: { - text: "internal-portal-test", - }, - type: 300, - edges: ["8AWcYxZgBw"], - }, - QsEdip17H5: { - type: 310, - data: { - flowId: "f54b6505-c352-4fbc-aca3-7c4be99b49d4", - }, - }, -}; diff --git a/api.planx.uk/modules/flows/findReplace/service.ts b/api.planx.uk/modules/flows/findReplace/service.ts index 471b2718d7..d79354b4c6 100644 --- a/api.planx.uk/modules/flows/findReplace/service.ts +++ b/api.planx.uk/modules/flows/findReplace/service.ts @@ -1,6 +1,6 @@ import { gql } from "graphql-request"; import { getFlowData } from "../../../helpers"; -import { getClient } from "../../../client"; +import { $api } from "../../../client"; import { FlowGraph } from "@opensystemslab/planx-core/types"; import { Flow } from "../../../types"; @@ -84,8 +84,10 @@ const findAndReplaceInFlow = async ( } // if matches, proceed with mutation to update flow data - const { client: $client } = getClient(); - const response = await $client.request( + + // XXX: This is using the API Hasura role + // If Find and Replace functionality is expanded to roles other than platformAdmin we'll need a manual permission check here to ensure that the user has permission to update the flows.data column + const response = await $api.client.request( gql` mutation UpdateFlow($data: jsonb = {}, $id: uuid!) { flow: update_flows_by_pk( diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index 3e5d8d7078..efd78d6ccd 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.5", "csv-stringify": "^6.3.0", "date-fns": "^2.29.3", + "dompurify": "^3.0.6", "express": "^4.18.2", "express-jwt": "^8.4.1", "express-pino-logger": "^7.0.0", @@ -27,6 +28,7 @@ "http-proxy-middleware": "^2.0.6", "husky": "^8.0.3", "isomorphic-fetch": "^3.0.0", + "jsdom": "^23.0.0", "jsondiffpatch": "^0.5.0", "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", @@ -72,9 +74,11 @@ "@types/cookie-parser": "^1.4.3", "@types/cookie-session": "^2.0.44", "@types/cors": "^2.8.13", + "@types/dompurify": "^3.0.5", "@types/express": "^4.17.17", "@types/express-pino-logger": "^4.0.3", "@types/jest": "^29.5.5", + "@types/jsdom": "^21.1.6", "@types/jsonwebtoken": "^9.0.2", "@types/lodash": "^4.14.197", "@types/lodash.omit": "^4.5.7", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index 2fe0c132fc..d701a1a3fe 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: date-fns: specifier: ^2.29.3 version: 2.30.0 + dompurify: + specifier: ^3.0.6 + version: 3.0.6 express: specifier: ^4.18.2 version: 4.18.2 @@ -77,6 +80,9 @@ dependencies: isomorphic-fetch: specifier: ^3.0.0 version: 3.0.0 + jsdom: + specifier: ^23.0.0 + version: 23.0.0 jsondiffpatch: specifier: ^0.5.0 version: 0.5.0 @@ -151,6 +157,9 @@ devDependencies: '@types/cors': specifier: ^2.8.13 version: 2.8.13 + '@types/dompurify': + specifier: ^3.0.5 + version: 3.0.5 '@types/express': specifier: ^4.17.17 version: 4.17.17 @@ -160,6 +169,9 @@ devDependencies: '@types/jest': specifier: ^29.5.5 version: 29.5.5 + '@types/jsdom': + specifier: ^21.1.6 + version: 21.1.6 '@types/jsonwebtoken': specifier: ^9.0.2 version: 9.0.2 @@ -1973,6 +1985,12 @@ packages: '@types/node': 16.18.38 dev: true + /@types/dompurify@3.0.5: + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + dependencies: + '@types/trusted-types': 2.0.7 + dev: true + /@types/express-pino-logger@4.0.3: resolution: {integrity: sha512-qdx2MZACwyopWYVOF759/vO5B7DLYIqoub5jasjih13lTfk31VnD/DI5Y6G2GrGrY1Z4XPwZke9it2f/oq8pcw==} dependencies: @@ -2049,6 +2067,14 @@ packages: pretty-format: 29.6.1 dev: true + /@types/jsdom@21.1.6: + resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==} + dependencies: + '@types/node': 18.18.1 + '@types/tough-cookie': 4.0.2 + parse5: 7.1.2 + dev: true + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} @@ -2257,7 +2283,10 @@ packages: /@types/tough-cookie@4.0.2: resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} - dev: false + + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: true /@types/uuid@9.0.2: resolution: {integrity: sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==} @@ -2446,6 +2475,15 @@ packages: engines: {node: '>=6.0'} dev: false + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -3376,6 +3414,13 @@ packages: engines: {node: '>= 6'} dev: false + /cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + dependencies: + rrweb-cssom: 0.6.0 + dev: false + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: false @@ -3391,6 +3436,14 @@ packages: type: 1.2.0 dev: false + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + dev: false + /date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -3431,6 +3484,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -3594,6 +3651,10 @@ packages: domelementtype: 2.3.0 dev: false + /dompurify@3.0.6: + resolution: {integrity: sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==} + dev: false + /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dependencies: @@ -3665,7 +3726,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -4693,6 +4753,13 @@ packages: react-is: 16.13.1 dev: false + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + dev: false + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -4717,6 +4784,16 @@ packages: toidentifier: 1.0.1 dev: false + /http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /http-proxy-middleware@2.0.6(@types/express@4.17.17): resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} engines: {node: '>=12.0.0'} @@ -4747,6 +4824,16 @@ packages: - debug dev: false + /https-proxy-agent@7.0.2: + resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -4770,6 +4857,13 @@ packages: safer-buffer: 2.1.2 dev: false + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754@1.1.13: resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} dev: false @@ -4986,6 +5080,10 @@ packages: isobject: 3.0.1 dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: false @@ -5693,6 +5791,42 @@ packages: dependencies: argparse: 2.0.1 + /jsdom@23.0.0: + resolution: {integrity: sha512-cbL/UCtohJguhFC7c2/hgW6BeZCNvP7URQGnx9tSJRYKCdnfbfWOrtuLTMfiB2VxKsx5wPHVsh/J0aBy9lIIhQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + cssstyle: 3.0.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.14.2 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -6358,6 +6492,10 @@ packages: boolbase: 1.0.0 dev: false + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: false + /oauth@0.9.15: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} dev: false @@ -6531,7 +6669,6 @@ packages: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: entities: 4.5.0 - dev: false /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -6815,6 +6952,10 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -6834,6 +6975,11 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: false + /pure-rand@6.0.2: resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} dev: true @@ -6858,6 +7004,10 @@ packages: deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. dev: false + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7108,6 +7258,10 @@ packages: dependencies: glob: 7.2.3 + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + dev: false + /rsvp@4.8.5: resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} engines: {node: 6.* || >= 7.*} @@ -7167,6 +7321,13 @@ packages: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -7634,6 +7795,10 @@ packages: swagger-ui-dist: 5.2.0 dev: false + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + /tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} dependencies: @@ -7729,10 +7894,27 @@ packages: engines: {node: '>=0.6'} dev: false + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + dependencies: + punycode: 2.3.1 + dev: false + /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -7936,6 +8118,11 @@ packages: set-value: 2.0.1 dev: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -7975,6 +8162,13 @@ packages: deprecated: Please see https://github.com/lydell/urix#deprecated dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /url@0.10.3: resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} dependencies: @@ -8048,6 +8242,13 @@ packages: engines: {node: '>= 0.8'} dev: false + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: false + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -8058,10 +8259,35 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 + dev: false + /whatwg-fetch@3.6.2: resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} dev: false + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: false + + /whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + dev: false + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -8132,6 +8358,19 @@ packages: signal-exit: 3.0.7 dev: true + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true @@ -8139,6 +8378,11 @@ packages: sax: 1.2.4 dev: false + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: false + /xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -8156,6 +8400,10 @@ packages: engines: {node: '>=4.0'} dev: false + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts index c642a57f31..b60b5f6784 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/editor.ts @@ -170,12 +170,11 @@ export const editorStore: StateCreator< }, createFlow: async (teamId, newSlug) => { - const data = { [ROOT_NODE_KEY]: { edges: [] } }; let response = (await client.mutate({ mutation: gql` mutation CreateFlow($data: jsonb, $slug: String, $teamId: Int) { insert_flows_one( - object: { data: $data, slug: $slug, team_id: $teamId, version: 1 } + object: { slug: $slug, team_id: $teamId, version: 1 } ) { id data @@ -185,7 +184,6 @@ export const editorStore: StateCreator< variables: { slug: newSlug, teamId, - data, }, })) as any; @@ -209,7 +207,7 @@ export const editorStore: StateCreator< seq: 1, src: "143711878a0ab64c35c32c6055358f5e", create: { - data, + data: { [ROOT_NODE_KEY]: { edges: [] } }, type: "http://sharejs.org/types/JSONv0", }, }, diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index 0975212613..2e6347e32a 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -307,16 +307,15 @@ permission: check: {} columns: + - copied_from + - created_at - creator_id - - team_id + - id - settings - slug - - created_at + - team_id - updated_at - - copied_from - - id - version - - data - role: teamEditor permission: check: @@ -328,16 +327,15 @@ - role: _eq: teamEditor columns: + - copied_from + - created_at - creator_id - - team_id + - id - settings - slug - - created_at + - team_id - updated_at - - copied_from - - id - version - - data select_permissions: - role: api permission: @@ -421,7 +419,6 @@ - role: platformAdmin permission: columns: - - data - settings - slug - team_id @@ -430,7 +427,6 @@ - role: teamEditor permission: columns: - - data - settings - slug - team_id diff --git a/hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/down.sql b/hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/down.sql new file mode 100644 index 0000000000..0cff8d22ec --- /dev/null +++ b/hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/down.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."flows" ALTER COLUMN "data" drop default; diff --git a/hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/up.sql b/hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/up.sql new file mode 100644 index 0000000000..1be78168cd --- /dev/null +++ b/hasura.planx.uk/migrations/1701100414893_alter_table_public_flows_alter_column_data/up.sql @@ -0,0 +1 @@ +alter table "public"."flows" alter column "data" set default '{ "_root": { "edges": [] } }';