From dab32138b7271aecbb61e44ce0f695c29193f16d Mon Sep 17 00:00:00 2001 From: Niko Sams Date: Tue, 14 Jan 2025 15:03:43 +0100 Subject: [PATCH 1/9] Demo Site: don't exclude middleware for /api/... requests (#3086) --- demo/admin/src/App.tsx | 2 +- demo/site/src/app/{ => [domain]}/api/status/route.tsx | 0 demo/site/src/app/{api => }/site-preview/route.ts | 0 demo/site/src/middleware.ts | 7 +++---- demo/site/src/middleware/{blockPreview.ts => preview.ts} | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) rename demo/site/src/app/{ => [domain]}/api/status/route.tsx (100%) rename demo/site/src/app/{api => }/site-preview/route.ts (100%) rename demo/site/src/middleware/{blockPreview.ts => preview.ts} (72%) diff --git a/demo/admin/src/App.tsx b/demo/admin/src/App.tsx index ef14c34dc86..ecca497406a 100644 --- a/demo/admin/src/App.tsx +++ b/demo/admin/src/App.tsx @@ -68,7 +68,7 @@ export function App() { siteConfig.scope.domain === "secondary" ? `${siteConfig.url}/block-preview` : `${siteConfig.url}/block-preview/${scope.domain}/${scope.language}`, - sitePreviewApiUrl: `${siteConfig.url}/api/site-preview`, + sitePreviewApiUrl: `${siteConfig.url}/site-preview`, }; }, }} diff --git a/demo/site/src/app/api/status/route.tsx b/demo/site/src/app/[domain]/api/status/route.tsx similarity index 100% rename from demo/site/src/app/api/status/route.tsx rename to demo/site/src/app/[domain]/api/status/route.tsx diff --git a/demo/site/src/app/api/site-preview/route.ts b/demo/site/src/app/site-preview/route.ts similarity index 100% rename from demo/site/src/app/api/site-preview/route.ts rename to demo/site/src/app/site-preview/route.ts diff --git a/demo/site/src/middleware.ts b/demo/site/src/middleware.ts index 58b34a15e3e..b975b1c2a4a 100644 --- a/demo/site/src/middleware.ts +++ b/demo/site/src/middleware.ts @@ -1,10 +1,10 @@ import { withAdminRedirectMiddleware } from "./middleware/adminRedirect"; -import { withBlockPreviewMiddleware } from "./middleware/blockPreview"; import { chain } from "./middleware/chain"; import { withCspHeadersMiddleware } from "./middleware/cspHeaders"; import { withDamRewriteMiddleware } from "./middleware/damRewrite"; import { withDomainRewriteMiddleware } from "./middleware/domainRewrite"; import { withPredefinedPagesMiddleware } from "./middleware/predefinedPages"; +import { withPreviewMiddleware } from "./middleware/preview"; import { withRedirectToMainHostMiddleware } from "./middleware/redirectToMainHost"; import { withSitePreviewMiddleware } from "./middleware/sitePreview"; @@ -14,7 +14,7 @@ export default chain([ withAdminRedirectMiddleware, withDamRewriteMiddleware, withCspHeadersMiddleware, // order matters: after redirects (that don't need csp headers), before everything else that needs csp headers - withBlockPreviewMiddleware, + withPreviewMiddleware, withPredefinedPagesMiddleware, withDomainRewriteMiddleware, // must be last (rewrites all urls) ]); @@ -23,14 +23,13 @@ export const config = { matcher: [ /* * Match all request paths except for the ones starting with: - * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, favicon.svg, favicon.png * - manifest.json * - robots.txt */ - "/((?!api|_next/static|_next/image|favicon.ico|favicon.svg|favicon.png|manifest.json|robots.txt).*)", + "/((?!_next/static|_next/image|favicon.ico|favicon.svg|favicon.png|manifest.json|robots.txt).*)", ], // TODO find a better solution for this (https://nextjs.org/docs/messages/edge-dynamic-code-evaluation) unstable_allowDynamic: [ diff --git a/demo/site/src/middleware/blockPreview.ts b/demo/site/src/middleware/preview.ts similarity index 72% rename from demo/site/src/middleware/blockPreview.ts rename to demo/site/src/middleware/preview.ts index 43168430427..6ec45d2c695 100644 --- a/demo/site/src/middleware/blockPreview.ts +++ b/demo/site/src/middleware/preview.ts @@ -2,9 +2,9 @@ import { NextRequest, NextResponse } from "next/server"; import { CustomMiddleware } from "./chain"; -export function withBlockPreviewMiddleware(middleware: CustomMiddleware) { +export function withPreviewMiddleware(middleware: CustomMiddleware) { return async (request: NextRequest) => { - if (request.nextUrl.pathname.startsWith("/block-preview/")) { + if (request.nextUrl.pathname.startsWith("/block-preview/") || request.nextUrl.pathname.startsWith("/site-preview/")) { // don't apply any other middlewares return NextResponse.next(); } From 394d037fbcff433c827456ab727d9362ba8b0d9e Mon Sep 17 00:00:00 2001 From: Daniel Karnutsch Date: Tue, 14 Jan 2025 15:05:05 +0100 Subject: [PATCH 2/9] Docs: Console command best practices for command and argument casing (#2660) Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> --- docs/docs/3-features-modules/5-console-commands/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/3-features-modules/5-console-commands/index.md b/docs/docs/3-features-modules/5-console-commands/index.md index 72cc5cdedab..7501d42e39b 100644 --- a/docs/docs/3-features-modules/5-console-commands/index.md +++ b/docs/docs/3-features-modules/5-console-commands/index.md @@ -61,6 +61,7 @@ npm run console:prod demo-command ## Best practices +- Use kebab case for command names and arguments. - Dangerous commands (e.g. resetting the database) should check the `NODE_ENV` and only run locally. ```ts From 77ba36cb3d41ba5ecb9f0ae52d3148f2300ea8a2 Mon Sep 17 00:00:00 2001 From: fichtnerma <72387685+fichtnerma@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:21:47 +0100 Subject: [PATCH 3/9] Support kB and MB when displaying the size of a file (#2823) --- demo/site/package.json | 1 + .../src/common/blocks/CallToActionBlock.tsx | 22 +++++++++++++------ demo/site/src/common/blocks/TextLinkBlock.tsx | 9 ++------ pnpm-lock.yaml | 8 +++++++ 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/demo/site/package.json b/demo/site/package.json index 73299185291..1284bfa3004 100644 --- a/demo/site/package.json +++ b/demo/site/package.json @@ -34,6 +34,7 @@ "@opentelemetry/sdk-node": "^0.53.0", "cache-manager": "^5.5.3", "express": "^4.0.0", + "filesize": "^10.1.6", "fs-extra": "^9.0.0", "graphql": "^15.0.0", "graphql-tag": "^2.12.6", diff --git a/demo/site/src/common/blocks/CallToActionBlock.tsx b/demo/site/src/common/blocks/CallToActionBlock.tsx index c5a3c75d92a..729eeb8805b 100644 --- a/demo/site/src/common/blocks/CallToActionBlock.tsx +++ b/demo/site/src/common/blocks/CallToActionBlock.tsx @@ -1,6 +1,7 @@ "use client"; import { PropsWithData, withPreview } from "@comet/cms-site"; import { CallToActionBlockData } from "@src/blocks.generated"; +import { filesize } from "filesize"; import { Button, ButtonVariant } from "../components/Button"; import { HiddenIfInvalidLink } from "../helpers/HiddenIfInvalidLink"; @@ -13,12 +14,19 @@ const buttonVariantMap: Record }; export const CallToActionBlock = withPreview( - ({ data: { textLink, variant } }: PropsWithData) => ( - - - - ), + ({ data: { textLink, variant } }: PropsWithData) => { + const linkBlock = textLink.link.block; + let buttonText = textLink.text; + if (linkBlock && linkBlock.type === "damFileDownload" && "file" in linkBlock.props && linkBlock.props.file) { + buttonText = `${buttonText} (${filesize(linkBlock?.props.file?.size)})`; + } + return ( + + + + ); + }, { label: "Call To Action" }, ); diff --git a/demo/site/src/common/blocks/TextLinkBlock.tsx b/demo/site/src/common/blocks/TextLinkBlock.tsx index e7fff8e3304..598748f07f5 100644 --- a/demo/site/src/common/blocks/TextLinkBlock.tsx +++ b/demo/site/src/common/blocks/TextLinkBlock.tsx @@ -1,6 +1,7 @@ "use client"; import { PropsWithData, withPreview } from "@comet/cms-site"; import { TextLinkBlockData } from "@src/blocks.generated"; +import { filesize } from "filesize"; import styled from "styled-components"; import { LinkBlock } from "./LinkBlock"; @@ -8,13 +9,7 @@ import { LinkBlock } from "./LinkBlock"; export const TextLinkBlock = withPreview( ({ data: { link, text } }: PropsWithData) => { if (link.block && link.block.type === "damFileDownload" && "file" in link.block.props && link.block.props.file) { - return ( - - <> - {text} ({Math.round(link.block.props.file.size / 1024)} KB) - - - ); + return {`${text} (${filesize(link.block.props.file.size)})`}; } return {text}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c85ceb8d006..41cff3228be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -679,6 +679,9 @@ importers: express: specifier: ^4.0.0 version: 4.18.2 + filesize: + specifier: ^10.1.6 + version: 10.1.6 fs-extra: specifier: ^9.0.0 version: 9.1.0 @@ -22415,6 +22418,11 @@ packages: minimatch: 5.1.6 dev: true + /filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + dev: false + /filesize@8.0.7: resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} engines: {node: '>= 0.4.0'} From 92b3255d2225bfb3584bea070c9505cfec2fe0ed Mon Sep 17 00:00:00 2001 From: fichtnerma <72387685+fichtnerma@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:45:10 +0100 Subject: [PATCH 4/9] Hide group title in `CrudMoreActionsMenu` when only one group is present (#2920) --- .changeset/cyan-shoes-beg.md | 5 +++ .../src/dataGrid/CrudMoreActionsMenu.tsx | 32 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 .changeset/cyan-shoes-beg.md diff --git a/.changeset/cyan-shoes-beg.md b/.changeset/cyan-shoes-beg.md new file mode 100644 index 00000000000..b937a98a032 --- /dev/null +++ b/.changeset/cyan-shoes-beg.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Hide group title in `CrudMoreActionsMenu` when only one group is present diff --git a/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx b/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx index 4fc1a9e1502..0656e0d07ee 100644 --- a/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx +++ b/packages/admin/admin/src/dataGrid/CrudMoreActionsMenu.tsx @@ -44,7 +44,7 @@ export interface CrudMoreActionsMenuProps } interface CrudMoreActionsGroupProps { - groupTitle: ReactNode; + groupTitle?: ReactNode; menuListProps?: MenuListProps; typographyProps?: ComponentProps; } @@ -52,9 +52,11 @@ interface CrudMoreActionsGroupProps { function CrudMoreActionsGroup({ groupTitle, children, menuListProps, typographyProps }: PropsWithChildren) { return ( <> - theme.palette.grey[500]} sx={{ padding: "20px 15px 0 15px" }} {...typographyProps}> - {groupTitle} - + {groupTitle && ( + theme.palette.grey[500]} sx={{ padding: "20px 15px 0 15px" }} {...typographyProps}> + {groupTitle} + + )} {children} ); @@ -116,11 +118,13 @@ export function CrudMoreActionsMenu({ slotProps, overallActions, selectiveAction const handleClick = (event: MouseEvent) => setAnchorEl(event.currentTarget); const handleClose = () => setAnchorEl(null); + const hasOverallActions = !!overallActions?.length; + const hasSelectiveActions = !!selectiveActions?.length; return ( <> } {...buttonProps} onClick={handleClick}> - + {!!selectionSize && } - {!!overallActions?.length && ( + {hasOverallActions && ( } + groupTitle={ + hasSelectiveActions ? ( + + ) : undefined + } {...groupProps} > {overallActions.map((item, index) => { @@ -165,11 +173,15 @@ export function CrudMoreActionsMenu({ slotProps, overallActions, selectiveAction )} - {!!overallActions?.length && !!selectiveActions?.length && } + {hasOverallActions && hasSelectiveActions && } - {!!selectiveActions?.length && ( + {hasSelectiveActions && ( } + groupTitle={ + hasOverallActions ? ( + + ) : undefined + } {...groupProps} > {selectiveActions.map((item, index) => { From 753cd6f04e709ef6c45da0c9d6ab10f578f22de3 Mon Sep 17 00:00:00 2001 From: Franz Unger Date: Tue, 14 Jan 2025 16:40:29 +0100 Subject: [PATCH 5/9] Add option for base64 encoding in `inject-site-configs` command (#3053) Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> --- .changeset/smooth-shrimps-exist.md | 5 +++++ demo/admin/src/config.tsx | 2 +- demo/api/src/config/environment-variables.ts | 2 +- demo/site/src/util/siteConfig.ts | 2 +- package.json | 2 +- packages/cli/src/commands/site-configs.ts | 7 ++++++- 6 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 .changeset/smooth-shrimps-exist.md diff --git a/.changeset/smooth-shrimps-exist.md b/.changeset/smooth-shrimps-exist.md new file mode 100644 index 00000000000..4e536a8fae8 --- /dev/null +++ b/.changeset/smooth-shrimps-exist.md @@ -0,0 +1,5 @@ +--- +"@comet/cli": minor +--- + +Add option for base64 encoding in `inject-site-configs` command diff --git a/demo/admin/src/config.tsx b/demo/admin/src/config.tsx index 6e9d14261e0..00c30bb00d0 100644 --- a/demo/admin/src/config.tsx +++ b/demo/admin/src/config.tsx @@ -21,7 +21,7 @@ export function createConfig() { ...cometConfig, apiUrl: environmentVariables.API_URL, adminUrl: environmentVariables.ADMIN_URL, - sitesConfig: JSON.parse(environmentVariables.PUBLIC_SITE_CONFIGS) as PublicSiteConfig[], + sitesConfig: JSON.parse(atob(environmentVariables.PUBLIC_SITE_CONFIGS)) as PublicSiteConfig[], buildDate: environmentVariables.BUILD_DATE, buildNumber: environmentVariables.BUILD_NUMBER, commitSha: environmentVariables.COMMIT_SHA, diff --git a/demo/api/src/config/environment-variables.ts b/demo/api/src/config/environment-variables.ts index 56839c72b16..783484d0b5d 100644 --- a/demo/api/src/config/environment-variables.ts +++ b/demo/api/src/config/environment-variables.ts @@ -146,6 +146,6 @@ export class EnvironmentVariables { SITE_PREVIEW_SECRET: string; @IsArray() - @Transform(({ value }) => JSON.parse(value)) + @Transform(({ value }) => JSON.parse(Buffer.from(value, "base64").toString())) PRIVATE_SITE_CONFIGS: PrivateSiteConfig[]; } diff --git a/demo/site/src/util/siteConfig.ts b/demo/site/src/util/siteConfig.ts index d668ab18077..a96282400e2 100644 --- a/demo/site/src/util/siteConfig.ts +++ b/demo/site/src/util/siteConfig.ts @@ -28,7 +28,7 @@ export function getSiteConfigs() { if (!siteConfigs) { const json = process.env.PUBLIC_SITE_CONFIGS; if (!json) throw new Error("process.env.PUBLIC_SITE_CONFIGS must be set."); - siteConfigs = JSON.parse(json) as PublicSiteConfig[]; + siteConfigs = JSON.parse(atob(json)) as PublicSiteConfig[]; } return siteConfigs; } diff --git a/package.json b/package.json index e44106cd299..4821e87decd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "license": "BSD-2-Clause", "scripts": { - "create-site-configs-env": "npx @comet/cli inject-site-configs -f demo/site-configs/site-configs.ts -i demo/.env.site-configs.tpl -o demo/.env.site-configs -d", + "create-site-configs-env": "npx @comet/cli inject-site-configs -f demo/site-configs/site-configs.ts -i demo/.env.site-configs.tpl -o demo/.env.site-configs --base64", "build:storybook": "pnpm recursive --filter '@comet/*admin*' --filter '@comet/eslint-plugin' --filter '@comet/cli' run build && pnpm --filter comet-storybook run build-storybook", "build:packages": "pnpm recursive --filter '@comet/*' run build", "build:docs": "pnpm recursive --filter '@comet/eslint-plugin' --filter '@comet/admin*' --filter 'comet-docs' run build", diff --git a/packages/cli/src/commands/site-configs.ts b/packages/cli/src/commands/site-configs.ts index 77f9cf22959..0a903d27e26 100644 --- a/packages/cli/src/commands/site-configs.ts +++ b/packages/cli/src/commands/site-configs.ts @@ -11,6 +11,7 @@ export const injectSiteConfigsCommand = new Command("inject-site-configs") .requiredOption("-i, --in-file ", "The filename of a template file to inject.") .requiredOption("-o, --out-file ", "Write the injected template to a file.") .option("-d --dotenv", "dotenv compatibility") // https://github.com/motdotla/dotenv/issues/521#issuecomment-999016064 + .option("--base64", "use base64 encoding") .option("-f, --site-config-file ", "Path to ts-file which provides a default export with (env: string) => SiteConfig[]") .action(async (options) => { const configFile = `${process.cwd()}/${options.siteConfigFile || "site-configs.ts"}`; @@ -51,7 +52,11 @@ export const injectSiteConfigsCommand = new Command("inject-site-configs") console.error(`inject-site-configs: ERROR: type must be ${Object.keys(replacerFunctions).join("|")} (got ${type})`); return substr; } - const ret = JSON.stringify(replacerFunctions[type](siteConfigs, env)).replace(/\\/g, "\\\\"); + const str = replacerFunctions[type](siteConfigs, env); + if (options.base64) { + return Buffer.from(JSON.stringify(str)).toString("base64"); + } + const ret = JSON.stringify(str).replace(/\\/g, "\\\\"); if (options.dotenv) return ret.replace(/\$/g, "\\$"); return ret; }); From ad93dfb5e105a0a2b673cd9e14bcc2d6d15d8ccb Mon Sep 17 00:00:00 2001 From: Daniel Karnutsch Date: Wed, 15 Jan 2025 08:03:59 +0100 Subject: [PATCH 6/9] Demo Admin: Adapt server based on Starter (#3098) --- demo/admin/package.json | 1 + demo/admin/server/index.js | 69 ++++++++ demo/admin/server/package.json | 11 +- demo/admin/server/server.js | 41 ----- pnpm-lock.yaml | 313 +++++++++++++++++++++++++-------- 5 files changed, 310 insertions(+), 125 deletions(-) create mode 100644 demo/admin/server/index.js delete mode 100644 demo/admin/server/server.js diff --git a/demo/admin/package.json b/demo/admin/package.json index 54926c5d7da..e949579fe50 100644 --- a/demo/admin/package.json +++ b/demo/admin/package.json @@ -19,6 +19,7 @@ "lint:prettier": "npx prettier --check './**/*.{js,json,md,yml,yaml}'", "lint:tsc": "tsc --project .", "lint:generated-files-not-modified": "$npm_execpath admin-generator && git diff --exit-code HEAD -- src/**/generated", + "serve": "node server", "start": "run-s intl:compile && run-p gql:types generate-block-types && dotenv -e .env.site-configs -- chokidar --initial -s \"../../packages/admin/*/src/**\" -c \"kill-port $ADMIN_PORT && vite --force\"" }, "dependencies": { diff --git a/demo/admin/server/index.js b/demo/admin/server/index.js new file mode 100644 index 00000000000..5434297dd83 --- /dev/null +++ b/demo/admin/server/index.js @@ -0,0 +1,69 @@ +/* eslint-disable no-undef */ +const express = require("express"); +const compression = require("compression"); +const helmet = require("helmet"); +const fs = require("fs"); + +const app = express(); +const port = process.env.APP_PORT ?? 3000; + +let indexFile = fs.readFileSync("./build/index.html", "utf8"); + +// Replace environment variables +indexFile = indexFile.replace(/\$([A-Z_]+)/g, (match, p1) => { + return process.env[p1] || ""; +}); + +app.use(compression()); +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + "script-src": ["'self'", "'unsafe-inline'"], + "img-src": ["'self'", "https:", "data:"], + "default-src": ["'self'", "https:"], + "media-src": ["'self'", "https:"], + "style-src": ["'self'", "https:", "'unsafe-inline'"], + "font-src": ["'self'", "https:", "data:"], + }, + }, + xXssProtection: false, + strictTransportSecurity: { + maxAge: 63072000, + includeSubDomains: true, + preload: true, + }, + }), +); + +app.get("/status/health", (req, res) => { + res.send("OK!"); +}); + +app.use( + express.static("./build", { + index: false, // Don't send index.html for requests to "/" as it will be handled by the fallback route (with replaced environment variables) + setHeaders: (res, path, stat) => { + if (path.endsWith(".js")) { + // The js file is static and the index.html uses a parameter as cache buster + // implemented as suggested by https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#caching_static_assets + res.setHeader("cache-control", "public, max-age=31536000, immutable"); + } else { + // Icons and Fonts could be changed over time, cache for 7d + res.setHeader("cache-control", "public, max-age=604800, immutable"); + } + }, + }), +); + +// As a fallback, route everything to index.html +app.get("*", (req, res) => { + // Don't cache the index.html at all to make sure applications updates are applied + // implemented as suggested by https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#preventing_storing + res.setHeader("cache-control", "no-store"); + res.send(indexFile); +}); + +app.listen(port, () => { + console.log(`Admin app listening at http://localhost:${port}`); +}); diff --git a/demo/admin/server/package.json b/demo/admin/server/package.json index ce1c559aed0..b11225bb51c 100644 --- a/demo/admin/server/package.json +++ b/demo/admin/server/package.json @@ -1,13 +1,12 @@ { "name": "comet-demo-admin-server", - "version": "1.0.0", "private": true, "dependencies": { - "compression": "^1.7.4", - "express": "^4.18.2" + "compression": "^1.7.5", + "express": "^4.21.1", + "helmet": "^7.2.0" }, - "scripts": { - "preserve": "envsubst < \"../build/index.html\" > \"/tmp/index.html\" && mv /tmp/index.html ../build/index.html", - "serve": "node server.js" + "engines": { + "node": "22" } } diff --git a/demo/admin/server/server.js b/demo/admin/server/server.js deleted file mode 100644 index 3556476dfbb..00000000000 --- a/demo/admin/server/server.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable no-undef */ -const express = require("express"); -const compression = require("compression"); - -const app = express(); -const port = process.env.APP_PORT ?? 3000; - -app.use(compression()); - -app.get("/status/health", (req, res) => { - res.send("OK!"); -}); - -// Serve static build an cache for 30d -app.use( - express.static("../build", { - setHeaders: (res, path, stat) => { - if (path.endsWith(".html")) { - // Don't cache the index.html at all - // implemented as suggested by https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#preventing_caching - res.setHeader("cache-control", "no-store, max-age: 0"); - } else if (path.endsWith(".js")) { - // The js file is static and the index.html uses a parameter as cache buster - // implemented as suggested by https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#caching_static_assets - res.setHeader("cache-control", "public, max-age=31536000, immutable"); - } else { - // Icons and Fonts could be changed over time - res.setHeader("cache-control", "public, max-age=604800, immutable"); - } - }, - }), -); - -// As a fallback, route everything to index.html -app.get("*", (req, res) => { - res.sendFile(`index.html`, { root: `${__dirname}/../build/` }); -}); - -app.listen(port, () => { - console.log(`Admin app listening at http://localhost:${port}`); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41cff3228be..f746b2010a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -379,11 +379,14 @@ importers: demo/admin/server: dependencies: compression: - specifier: ^1.7.4 - version: 1.7.4 + specifier: ^1.7.5 + version: 1.7.5 express: - specifier: ^4.18.2 - version: 4.18.2 + specifier: ^4.21.1 + version: 4.21.2 + helmet: + specifier: ^7.2.0 + version: 7.2.0 demo/api: dependencies: @@ -17460,10 +17463,10 @@ packages: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 is-string: 1.0.7 dev: false @@ -17525,7 +17528,7 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.23.3 es-shim-unscopables: 1.0.0 @@ -17535,7 +17538,7 @@ packages: resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 @@ -17545,7 +17548,7 @@ packages: resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.23.3 es-shim-unscopables: 1.0.0 @@ -17554,11 +17557,11 @@ packages: /array.prototype.tosorted@1.1.1: resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 dev: false /array.prototype.tosorted@1.1.4: @@ -18180,6 +18183,26 @@ packages: transitivePeerDependencies: - supports-color + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /bonjour-service@1.1.0: resolution: {integrity: sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q==} dependencies: @@ -18419,7 +18442,7 @@ packages: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.2 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 /call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} @@ -19050,6 +19073,21 @@ packages: - supports-color dev: false + /compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} + engines: {node: '>= 0.8.0'} + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.0.2 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /compute-scroll-into-view@1.0.20: resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} dev: false @@ -19131,6 +19169,11 @@ packages: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -19162,6 +19205,11 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + /cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + dev: false + /copy-text-to-clipboard@3.2.0: resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} engines: {node: '>=12'} @@ -20277,9 +20325,9 @@ packages: /deep-equal@2.2.0: resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 es-get-iterator: 1.1.3 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 is-arguments: 1.1.1 is-array-buffer: 3.0.1 is-date-object: 1.0.5 @@ -20290,7 +20338,7 @@ packages: object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.4.3 - side-channel: 1.0.4 + side-channel: 1.0.6 which-boxed-primitive: 1.0.2 which-collection: 1.0.1 which-typed-array: 1.1.9 @@ -20353,7 +20401,7 @@ packages: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} engines: {node: '>= 0.4'} dependencies: - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.2 object-keys: 1.1.1 /define-properties@1.2.1: @@ -20361,7 +20409,7 @@ packages: engines: {node: '>= 0.4'} dependencies: define-data-property: 1.1.4 - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.2 object-keys: 1.1.1 dev: false @@ -20955,6 +21003,11 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -21052,18 +21105,18 @@ packages: engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.7 es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function-bind: 1.1.2 function.prototype.name: 1.1.5 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 get-symbol-description: 1.0.0 globalthis: 1.0.3 gopd: 1.0.1 has: 1.0.3 - has-property-descriptors: 1.0.0 - has-proto: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 has-symbols: 1.0.3 internal-slot: 1.0.4 is-array-buffer: 3.0.1 @@ -21074,7 +21127,7 @@ packages: is-string: 1.0.7 is-typed-array: 1.1.10 is-weakref: 1.0.2 - object-inspect: 1.12.3 + object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.4.3 @@ -21151,7 +21204,7 @@ packages: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 is-arguments: 1.1.1 is-map: 2.0.2 @@ -21201,7 +21254,7 @@ packages: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 has: 1.0.3 has-tostringtag: 1.0.0 @@ -22136,6 +22189,45 @@ packages: transitivePeerDependencies: - supports-color + /express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.4 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -22496,6 +22588,21 @@ packages: transitivePeerDependencies: - supports-color + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -22852,7 +22959,7 @@ packages: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 functions-have-names: 1.2.3 @@ -22918,20 +23025,13 @@ packages: pify: 5.0.0 dev: false - /get-intrinsic@1.2.0: - resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} - dependencies: - function-bind: 1.1.2 - has: 1.0.3 - has-symbols: 1.0.3 - /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - has-proto: 1.0.1 + has-proto: 1.0.3 has-symbols: 1.0.3 hasown: 2.0.2 @@ -23001,8 +23101,8 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 /get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} @@ -23214,7 +23314,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 /got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} @@ -23548,24 +23648,14 @@ packages: engines: {node: '>=8'} dev: true - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - dependencies: - get-intrinsic: 1.2.0 - /has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: es-define-property: 1.0.0 - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - /has-proto@1.0.3: resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} engines: {node: '>= 0.4'} - dev: false /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} @@ -23689,6 +23779,11 @@ packages: engines: {node: '>=10.0.0'} dev: false + /helmet@7.2.0: + resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==} + engines: {node: '>=16.0.0'} + dev: false + /history@4.10.1: resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} dependencies: @@ -24175,9 +24270,9 @@ packages: resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 has: 1.0.3 - side-channel: 1.0.4 + side-channel: 1.0.6 /internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} @@ -24185,7 +24280,7 @@ packages: dependencies: es-errors: 1.3.0 hasown: 2.0.2 - side-channel: 1.0.4 + side-channel: 1.0.6 dev: false /internmap@2.0.3: @@ -24291,8 +24386,8 @@ packages: /is-array-buffer@3.0.1: resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 is-typed-array: 1.1.13 /is-array-buffer@3.0.4: @@ -24510,7 +24605,7 @@ packages: resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 dev: false @@ -24651,7 +24746,7 @@ packages: /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 /is-shared-array-buffer@1.0.3: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} @@ -24695,7 +24790,7 @@ packages: engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.7 for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 @@ -24737,13 +24832,13 @@ packages: /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 /is-weakset@2.0.2: resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 /is-weakset@2.0.3: resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} @@ -26700,6 +26795,10 @@ packages: /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -27438,6 +27537,11 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + /negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + dev: false + /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -27809,11 +27913,11 @@ packages: /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true /object-inspect@1.13.2: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} - dev: false /object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} @@ -27835,7 +27939,7 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -27854,7 +27958,7 @@ packages: resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 dev: false @@ -27872,7 +27976,7 @@ packages: resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 dev: false @@ -27907,7 +28011,7 @@ packages: resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 dev: false @@ -28426,6 +28530,10 @@ packages: lru-cache: 10.4.3 minipass: 7.1.2 + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + dev: false + /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -29668,6 +29776,13 @@ packages: dependencies: side-channel: 1.0.4 + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -29760,6 +29875,16 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /raw-loader@4.0.2(webpack@5.88.2): resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} engines: {node: '>= 10.13.0'} @@ -30648,7 +30773,7 @@ packages: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 functions-have-names: 1.2.3 @@ -31154,8 +31279,8 @@ packages: /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 is-regex: 1.1.4 /safe-regex-test@1.0.3: @@ -31336,6 +31461,27 @@ packages: transitivePeerDependencies: - supports-color + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} dependencies: @@ -31387,6 +31533,18 @@ packages: transitivePeerDependencies: - supports-color + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + dev: false + /server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} dev: false @@ -31501,9 +31659,9 @@ packages: /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 - object-inspect: 1.12.3 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 /side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} @@ -31513,7 +31671,6 @@ packages: es-errors: 1.3.0 get-intrinsic: 1.2.4 object-inspect: 1.13.2 - dev: false /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -32005,14 +32162,14 @@ packages: /string.prototype.matchall@4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 internal-slot: 1.0.4 regexp.prototype.flags: 1.4.3 - side-channel: 1.0.4 + side-channel: 1.0.6 dev: false /string.prototype.padend@3.1.4: @@ -32043,7 +32200,7 @@ packages: /string.prototype.trimend@1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 @@ -32058,7 +32215,7 @@ packages: /string.prototype.trimstart@1.0.6: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.1.4 es-abstract: 1.21.1 @@ -33165,7 +33322,7 @@ packages: /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 for-each: 0.3.3 is-typed-array: 1.1.13 @@ -33215,7 +33372,7 @@ packages: /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 @@ -34145,10 +34302,10 @@ packages: bonjour-service: 1.1.0 chokidar: 3.5.3 colorette: 2.0.19 - compression: 1.7.4 + compression: 1.7.5 connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.18.2 + express: 4.21.2 graceful-fs: 4.2.11 html-entities: 2.3.3 http-proxy-middleware: 2.0.6(@types/express@4.17.16) @@ -34411,7 +34568,7 @@ packages: engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.7 for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 From 27d784af7b77b2b8b1779e2e7587ea73721a57ba Mon Sep 17 00:00:00 2001 From: Franz Unger Date: Wed, 15 Jan 2025 09:16:55 +0100 Subject: [PATCH 7/9] Fix site-preview path check in Middleware (#3126) --- demo/site/src/middleware/preview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/site/src/middleware/preview.ts b/demo/site/src/middleware/preview.ts index 6ec45d2c695..7d27b52fe39 100644 --- a/demo/site/src/middleware/preview.ts +++ b/demo/site/src/middleware/preview.ts @@ -4,7 +4,7 @@ import { CustomMiddleware } from "./chain"; export function withPreviewMiddleware(middleware: CustomMiddleware) { return async (request: NextRequest) => { - if (request.nextUrl.pathname.startsWith("/block-preview/") || request.nextUrl.pathname.startsWith("/site-preview/")) { + if (request.nextUrl.pathname.startsWith("/block-preview/") || request.nextUrl.pathname === "/site-preview") { // don't apply any other middlewares return NextResponse.next(); } From 64173b51343528ab18bc3f07b6b1002157d58614 Mon Sep 17 00:00:00 2001 From: fichtnerma <72387685+fichtnerma@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:17:47 +0100 Subject: [PATCH 8/9] Fix page tree node slug validation to prevent URL encoded characters (#2229) Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> Co-authored-by: Daniel Karnutsch --- .changeset/blue-flies-compete.md | 6 ++++ .../src/pages/createEditPageNode.tsx | 2 +- .../src/common/validators/is-slug.spec.ts | 28 +++++++++++++++++++ .../cms-api/src/common/validators/is-slug.ts | 4 +-- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .changeset/blue-flies-compete.md diff --git a/.changeset/blue-flies-compete.md b/.changeset/blue-flies-compete.md new file mode 100644 index 00000000000..f3fb42f0f34 --- /dev/null +++ b/.changeset/blue-flies-compete.md @@ -0,0 +1,6 @@ +--- +"@comet/cms-admin": patch +"@comet/cms-api": patch +--- + +Fix page tree node slug validation to prevent URL encoded characters diff --git a/packages/admin/cms-admin/src/pages/createEditPageNode.tsx b/packages/admin/cms-admin/src/pages/createEditPageNode.tsx index d5bce8f3bad..3eeb0e60506 100644 --- a/packages/admin/cms-admin/src/pages/createEditPageNode.tsx +++ b/packages/admin/cms-admin/src/pages/createEditPageNode.tsx @@ -533,7 +533,7 @@ const transformToSlug = (name: string, locale: string) => { }; const isValidSlug = (value: string) => { - return /^([a-zA-Z0-9-._~]|%[0-9a-fA-F]{2})+$/.test(value); + return /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/.test(value); }; interface InitialValues { diff --git a/packages/api/cms-api/src/common/validators/is-slug.spec.ts b/packages/api/cms-api/src/common/validators/is-slug.spec.ts index 96250e0f11b..8e7173a83db 100644 --- a/packages/api/cms-api/src/common/validators/is-slug.spec.ts +++ b/packages/api/cms-api/src/common/validators/is-slug.spec.ts @@ -6,6 +6,18 @@ describe("IsSlug", () => { const validator = new IsSlugConstraint(); expect(await validator.validate("comet")).toBe(true); }); + it("should accept a valid slug with numbers", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("comet--2")).toBe(true); + }); + it("should accept a valid slug with special char at the end", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("abc-")).toBe(true); + }); + it("should accept a valid slug with uppercase characters", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("Comet")).toBe(true); + }); it("should not accept space", async () => { const validator = new IsSlugConstraint(); expect(await validator.validate("com et")).toBe(false); @@ -14,5 +26,21 @@ describe("IsSlug", () => { const validator = new IsSlugConstraint(); expect(await validator.validate("comät")).toBe(false); }); + it("should not allow url encoded characters", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("slug%2F2")).toBeFalsy(); + }); + it("should not allow percent encoded special characters ../", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("%2E%2E%2Ftest")).toBeFalsy(); + }); + it("should not allow special characters ../", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("../test")).toBeFalsy(); + }); + it("should not allow special character / ", async () => { + const validator = new IsSlugConstraint(); + expect(await validator.validate("slug/")).toBeFalsy(); + }); }); }); diff --git a/packages/api/cms-api/src/common/validators/is-slug.ts b/packages/api/cms-api/src/common/validators/is-slug.ts index 80e1227eafe..381fa36e588 100644 --- a/packages/api/cms-api/src/common/validators/is-slug.ts +++ b/packages/api/cms-api/src/common/validators/is-slug.ts @@ -15,8 +15,8 @@ export const IsSlug = (validationOptions?: ValidationOptions) => { @ValidatorConstraint({ name: "IsSlug", async: true }) export class IsSlugConstraint implements ValidatorConstraintInterface { async validate(value: string): Promise { - // Regex matches unreserved characters and percent encoding (see https://tools.ietf.org/html/rfc3986#section-2.1) - return /^([a-zA-Z0-9-._~]|%[0-9a-fA-F]{2})+$/.test(value); + // Regex matches unreserved characters + return /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/.test(value); } defaultMessage(): string { From 0bb181a52bfaa48f360ed73cf30faefa9e2846a7 Mon Sep 17 00:00:00 2001 From: fichtnerma <72387685+fichtnerma@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:23:23 +0100 Subject: [PATCH 9/9] Prevent Data Grids with the same name to overwrite each others pinned and column-visibility states (#2574) Co-authored-by: Markus Fichtner --- .changeset/violet-goats-study.md | 5 +++++ .../admin/src/dataGrid/usePersistentColumnState.tsx | 13 +++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 .changeset/violet-goats-study.md diff --git a/.changeset/violet-goats-study.md b/.changeset/violet-goats-study.md new file mode 100644 index 00000000000..efc5e25c86d --- /dev/null +++ b/.changeset/violet-goats-study.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": patch +--- + +`usePersistentColumnState`: Prevent Data Grids with the same name to overwrite each others pinned and column-visibility states diff --git a/packages/admin/admin/src/dataGrid/usePersistentColumnState.tsx b/packages/admin/admin/src/dataGrid/usePersistentColumnState.tsx index 012d27944ea..0b468301506 100644 --- a/packages/admin/admin/src/dataGrid/usePersistentColumnState.tsx +++ b/packages/admin/admin/src/dataGrid/usePersistentColumnState.tsx @@ -1,5 +1,6 @@ import { DataGridProps, GridColumnVisibilityModel, useGridApiRef } from "@mui/x-data-grid"; import { MutableRefObject, useCallback, useEffect, useMemo, useState } from "react"; +import { useRouteMatch } from "react-router"; import { useStoredState } from "../hooks/useStoredState"; import { GridColDef } from "./GridColDef"; @@ -55,10 +56,13 @@ type GridProps = Omit & { export function usePersistentColumnState(stateKey: string): GridProps { const apiRef = useGridApiRef(); const columns = useGridColumns(apiRef); + const match = useRouteMatch(); + + const storageKeyPrefix = `${match.path}${stateKey}`; const mediaQueryColumnVisibilityModel = useVisibilityModelFromColumnMediaQueries(columns); const [storedColumnVisibilityModel, setStoredColumnVisibilityModel] = useStoredState( - `${stateKey}ColumnVisibility`, + `${storageKeyPrefix}ColumnVisibility`, {}, ); @@ -80,7 +84,8 @@ export function usePersistentColumnState(stateKey: string): GridProps { [mediaQueryColumnVisibilityModel, setStoredColumnVisibilityModel], ); - const [pinnedColumns, setPinnedColumns] = useStoredState(`${stateKey}PinnedColumns`, {}); + const [pinnedColumns, setPinnedColumns] = useStoredState(`${storageKeyPrefix}PinnedColumns`, {}); + const handlePinnedColumnsChange = useCallback( (newModel: GridPinnedColumns) => { setPinnedColumns(newModel); @@ -103,7 +108,7 @@ export function usePersistentColumnState(stateKey: string): GridProps { }, [columns, setPinnedColumns, pinnedColumns]); //no API for column dimensions as controlled state, export on change instead - const columnDimensionsKey = `${stateKey}ColumnDimensions`; + const columnDimensionsKey = `${storageKeyPrefix}ColumnDimensions`; const initialColumnDimensions = useMemo(() => { const serializedState = window.localStorage.getItem(columnDimensionsKey); return serializedState ? JSON.parse(serializedState) : undefined; @@ -115,7 +120,7 @@ export function usePersistentColumnState(stateKey: string): GridProps { }, [columnDimensionsKey, apiRef]); //no API for column order as controlled state, export on change instead - const columnOrderKey = `${stateKey}ColumnOrder`; + const columnOrderKey = `${storageKeyPrefix}ColumnOrder`; const initialColumnOrder = useMemo(() => { const serializedState = window.localStorage.getItem(columnOrderKey); return serializedState ? JSON.parse(serializedState) : undefined;