From cf762ec6b1f294f3a549040b8f88820a44a0675e Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 7 Feb 2024 17:35:08 +0200 Subject: [PATCH 01/10] [Next.js] [BYOC] Component Builder integration endpoint --- .../src/pages/api/editing/feaas/render.ts | 28 ++ .../nextjs-xmcloud/src/pages/feaas/render.tsx | 30 ++ .../src/editing/editing-render-middleware.ts | 70 +--- .../editing/feaas-render-middleware.test.ts | 383 ++++++++++++++++++ .../src/editing/feaas-render-middleware.ts | 152 +++++++ .../sitecore-jss-nextjs/src/editing/index.ts | 1 + .../src/editing/render-middleware.test.ts | 90 ++++ .../src/editing/render-middleware.ts | 75 ++++ 8 files changed, 766 insertions(+), 63 deletions(-) create mode 100644 packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts create mode 100644 packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx create mode 100644 packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts create mode 100644 packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts create mode 100644 packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts create mode 100644 packages/sitecore-jss-nextjs/src/editing/render-middleware.ts diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts new file mode 100644 index 0000000000..15fc42bfdc --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts @@ -0,0 +1,28 @@ +import { FEAASRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing'; + +/** + * This Next.js API route is used to handle GET requests from Sitecore Component Builder. + * + * The `FEAASRenderMiddleware` will: + * 1. Enable Next.js Preview Mode + * 2. Invoke the /feaas/render page request, passing along the Preview Mode cookies. + * 3. Return the rendered page HTML to the Sitecore Component Builder: + * - If "feaasSrc" query parameter is provided, the page will render a FEAAS component. + * - The page provides all the registered FEAAS components + */ + +// Bump body size limit (1mb by default) and disable response limit for Sitecore editor payloads +// See https://nextjs.org/docs/api-routes/request-helpers#custom-config +export const config = { + api: { + bodyParser: { + sizeLimit: '2mb', + }, + responseLimit: false, + }, +}; + +// Wire up the FEAASRenderMiddleware handler +const handler = new FEAASRenderMiddleware().getHandler(); + +export default handler; diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx new file mode 100644 index 0000000000..cb45e79f73 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx @@ -0,0 +1,30 @@ +import { GetServerSideProps } from 'next'; +import BYOC from 'src/byoc'; +import * as FEAAS from '@sitecore-feaas/clientside/react'; + +/** + * The FEAASRender page is responsible for: + * - Rendering the FEAAS component if the "feaasSrc" is provided. + * - Rendering all the registered components. + * - The page is rendered only if it's requested by the api route (/api/editing/feaas/render) using the preview mode. + */ +const FEAASRender = ({ feaasSrc }: { feaasSrc: string }): JSX.Element => { + return ( + <> + {/** Render the component if the "feaasSrc" is provided */} + {feaasSrc && } + {/** Render all the registered components */} + + + ); +}; + +export const getServerSideProps: GetServerSideProps = async (context) => { + return { + props: { + feaasSrc: context.query.feaasSrc || null, + }, + }; +}; + +export default FEAASRender; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index d3d3bf5f93..39603d2418 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -1,24 +1,15 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { STATIC_PROPS_ID, SERVER_PROPS_ID } from 'next/constants'; -import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; +import { debug } from '@sitecore-jss/sitecore-jss'; import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/layout'; import { parse } from 'node-html-parser'; import { EditingData } from './editing-data'; import { EditingDataService, editingDataService } from './editing-data-service'; -import { - QUERY_PARAM_EDITING_SECRET, - QUERY_PARAM_PROTECTION_BYPASS_SITECORE, - QUERY_PARAM_PROTECTION_BYPASS_VERCEL, -} from './constants'; +import { QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; +import { RenderMiddlewareBase, RenderMiddlewareBaseConfig } from './render-middleware'; -export interface EditingRenderMiddlewareConfig { - /** - * The `AxiosDataFetcher` instance to use for API requests. - * @default new AxiosDataFetcher() - * @see AxiosDataFetcher - */ - dataFetcher?: AxiosDataFetcher; +export type EditingRenderMiddlewareConfig = RenderMiddlewareBaseConfig & { /** * The `EditingDataService` instance to use. * This would typically only be necessary if you've got a custom `EditingDataService` instance (e.g. using a custom API route). @@ -38,34 +29,23 @@ export interface EditingRenderMiddlewareConfig { * @see resolveServerUrl */ resolvePageUrl?: (serverUrl: string, itemPath: string) => string; - /** - * Function used to determine the root server URL. This is used for the route/page and subsequent data API requests. - * By default, the host header is used, with https protocol on Vercel (due to serverless function architecture) and http protocol elsewhere. - * @param {NextApiRequest} req The current request. - * @default `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; - * @see resolvePageUrl - */ - resolveServerUrl?: (req: NextApiRequest) => string; -} +}; /** * Middleware / handler for use in the editing render Next.js API route (e.g. '/api/editing/render') * which is required for Sitecore editing support. */ -export class EditingRenderMiddleware { +export class EditingRenderMiddleware extends RenderMiddlewareBase { private editingDataService: EditingDataService; - private dataFetcher: AxiosDataFetcher; private resolvePageUrl: (serverUrl: string, itemPath: string) => string; - private resolveServerUrl: (req: NextApiRequest) => string; /** * @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config */ constructor(config?: EditingRenderMiddlewareConfig) { + super(config); this.editingDataService = config?.editingDataService ?? editingDataService; - this.dataFetcher = config?.dataFetcher ?? new AxiosDataFetcher({ debugger: debug.editing }); this.resolvePageUrl = config?.resolvePageUrl ?? this.defaultResolvePageUrl; - this.resolveServerUrl = config?.resolveServerUrl ?? this.defaultResolveServerUrl; } /** @@ -76,28 +56,6 @@ export class EditingRenderMiddleware { return this.handler; } - /** - * Gets query parameters that should be passed along to subsequent requests - * @param {Object} query Object of query parameters from incoming URL - * @returns Object of approved query parameters - */ - protected getQueryParamsForPropagation = ( - query: Partial<{ [key: string]: string | string[] }> - ): { [key: string]: string } => { - const params: { [key: string]: string } = {}; - if (query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE]) { - params[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = query[ - QUERY_PARAM_PROTECTION_BYPASS_SITECORE - ] as string; - } - if (query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL]) { - params[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = query[ - QUERY_PARAM_PROTECTION_BYPASS_VERCEL - ] as string; - } - return params; - }; - private handler = async (req: NextApiRequest, res: NextApiResponse): Promise => { const { method, query, body, headers } = req; @@ -240,20 +198,6 @@ export class EditingRenderMiddleware { private defaultResolvePageUrl = (serverUrl: string, itemPath: string) => { return `${serverUrl}${itemPath}`; }; - - /** - * Default server URL resolution. - * Note we use https protocol on Vercel due to serverless function architecture. - * In all other scenarios, including localhost (with or without a proxy e.g. ngrok) - * and within a nodejs container, http protocol should be used. - * - * For information about the VERCEL environment variable, see - * https://vercel.com/docs/environment-variables#system-environment-variables - * @param {NextApiRequest} req - */ - private defaultResolveServerUrl = (req: NextApiRequest) => { - return `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; - }; } /** diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts new file mode 100644 index 0000000000..475d9648fd --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts @@ -0,0 +1,383 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect, use } from 'chai'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss'; +import { + QUERY_PARAM_EDITING_SECRET, + QUERY_PARAM_PROTECTION_BYPASS_SITECORE, + QUERY_PARAM_PROTECTION_BYPASS_VERCEL, +} from './constants'; +import { FEAASRenderMiddleware } from './feaas-render-middleware'; +import { spy, match } from 'sinon'; +import sinonChai from 'sinon-chai'; + +use(sinonChai); + +const mockNextJsPreviewCookies = [ + '__prerender_bypass:1122334455', + '__next_preview_data:6677889900', +]; + +type Query = { + [key: string]: string; +}; + +const mockRequest = (query?: Query, method?: string, host?: string) => { + return { + body: {}, + method: method ?? 'GET', + query: query ?? {}, + headers: { host: host ?? 'localhost:3000' }, + } as NextApiRequest; +}; + +const mockResponse = () => { + const res = {} as NextApiResponse; + res.status = spy(() => { + return res; + }); + res.json = spy(() => { + return res; + }); + res.send = spy(() => { + return res; + }); + res.end = spy(() => { + return res; + }); + res.getHeader = spy((name: string) => { + return name === 'Set-Cookie' ? mockNextJsPreviewCookies : undefined; + }); + res.setHeader = spy(); + res.setPreviewData = spy(() => { + return res; + }); + return res; +}; + +const mockFetcher = (html?: string) => { + const fetcher = {} as AxiosDataFetcher; + fetcher.get = spy(() => { + return Promise.resolve({ data: html ?? '' }); + }); + return fetcher; +}; + +describe('FEAASRenderMiddleware', () => { + const secret = 'secret1234'; + + beforeEach(() => { + process.env.JSS_EDITING_SECRET = secret; + }); + + after(() => { + delete process.env.JSS_EDITING_SECRET; + }); + + it('should handle request', async () => { + const html = 'Something amazing'; + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + + const fetcher = mockFetcher(html); + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({}); + expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie'); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith( + match('http://localhost:3000/feaas/render?timestamp'), + { + headers: { + Cookie: mockNextJsPreviewCookies.join(';'), + }, + } + ); + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(200); + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith( + 'Something amazing' + ); + }); + + it('should handle request when feaasSrc query parameter is present', async () => { + const html = 'Something amazing'; + const query = { + feaasSrc: 'https://feaas.blob.core.windows.net/components/xxx/xyz/responsive/staged', + } as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + + const fetcher = mockFetcher(html); + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({}); + expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie'); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith( + match( + 'http://localhost:3000/feaas/render?feaasSrc=https%3A%2F%2Ffeaas.blob.core.windows.net%2Fcomponents%2Fxxx%2Fxyz%2Fresponsive%2Fstaged×tamp' + ), + { + headers: { + Cookie: mockNextJsPreviewCookies.join(';'), + }, + } + ); + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(200); + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith( + 'Something amazing' + ); + }); + + it('should throw error for page render request', async () => { + const html = 'Error occured'; + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const previewData = {}; + + const fetcher = {} as AxiosDataFetcher; + fetcher.get = spy(() => { + return Promise.reject({ response: { data: html, status: 500 } }); + }); + + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData); + expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie'); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith( + match('http://localhost:3000/feaas/render?timestamp'), + { + headers: { + Cookie: mockNextJsPreviewCookies.join(';'), + }, + } + ); + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(500); + expect(res.json).to.have.been.calledOnce; + }); + + it('should respondWith 405 for unsupported method', async () => { + const fetcher = mockFetcher(); + const req = mockRequest({}, 'POST'); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.setHeader).to.have.been.calledWithExactly('Allow', 'GET'); + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(405); + expect(res.json).to.have.been.calledOnce; + }); + + it('should respond with 401 for missing secret', async () => { + const fetcher = mockFetcher(); + const query = {} as Query; + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(401); + expect(res.json).to.have.been.calledOnce; + }); + + it('should respond with 401 for invalid secret', async () => { + const fetcher = mockFetcher(); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = 'nope'; + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(401); + expect(res.json).to.have.been.calledOnce; + }); + + it('should use host header with http for serverUrl', async () => { + const html = 'Something amazing'; + const fetcher = mockFetcher(html); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(query, undefined, 'testhostheader.com'); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(fetcher.get).to.have.been.calledWithMatch('http://testhostheader.com'); + }); + + it('should use https for serverUrl on Vercel', async () => { + const html = 'Something amazing'; + const fetcher = mockFetcher(html); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(query, undefined, 'vercel.com'); + const res = mockResponse(); + process.env.VERCEL = '1'; + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(fetcher.get).to.have.been.calledWithMatch('https://vercel.com'); + + delete process.env.VERCEL; + }); + + it('should use custom resolveServerUrl', async () => { + const html = 'Something amazing'; + const fetcher = mockFetcher(html); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(query); + const res = mockResponse(); + + const serverUrl = 'https://test.com'; + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + resolveServerUrl: () => { + return serverUrl; + }, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(fetcher.get).to.have.been.calledWithMatch(serverUrl); + }); + + it('should use custom resolvePageUrl', async () => { + const html = 'Something amazing'; + const fetcher = mockFetcher(html); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(query); + const res = mockResponse(); + + const serverUrl = 'https://test.com'; + const expectedPageUrl = `${serverUrl}/some/path/feaas/render`; + const resolvePageUrl = spy((serverUrl: string) => { + return `${serverUrl}/some/path/feaas/render`; + }); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + resolvePageUrl: resolvePageUrl, + resolveServerUrl: () => { + return serverUrl; + }, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(resolvePageUrl).to.have.been.calledOnce; + expect(resolvePageUrl).to.have.been.calledWith(serverUrl); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get).to.have.been.calledWithMatch(expectedPageUrl); + }); + + it('should respondWith 500 if rendered html empty', async () => { + const fetcher = mockFetcher(''); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(500); + expect(res.json).to.have.been.calledOnce; + }); + + it('should pass along protection bypass query parameters', async () => { + const html = 'Something amazing'; + const query = {} as Query; + const bypassTokenSitecore = 'token1234Sitecore'; + const bypassTokenVercel = 'token1234Vercel'; + query[QUERY_PARAM_EDITING_SECRET] = secret; + query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = bypassTokenSitecore; + query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = bypassTokenVercel; + const previewData = {}; + + const fetcher = mockFetcher(html); + const req = mockRequest(query); + const res = mockResponse(); + + const middleware = new FEAASRenderMiddleware({ + dataFetcher: fetcher, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get, 'pass along protection bypass query params').to.have.been.calledWithMatch( + `http://localhost:3000/feaas/render?${QUERY_PARAM_PROTECTION_BYPASS_SITECORE}=${bypassTokenSitecore}&${QUERY_PARAM_PROTECTION_BYPASS_VERCEL}=${bypassTokenVercel}×tamp` + ); + }); +}); diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts new file mode 100644 index 0000000000..76b591b60c --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts @@ -0,0 +1,152 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { debug } from '@sitecore-jss/sitecore-jss'; +import { QUERY_PARAM_EDITING_SECRET } from './constants'; +import { getJssEditingSecret } from '../utils/utils'; +import { RenderMiddlewareBase, RenderMiddlewareBaseConfig } from './render-middleware'; + +/** + * Configuration for `FEAASRenderMiddleware`. + */ +export type FEAASRenderMiddlewareConfig = RenderMiddlewareBaseConfig & { + /** + * Function used to determine FEAAS page URL to render. + * This may be necessary for certain custom Next.js routing configurations. + * @param {string} serverUrl The root server URL e.g. 'http://localhost:3000' + * @returns {string} The URL to render + * @default `${serverUrl}/feaas/render` + * @see resolveServerUrl + */ + resolvePageUrl?: (serverUrl: string) => string; +}; + +/** + * Middleware / handler for use in the feaas render Next.js API route (e.g. '/api/editing/feaas/render') + * which is required for Sitecore editing support. + */ +export class FEAASRenderMiddleware extends RenderMiddlewareBase { + private resolvePageUrl: (serverUrl: string) => string; + + /** + * @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config + */ + constructor(protected config?: FEAASRenderMiddlewareConfig) { + super(config); + + this.resolvePageUrl = config?.resolvePageUrl ?? this.defaultResolvePageUrl; + } + + /** + * Gets the Next.js API route handler + * @returns route handler + */ + public getHandler(): (req: NextApiRequest, res: NextApiResponse) => Promise { + return this.handler; + } + + private handler = async (req: NextApiRequest, res: NextApiResponse): Promise => { + const { method, query, body, headers } = req; + + const startTimestamp = Date.now(); + + debug.editing('feaas render middleware start: %o', { + method, + query, + headers, + body, + }); + + if (method !== 'GET') { + debug.editing('invalid method - sent %s expected GET', method); + res.setHeader('Allow', 'GET'); + return res.status(405).json({ + html: `Invalid request method '${method}'`, + }); + } + + // Validate secret + const secret = query[QUERY_PARAM_EDITING_SECRET]; + if (secret !== getJssEditingSecret()) { + debug.editing( + 'invalid editing secret - sent "%s" expected "%s"', + secret, + getJssEditingSecret() + ); + return res.status(401).json({ + html: 'Missing or invalid secret', + }); + } + + try { + // Resolve server URL + const serverUrl = this.resolveServerUrl(req); + + // Get query string parameters to propagate on subsequent requests (e.g. for deployment protection bypass) + const params = this.getQueryParamsForPropagation(query); + + // Enable Next.js Preview Mode + res.setPreviewData({}); + + // Grab the Next.js preview cookies to send on to the render request + const cookies = res.getHeader('Set-Cookie') as string[]; + + // Make actual render request for page route, passing on preview cookies as well as any approved query string parameters. + // Note timestamp effectively disables caching the request in Axios (no amount of cache headers seemed to do it) + const requestUrl = new URL(this.resolvePageUrl(serverUrl)); + + for (const key in params) { + if ({}.hasOwnProperty.call(params, key)) { + requestUrl.searchParams.append(key, params[key]); + } + } + + // Pass "feaasSrc" in case a FEAASComponent is being requested + if (query.feaasSrc) { + requestUrl.searchParams.append('feaasSrc', query.feaasSrc as string); + } + + requestUrl.searchParams.append('timestamp', Date.now().toString()); + + debug.editing('fetching page route for %s', requestUrl.toString()); + + const pageRes = await this.dataFetcher.get(requestUrl.toString(), { + headers: { + Cookie: cookies.join(';'), + }, + }); + + const html = pageRes.data; + + if (!html || html.length === 0) { + throw new Error('Failed to render html'); + } + + // Return expected JSON result + debug.editing('feaas render middleware end in %dms: %o', Date.now() - startTimestamp, { + status: 200, + html, + }); + res.status(200).send(html); + } catch (err) { + const error = err as Record; + + if (error.response || error.request) { + // Axios error, which could mean the server or page URL isn't quite right, so provide a more helpful hint + console.info( + // eslint-disable-next-line quotes + "Hint: for non-standard server or Next.js route configurations, you may need to override the 'resolveServerUrl' or 'resolvePageUrl' available on the 'FEAASRenderMiddleware' config." + ); + } + res.status(500).json({ + html: `${error}`, + }); + } + }; + + /** + * Default FEAAS page URL resolution. + * @param {string} serverUrl The root server URL e.g. 'http://localhost:3000' + */ + private defaultResolvePageUrl = (serverUrl: string) => { + return `${serverUrl}/feaas/render`; + }; +} diff --git a/packages/sitecore-jss-nextjs/src/editing/index.ts b/packages/sitecore-jss-nextjs/src/editing/index.ts index f6ab628cf3..a573ab8347 100644 --- a/packages/sitecore-jss-nextjs/src/editing/index.ts +++ b/packages/sitecore-jss-nextjs/src/editing/index.ts @@ -15,3 +15,4 @@ export { editingDataService, } from './editing-data-service'; export { VercelEditingDataCache } from './vercel-editing-data-cache'; +export { FEAASRenderMiddleware, FEAASRenderMiddlewareConfig } from './feaas-render-middleware'; diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts new file mode 100644 index 0000000000..404c9e8639 --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts @@ -0,0 +1,90 @@ +/* eslint-disable dot-notation */ +import chai, { use } from 'chai'; +import sinonChai from 'sinon-chai'; +import chaiString from 'chai-string'; +import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss'; +import { RenderMiddlewareBase } from './render-middleware'; +import { NextApiRequest } from 'next'; +import { + QUERY_PARAM_EDITING_SECRET, + QUERY_PARAM_PROTECTION_BYPASS_SITECORE, + QUERY_PARAM_PROTECTION_BYPASS_VERCEL, +} from './constants'; + +use(sinonChai); +const expect = chai.use(chaiString).expect; + +type Query = { + [key: string]: string; +}; + +describe('RenderMiddlewareBase', () => { + class SampleMiddleware extends RenderMiddlewareBase {} + + const mockFetcher = () => ({} as AxiosDataFetcher); + + describe('defaultResolveServerUrl', () => { + it('should use https protocol on Vercel', async () => { + process.env.VERCEL = '1'; + + const middleware = new SampleMiddleware(); + + const url = middleware['defaultResolveServerUrl']({ + headers: { host: 'localhost' }, + } as NextApiRequest); + + expect(url).to.equal('https://localhost'); + + delete process.env.VERCEL; + }); + + it('should use http protocol', async () => { + const middleware = new SampleMiddleware(); + + const url = middleware['defaultResolveServerUrl']({ + headers: { host: 'localhost' }, + } as NextApiRequest); + + expect(url).to.equal('http://localhost'); + }); + }); + + it('should use custom resolveServerUrl', async () => { + const serverUrl = 'https://test.com'; + + const middleware = new SampleMiddleware({ + resolveServerUrl: () => serverUrl, + }); + + expect(middleware['resolveServerUrl']({} as NextApiRequest)).to.equal(serverUrl); + }); + + it('should use custom dataFetcher', () => { + const dataFetcher = mockFetcher(); + + const middleware = new SampleMiddleware({ + dataFetcher, + }); + + expect(middleware['dataFetcher']).to.equal(dataFetcher); + }); + + describe('getQueryParamsForPropagation', () => { + it('should construct query params for protection bypass', () => { + const middleware = new SampleMiddleware(); + + const secret = 'secret1234'; + const query = {} as Query; + const bypassTokenSitecore = 'token1234Sitecore'; + const bypassTokenVercel = 'token1234Vercel'; + query[QUERY_PARAM_EDITING_SECRET] = secret; + query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = bypassTokenSitecore; + query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = bypassTokenVercel; + + expect(middleware['getQueryParamsForPropagation'](query)).to.deep.equal({ + [QUERY_PARAM_PROTECTION_BYPASS_SITECORE]: bypassTokenSitecore, + [QUERY_PARAM_PROTECTION_BYPASS_VERCEL]: bypassTokenVercel, + }); + }); + }); +}); diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts new file mode 100644 index 0000000000..8f9205bd4b --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts @@ -0,0 +1,75 @@ +import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; +import { NextApiRequest } from 'next'; +import { + QUERY_PARAM_PROTECTION_BYPASS_SITECORE, + QUERY_PARAM_PROTECTION_BYPASS_VERCEL, +} from './constants'; + +/** + * Configuration for `RenderMiddlewareBase`. + */ +export type RenderMiddlewareBaseConfig = { + /** + * The `AxiosDataFetcher` instance to use for API requests. + * @default new AxiosDataFetcher() + * @see AxiosDataFetcher + */ + dataFetcher?: AxiosDataFetcher; + /** + * Function used to determine the root server URL. This is used for the route/page and subsequent data API requests. + * By default, the host header is used, with https protocol on Vercel (due to serverless function architecture) and http protocol elsewhere. + * @param {NextApiRequest} req The current request. + * @default `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; + * @see resolvePageUrl + */ + resolveServerUrl?: (req: NextApiRequest) => string; +}; + +/** + * Base class for middleware that handles pages and components rendering in Sitecore Editors. + */ +export abstract class RenderMiddlewareBase { + protected dataFetcher: AxiosDataFetcher; + protected resolveServerUrl: (req: NextApiRequest) => string; + + constructor(protected config?: RenderMiddlewareBaseConfig) { + this.dataFetcher = config?.dataFetcher ?? new AxiosDataFetcher({ debugger: debug.editing }); + this.resolveServerUrl = config?.resolveServerUrl ?? this.defaultResolveServerUrl; + } + + /** + * Gets query parameters that should be passed along to subsequent requests + * @param {Object} query Object of query parameters from incoming URL + * @returns Object of approved query parameters + */ + protected getQueryParamsForPropagation = ( + query: Partial<{ [key: string]: string | string[] }> + ): { [key: string]: string } => { + const params: { [key: string]: string } = {}; + if (query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE]) { + params[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = query[ + QUERY_PARAM_PROTECTION_BYPASS_SITECORE + ] as string; + } + if (query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL]) { + params[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = query[ + QUERY_PARAM_PROTECTION_BYPASS_VERCEL + ] as string; + } + return params; + }; + + /** + * Default server URL resolution. + * Note we use https protocol on Vercel due to serverless function architecture. + * In all other scenarios, including localhost (with or without a proxy e.g. ngrok) + * and within a nodejs container, http protocol should be used. + * + * For information about the VERCEL environment variable, see + * https://vercel.com/docs/environment-variables#system-environment-variables + * @param {NextApiRequest} req + */ + private defaultResolveServerUrl = (req: NextApiRequest) => { + return `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; + }; +} From 424bd979b64bbedb431bfcf394edb3800d23b141 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 7 Feb 2024 17:47:01 +0200 Subject: [PATCH 02/10] Update --- .../src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx index cb45e79f73..2f3e2e30ff 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/feaas/render.tsx @@ -24,6 +24,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => { props: { feaasSrc: context.query.feaasSrc || null, }, + // Don't show the page if it's not requested by the api route using the preview mode + notFound: !context.preview, }; }; From befcaf30a66b0da44047d4849dd3ca0da70e525f Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 7 Feb 2024 17:50:07 +0200 Subject: [PATCH 03/10] CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 261de87721..3c072f2e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ Our versioning strategy is as follows: * `[nextjs/template]` `[sitecore-jss-nextjs]` On-demand ISR [#1674](https://github.com/Sitecore/jss/pull/1674)) * `[sitecore-jss]` `[templates/nextjs-xmcloud]` Load the content styles for the RichText component ([#1670](https://github.com/Sitecore/jss/pull/1670))([#1683](https://github.com/Sitecore/jss/pull/1683)) ([#1684](https://github.com/Sitecore/jss/pull/1684)) ([#1693](https://github.com/Sitecore/jss/pull/1693)) * `[templates/react]` `[sitecore-jss-react]` Replace package 'deep-equal' with 'fast-deep-equal'. No functionality change only performance improvement ([#1719](https://github.com/Sitecore/jss/pull/1719)) ([#1665](https://github.com/Sitecore/jss/pull/1665)) -* `[templates/nextjs-xmcloud]` `[sitecore-jss]` `[sitecore-jss-nextjs]` `[sitecore-jss-react]` Add support for loading appropriate stylesheets whenever a theme is applied to BYOC and SXA components by introducing new function getComponentLibraryStylesheetLinks, which replaces getFEAASLibraryStylesheetLinks (which has been marked as deprecated) ([#1722](https://github.com/Sitecore/jss/pull/1722)) +* `[templates/nextjs-xmcloud]` `[sitecore-jss]` `[sitecore-jss-nextjs]` `[sitecore-jss-react]` Add support for loading appropriate stylesheets whenever a theme is applied to BYOC and SXA components by introducing new function getComponentLibraryStylesheetLinks, which replaces getFEAASLibraryStylesheetLinks (which has been marked as deprecated) ([#1722](https://github.com/Sitecore/jss/pull/1722)) +* `[sitecore-jss-nextjs]` `[templates/nextjs]` [BYOC] Component Builder integration endpoint ([#1729](https://github.com/Sitecore/jss/pull/1729)) ### 🐛 Bug Fixes From fee069a8058255cec0febedb815761a5eb86c01b Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 7 Feb 2024 18:58:46 +0200 Subject: [PATCH 04/10] Updated NextConfig --- .../src/lib/next-config/plugins/feaas.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js index d79aeadee4..a2376f7f34 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js @@ -3,6 +3,19 @@ */ const feaasPlugin = (nextConfig = {}) => { return Object.assign({}, nextConfig, { + // Enable FEAAS Nextjs Image integration + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + { + protocol: 'http', + hostname: '**', + }, + ], + }, webpack: (config, options) => { if (options.isServer) { // Force use of CommonJS on the server for FEAAS SDK since JSS also uses CommonJS entrypoint to FEAAS SDK. From 3dd17432780c980c14893b3f15f82a5ab6e21fdf Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 8 Feb 2024 01:41:56 +0200 Subject: [PATCH 05/10] Addressing the pr review comments --- .../src/lib/next-config/plugins/feaas.js | 3 +- .../src/pages/api/editing/feaas/render.ts | 18 +- .../src/editing/editing-render-middleware.ts | 43 ++- .../editing/feaas-render-middleware.test.ts | 252 +++--------------- .../src/editing/feaas-render-middleware.ts | 94 ++----- .../src/editing/render-middleware.test.ts | 50 ---- .../src/editing/render-middleware.ts | 46 +--- 7 files changed, 114 insertions(+), 392 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js index a2376f7f34..eb5ed87004 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js @@ -3,9 +3,10 @@ */ const feaasPlugin = (nextConfig = {}) => { return Object.assign({}, nextConfig, { - // Enable FEAAS Nextjs Image integration + // Enable FEAAS Nextjs Image integration by enabling the remotePatterns option. images: { remotePatterns: [ + ...(nextConfig.images?.remotePatterns || []), { protocol: 'https', hostname: '**', diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts index 15fc42bfdc..3830d592cb 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/pages/api/editing/feaas/render.ts @@ -4,24 +4,12 @@ import { FEAASRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing * This Next.js API route is used to handle GET requests from Sitecore Component Builder. * * The `FEAASRenderMiddleware` will: - * 1. Enable Next.js Preview Mode - * 2. Invoke the /feaas/render page request, passing along the Preview Mode cookies. - * 3. Return the rendered page HTML to the Sitecore Component Builder: + * 1. Enable Next.js Preview Mode. + * 2. Redirect the request to the /feaas/render page. * - If "feaasSrc" query parameter is provided, the page will render a FEAAS component. - * - The page provides all the registered FEAAS components + * - The page provides all the registered FEAAS components. */ -// Bump body size limit (1mb by default) and disable response limit for Sitecore editor payloads -// See https://nextjs.org/docs/api-routes/request-helpers#custom-config -export const config = { - api: { - bodyParser: { - sizeLimit: '2mb', - }, - responseLimit: false, - }, -}; - // Wire up the FEAASRenderMiddleware handler const handler = new FEAASRenderMiddleware().getHandler(); diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index 39603d2418..789121278f 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -1,15 +1,21 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { STATIC_PROPS_ID, SERVER_PROPS_ID } from 'next/constants'; -import { debug } from '@sitecore-jss/sitecore-jss'; +import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/layout'; import { parse } from 'node-html-parser'; import { EditingData } from './editing-data'; import { EditingDataService, editingDataService } from './editing-data-service'; import { QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; -import { RenderMiddlewareBase, RenderMiddlewareBaseConfig } from './render-middleware'; +import { RenderMiddlewareBase } from './render-middleware'; -export type EditingRenderMiddlewareConfig = RenderMiddlewareBaseConfig & { +export interface EditingRenderMiddlewareConfig { + /** + * The `AxiosDataFetcher` instance to use for API requests. + * @default new AxiosDataFetcher() + * @see AxiosDataFetcher + */ + dataFetcher?: AxiosDataFetcher; /** * The `EditingDataService` instance to use. * This would typically only be necessary if you've got a custom `EditingDataService` instance (e.g. using a custom API route). @@ -29,7 +35,15 @@ export type EditingRenderMiddlewareConfig = RenderMiddlewareBaseConfig & { * @see resolveServerUrl */ resolvePageUrl?: (serverUrl: string, itemPath: string) => string; -}; + /** + * Function used to determine the root server URL. This is used for the route/page and subsequent data API requests. + * By default, the host header is used, with https protocol on Vercel (due to serverless function architecture) and http protocol elsewhere. + * @param {NextApiRequest} req The current request. + * @default `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; + * @see resolvePageUrl + */ + resolveServerUrl?: (req: NextApiRequest) => string; +} /** * Middleware / handler for use in the editing render Next.js API route (e.g. '/api/editing/render') @@ -37,15 +51,20 @@ export type EditingRenderMiddlewareConfig = RenderMiddlewareBaseConfig & { */ export class EditingRenderMiddleware extends RenderMiddlewareBase { private editingDataService: EditingDataService; + private dataFetcher: AxiosDataFetcher; private resolvePageUrl: (serverUrl: string, itemPath: string) => string; + private resolveServerUrl: (req: NextApiRequest) => string; /** * @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config */ constructor(config?: EditingRenderMiddlewareConfig) { - super(config); + super(); + this.editingDataService = config?.editingDataService ?? editingDataService; + this.dataFetcher = config?.dataFetcher ?? new AxiosDataFetcher({ debugger: debug.editing }); this.resolvePageUrl = config?.resolvePageUrl ?? this.defaultResolvePageUrl; + this.resolveServerUrl = config?.resolveServerUrl ?? this.defaultResolveServerUrl; } /** @@ -198,6 +217,20 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase { private defaultResolvePageUrl = (serverUrl: string, itemPath: string) => { return `${serverUrl}${itemPath}`; }; + + /** + * Default server URL resolution. + * Note we use https protocol on Vercel due to serverless function architecture. + * In all other scenarios, including localhost (with or without a proxy e.g. ngrok) + * and within a nodejs container, http protocol should be used. + * + * For information about the VERCEL environment variable, see + * https://vercel.com/docs/environment-variables#system-environment-variables + * @param {NextApiRequest} req + */ + private defaultResolveServerUrl = (req: NextApiRequest) => { + return `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; + }; } /** diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts index 475d9648fd..8723c501b2 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts @@ -1,24 +1,19 @@ +/* eslint-disable quotes */ /* eslint-disable no-unused-expressions */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { expect, use } from 'chai'; import { NextApiRequest, NextApiResponse } from 'next'; -import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss'; import { QUERY_PARAM_EDITING_SECRET, QUERY_PARAM_PROTECTION_BYPASS_SITECORE, QUERY_PARAM_PROTECTION_BYPASS_VERCEL, } from './constants'; import { FEAASRenderMiddleware } from './feaas-render-middleware'; -import { spy, match } from 'sinon'; +import { spy } from 'sinon'; import sinonChai from 'sinon-chai'; use(sinonChai); -const mockNextJsPreviewCookies = [ - '__prerender_bypass:1122334455', - '__next_preview_data:6677889900', -]; - type Query = { [key: string]: string; }; @@ -37,31 +32,23 @@ const mockResponse = () => { res.status = spy(() => { return res; }); - res.json = spy(() => { - return res; - }); res.send = spy(() => { return res; }); res.end = spy(() => { return res; }); - res.getHeader = spy((name: string) => { - return name === 'Set-Cookie' ? mockNextJsPreviewCookies : undefined; - }); - res.setHeader = spy(); res.setPreviewData = spy(() => { return res; }); - return res; -}; - -const mockFetcher = (html?: string) => { - const fetcher = {} as AxiosDataFetcher; - fetcher.get = spy(() => { - return Promise.resolve({ data: html ?? '' }); + res.setHeader = spy(() => { + return res; + }); + res.redirect = spy(() => { + return res; }); - return fetcher; + + return res; }; describe('FEAASRenderMiddleware', () => { @@ -76,124 +63,71 @@ describe('FEAASRenderMiddleware', () => { }); it('should handle request', async () => { - const html = 'Something amazing'; const query = {} as Query; query[QUERY_PARAM_EDITING_SECRET] = secret; - const fetcher = mockFetcher(html); const req = mockRequest(query); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({}); - expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie'); - expect(fetcher.get).to.have.been.calledOnce; - expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith( - match('http://localhost:3000/feaas/render?timestamp'), - { - headers: { - Cookie: mockNextJsPreviewCookies.join(';'), - }, - } - ); - expect(res.status).to.have.been.calledOnce; - expect(res.status).to.have.been.calledWith(200); - expect(res.send).to.have.been.calledOnce; - expect(res.send).to.have.been.calledWith( - 'Something amazing' - ); + expect(res.redirect).to.have.been.calledOnce; + expect(res.redirect).to.have.been.calledWith('/feaas/render'); }); it('should handle request when feaasSrc query parameter is present', async () => { - const html = 'Something amazing'; const query = { feaasSrc: 'https://feaas.blob.core.windows.net/components/xxx/xyz/responsive/staged', } as Query; query[QUERY_PARAM_EDITING_SECRET] = secret; - const fetcher = mockFetcher(html); const req = mockRequest(query); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({}); - expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie'); - expect(fetcher.get).to.have.been.calledOnce; - expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith( - match( - 'http://localhost:3000/feaas/render?feaasSrc=https%3A%2F%2Ffeaas.blob.core.windows.net%2Fcomponents%2Fxxx%2Fxyz%2Fresponsive%2Fstaged×tamp' - ), - { - headers: { - Cookie: mockNextJsPreviewCookies.join(';'), - }, - } - ); - expect(res.status).to.have.been.calledOnce; - expect(res.status).to.have.been.calledWith(200); - expect(res.send).to.have.been.calledOnce; - expect(res.send).to.have.been.calledWith( - 'Something amazing' + expect(res.redirect).to.have.been.calledOnce; + expect(res.redirect).to.have.been.calledWith( + '/feaas/render?feaasSrc=https%3A%2F%2Ffeaas.blob.core.windows.net%2Fcomponents%2Fxxx%2Fxyz%2Fresponsive%2Fstaged' ); }); - it('should throw error for page render request', async () => { - const html = 'Error occured'; + it('should throw error', async () => { const query = {} as Query; query[QUERY_PARAM_EDITING_SECRET] = secret; - const previewData = {}; - - const fetcher = {} as AxiosDataFetcher; - fetcher.get = spy(() => { - return Promise.reject({ response: { data: html, status: 500 } }); - }); const req = mockRequest(query); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, + res.setPreviewData = spy(() => { + throw new Error('Test Error'); }); + + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); - expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData); - expect(res.getHeader, 'get preview cookies').to.have.been.calledWith('Set-Cookie'); - expect(fetcher.get).to.have.been.calledOnce; - expect(fetcher.get, 'pass along preview cookies').to.have.been.calledWith( - match('http://localhost:3000/feaas/render?timestamp'), - { - headers: { - Cookie: mockNextJsPreviewCookies.join(';'), - }, - } - ); + expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith({}); expect(res.status).to.have.been.calledOnce; expect(res.status).to.have.been.calledWith(500); - expect(res.json).to.have.been.calledOnce; + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith('Error: Test Error'); }); it('should respondWith 405 for unsupported method', async () => { - const fetcher = mockFetcher(); const req = mockRequest({}, 'POST'); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); @@ -201,160 +135,65 @@ describe('FEAASRenderMiddleware', () => { expect(res.setHeader).to.have.been.calledWithExactly('Allow', 'GET'); expect(res.status).to.have.been.calledOnce; expect(res.status).to.have.been.calledWith(405); - expect(res.json).to.have.been.calledOnce; + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith( + "Invalid request method 'POST'" + ); }); it('should respond with 401 for missing secret', async () => { - const fetcher = mockFetcher(); const query = {} as Query; const req = mockRequest(query); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); expect(res.status).to.have.been.calledOnce; expect(res.status).to.have.been.calledWith(401); - expect(res.json).to.have.been.calledOnce; + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith('Missing or invalid secret'); }); it('should respond with 401 for invalid secret', async () => { - const fetcher = mockFetcher(); const query = {} as Query; query[QUERY_PARAM_EDITING_SECRET] = 'nope'; const req = mockRequest(query); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); expect(res.status).to.have.been.calledOnce; expect(res.status).to.have.been.calledWith(401); - expect(res.json).to.have.been.calledOnce; - }); - - it('should use host header with http for serverUrl', async () => { - const html = 'Something amazing'; - const fetcher = mockFetcher(html); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(query, undefined, 'testhostheader.com'); - const res = mockResponse(); - - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); - const handler = middleware.getHandler(); - - await handler(req, res); - - expect(fetcher.get).to.have.been.calledWithMatch('http://testhostheader.com'); - }); - - it('should use https for serverUrl on Vercel', async () => { - const html = 'Something amazing'; - const fetcher = mockFetcher(html); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(query, undefined, 'vercel.com'); - const res = mockResponse(); - process.env.VERCEL = '1'; - - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); - const handler = middleware.getHandler(); - - await handler(req, res); - - expect(fetcher.get).to.have.been.calledWithMatch('https://vercel.com'); - - delete process.env.VERCEL; - }); - - it('should use custom resolveServerUrl', async () => { - const html = 'Something amazing'; - const fetcher = mockFetcher(html); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(query); - const res = mockResponse(); - - const serverUrl = 'https://test.com'; - - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - resolveServerUrl: () => { - return serverUrl; - }, - }); - const handler = middleware.getHandler(); - - await handler(req, res); - - expect(fetcher.get).to.have.been.calledWithMatch(serverUrl); + expect(res.send).to.have.been.calledOnce; + expect(res.send).to.have.been.calledWith('Missing or invalid secret'); }); - it('should use custom resolvePageUrl', async () => { - const html = 'Something amazing'; - const fetcher = mockFetcher(html); + it('should use custom pageUrl', async () => { const query = {} as Query; query[QUERY_PARAM_EDITING_SECRET] = secret; const req = mockRequest(query); const res = mockResponse(); - const serverUrl = 'https://test.com'; - const expectedPageUrl = `${serverUrl}/some/path/feaas/render`; - const resolvePageUrl = spy((serverUrl: string) => { - return `${serverUrl}/some/path/feaas/render`; - }); + const pageUrl = '/some/path/feaas/render'; const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - resolvePageUrl: resolvePageUrl, - resolveServerUrl: () => { - return serverUrl; - }, + pageUrl, }); const handler = middleware.getHandler(); await handler(req, res); - expect(resolvePageUrl).to.have.been.calledOnce; - expect(resolvePageUrl).to.have.been.calledWith(serverUrl); - expect(fetcher.get).to.have.been.calledOnce; - expect(fetcher.get).to.have.been.calledWithMatch(expectedPageUrl); - }); - - it('should respondWith 500 if rendered html empty', async () => { - const fetcher = mockFetcher(''); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(query); - const res = mockResponse(); - - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); - const handler = middleware.getHandler(); - - await handler(req, res); - - expect(res.status).to.have.been.calledOnce; - expect(res.status).to.have.been.calledWith(500); - expect(res.json).to.have.been.calledOnce; + expect(res.redirect).to.have.been.calledOnce; + expect(res.redirect).to.have.been.calledWith(pageUrl); }); it('should pass along protection bypass query parameters', async () => { - const html = 'Something amazing'; const query = {} as Query; const bypassTokenSitecore = 'token1234Sitecore'; const bypassTokenVercel = 'token1234Vercel'; @@ -363,21 +202,18 @@ describe('FEAASRenderMiddleware', () => { query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = bypassTokenVercel; const previewData = {}; - const fetcher = mockFetcher(html); const req = mockRequest(query); const res = mockResponse(); - const middleware = new FEAASRenderMiddleware({ - dataFetcher: fetcher, - }); + const middleware = new FEAASRenderMiddleware(); const handler = middleware.getHandler(); await handler(req, res); expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData); - expect(fetcher.get).to.have.been.calledOnce; - expect(fetcher.get, 'pass along protection bypass query params').to.have.been.calledWithMatch( - `http://localhost:3000/feaas/render?${QUERY_PARAM_PROTECTION_BYPASS_SITECORE}=${bypassTokenSitecore}&${QUERY_PARAM_PROTECTION_BYPASS_VERCEL}=${bypassTokenVercel}×tamp` + expect(res.redirect).to.have.been.calledOnce; + expect(res.redirect).to.have.been.calledWith( + '/feaas/render?x-sitecore-protection-bypass=token1234Sitecore&x-vercel-protection-bypass=token1234Vercel' ); }); }); diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts index 76b591b60c..450e70700d 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts @@ -2,37 +2,34 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { debug } from '@sitecore-jss/sitecore-jss'; import { QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; -import { RenderMiddlewareBase, RenderMiddlewareBaseConfig } from './render-middleware'; +import { RenderMiddlewareBase } from './render-middleware'; /** * Configuration for `FEAASRenderMiddleware`. */ -export type FEAASRenderMiddlewareConfig = RenderMiddlewareBaseConfig & { +export interface FEAASRenderMiddlewareConfig { /** - * Function used to determine FEAAS page URL to render. + * Defines FEAAS page route to render. * This may be necessary for certain custom Next.js routing configurations. - * @param {string} serverUrl The root server URL e.g. 'http://localhost:3000' - * @returns {string} The URL to render - * @default `${serverUrl}/feaas/render` - * @see resolveServerUrl */ - resolvePageUrl?: (serverUrl: string) => string; -}; + pageUrl?: string; +} /** * Middleware / handler for use in the feaas render Next.js API route (e.g. '/api/editing/feaas/render') * which is required for Sitecore editing support. */ export class FEAASRenderMiddleware extends RenderMiddlewareBase { - private resolvePageUrl: (serverUrl: string) => string; + private pageUrl: string; + private defaultPageUrl = '/feaas/render'; /** * @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config */ constructor(protected config?: FEAASRenderMiddlewareConfig) { - super(config); + super(); - this.resolvePageUrl = config?.resolvePageUrl ?? this.defaultResolvePageUrl; + this.pageUrl = config?.pageUrl ?? this.defaultPageUrl; } /** @@ -44,7 +41,7 @@ export class FEAASRenderMiddleware extends RenderMiddlewareBase { } private handler = async (req: NextApiRequest, res: NextApiResponse): Promise => { - const { method, query, body, headers } = req; + const { method, query, headers } = req; const startTimestamp = Date.now(); @@ -52,15 +49,12 @@ export class FEAASRenderMiddleware extends RenderMiddlewareBase { method, query, headers, - body, }); if (method !== 'GET') { debug.editing('invalid method - sent %s expected GET', method); res.setHeader('Allow', 'GET'); - return res.status(405).json({ - html: `Invalid request method '${method}'`, - }); + return res.status(405).send(`Invalid request method '${method}'`); } // Validate secret @@ -71,82 +65,46 @@ export class FEAASRenderMiddleware extends RenderMiddlewareBase { secret, getJssEditingSecret() ); - return res.status(401).json({ - html: 'Missing or invalid secret', - }); + return res.status(401).send('Missing or invalid secret'); } try { - // Resolve server URL - const serverUrl = this.resolveServerUrl(req); - // Get query string parameters to propagate on subsequent requests (e.g. for deployment protection bypass) const params = this.getQueryParamsForPropagation(query); // Enable Next.js Preview Mode res.setPreviewData({}); - // Grab the Next.js preview cookies to send on to the render request - const cookies = res.getHeader('Set-Cookie') as string[]; - - // Make actual render request for page route, passing on preview cookies as well as any approved query string parameters. - // Note timestamp effectively disables caching the request in Axios (no amount of cache headers seemed to do it) - const requestUrl = new URL(this.resolvePageUrl(serverUrl)); + const queryParams = new URLSearchParams(); for (const key in params) { if ({}.hasOwnProperty.call(params, key)) { - requestUrl.searchParams.append(key, params[key]); + queryParams.append(key, params[key]); } } // Pass "feaasSrc" in case a FEAASComponent is being requested if (query.feaasSrc) { - requestUrl.searchParams.append('feaasSrc', query.feaasSrc as string); + queryParams.append('feaasSrc', query.feaasSrc as string); } - requestUrl.searchParams.append('timestamp', Date.now().toString()); - - debug.editing('fetching page route for %s', requestUrl.toString()); + const redirectUrl = + this.pageUrl + (queryParams.toString() ? `?${queryParams.toString()}` : ''); - const pageRes = await this.dataFetcher.get(requestUrl.toString(), { - headers: { - Cookie: cookies.join(';'), - }, - }); + debug.editing('redirecting to page route %s', redirectUrl); - const html = pageRes.data; + debug.editing('feaas render middleware end in %dms', Date.now() - startTimestamp); - if (!html || html.length === 0) { - throw new Error('Failed to render html'); - } - - // Return expected JSON result - debug.editing('feaas render middleware end in %dms: %o', Date.now() - startTimestamp, { - status: 200, - html, - }); - res.status(200).send(html); + res.redirect(redirectUrl); } catch (err) { const error = err as Record; - if (error.response || error.request) { - // Axios error, which could mean the server or page URL isn't quite right, so provide a more helpful hint - console.info( - // eslint-disable-next-line quotes - "Hint: for non-standard server or Next.js route configurations, you may need to override the 'resolveServerUrl' or 'resolvePageUrl' available on the 'FEAASRenderMiddleware' config." - ); - } - res.status(500).json({ - html: `${error}`, - }); - } - }; + console.info( + // eslint-disable-next-line quotes + "Hint: for non-standard server or Next.js route configurations, you may need to override the 'pageUrl' available on the 'FEAASRenderMiddleware' config." + ); - /** - * Default FEAAS page URL resolution. - * @param {string} serverUrl The root server URL e.g. 'http://localhost:3000' - */ - private defaultResolvePageUrl = (serverUrl: string) => { - return `${serverUrl}/feaas/render`; + res.status(500).send(`${error}`); + } }; } diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts index 404c9e8639..04211687b8 100644 --- a/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts @@ -2,9 +2,7 @@ import chai, { use } from 'chai'; import sinonChai from 'sinon-chai'; import chaiString from 'chai-string'; -import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss'; import { RenderMiddlewareBase } from './render-middleware'; -import { NextApiRequest } from 'next'; import { QUERY_PARAM_EDITING_SECRET, QUERY_PARAM_PROTECTION_BYPASS_SITECORE, @@ -21,54 +19,6 @@ type Query = { describe('RenderMiddlewareBase', () => { class SampleMiddleware extends RenderMiddlewareBase {} - const mockFetcher = () => ({} as AxiosDataFetcher); - - describe('defaultResolveServerUrl', () => { - it('should use https protocol on Vercel', async () => { - process.env.VERCEL = '1'; - - const middleware = new SampleMiddleware(); - - const url = middleware['defaultResolveServerUrl']({ - headers: { host: 'localhost' }, - } as NextApiRequest); - - expect(url).to.equal('https://localhost'); - - delete process.env.VERCEL; - }); - - it('should use http protocol', async () => { - const middleware = new SampleMiddleware(); - - const url = middleware['defaultResolveServerUrl']({ - headers: { host: 'localhost' }, - } as NextApiRequest); - - expect(url).to.equal('http://localhost'); - }); - }); - - it('should use custom resolveServerUrl', async () => { - const serverUrl = 'https://test.com'; - - const middleware = new SampleMiddleware({ - resolveServerUrl: () => serverUrl, - }); - - expect(middleware['resolveServerUrl']({} as NextApiRequest)).to.equal(serverUrl); - }); - - it('should use custom dataFetcher', () => { - const dataFetcher = mockFetcher(); - - const middleware = new SampleMiddleware({ - dataFetcher, - }); - - expect(middleware['dataFetcher']).to.equal(dataFetcher); - }); - describe('getQueryParamsForPropagation', () => { it('should construct query params for protection bypass', () => { const middleware = new SampleMiddleware(); diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts index 8f9205bd4b..2d2503efd9 100644 --- a/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.ts @@ -1,44 +1,14 @@ -import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; -import { NextApiRequest } from 'next'; import { QUERY_PARAM_PROTECTION_BYPASS_SITECORE, QUERY_PARAM_PROTECTION_BYPASS_VERCEL, } from './constants'; -/** - * Configuration for `RenderMiddlewareBase`. - */ -export type RenderMiddlewareBaseConfig = { - /** - * The `AxiosDataFetcher` instance to use for API requests. - * @default new AxiosDataFetcher() - * @see AxiosDataFetcher - */ - dataFetcher?: AxiosDataFetcher; - /** - * Function used to determine the root server URL. This is used for the route/page and subsequent data API requests. - * By default, the host header is used, with https protocol on Vercel (due to serverless function architecture) and http protocol elsewhere. - * @param {NextApiRequest} req The current request. - * @default `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; - * @see resolvePageUrl - */ - resolveServerUrl?: (req: NextApiRequest) => string; -}; - /** * Base class for middleware that handles pages and components rendering in Sitecore Editors. */ export abstract class RenderMiddlewareBase { - protected dataFetcher: AxiosDataFetcher; - protected resolveServerUrl: (req: NextApiRequest) => string; - - constructor(protected config?: RenderMiddlewareBaseConfig) { - this.dataFetcher = config?.dataFetcher ?? new AxiosDataFetcher({ debugger: debug.editing }); - this.resolveServerUrl = config?.resolveServerUrl ?? this.defaultResolveServerUrl; - } - /** - * Gets query parameters that should be passed along to subsequent requests + * Gets query parameters that should be passed along to subsequent requests (e.g. for deployment protection bypass) * @param {Object} query Object of query parameters from incoming URL * @returns Object of approved query parameters */ @@ -58,18 +28,4 @@ export abstract class RenderMiddlewareBase { } return params; }; - - /** - * Default server URL resolution. - * Note we use https protocol on Vercel due to serverless function architecture. - * In all other scenarios, including localhost (with or without a proxy e.g. ngrok) - * and within a nodejs container, http protocol should be used. - * - * For information about the VERCEL environment variable, see - * https://vercel.com/docs/environment-variables#system-environment-variables - * @param {NextApiRequest} req - */ - private defaultResolveServerUrl = (req: NextApiRequest) => { - return `${process.env.VERCEL ? 'https' : 'http'}://${req.headers.host}`; - }; } From 19f2ec4a6ef22ad396201f5048587db5003f3c50 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 8 Feb 2024 01:45:36 +0200 Subject: [PATCH 06/10] cleanup --- .../src/editing/feaas-render-middleware.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts index 8723c501b2..3b98b23ab3 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.test.ts @@ -35,9 +35,6 @@ const mockResponse = () => { res.send = spy(() => { return res; }); - res.end = spy(() => { - return res; - }); res.setPreviewData = spy(() => { return res; }); From 8877231ce2d0794e8486d685fa83b0d50d872090 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 8 Feb 2024 01:46:07 +0200 Subject: [PATCH 07/10] Update --- .../sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts index 450e70700d..98c2e53a28 100644 --- a/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/feaas-render-middleware.ts @@ -11,6 +11,7 @@ export interface FEAASRenderMiddlewareConfig { /** * Defines FEAAS page route to render. * This may be necessary for certain custom Next.js routing configurations. + * @default /feaas/render */ pageUrl?: string; } From 78ab4441a2a9c0cd681ef3c87aa9dc78d8e82311 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 8 Feb 2024 01:47:54 +0200 Subject: [PATCH 08/10] Cleanup --- .../sitecore-jss-nextjs/src/editing/render-middleware.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts index 04211687b8..7b56134fe5 100644 --- a/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/render-middleware.test.ts @@ -1,6 +1,5 @@ /* eslint-disable dot-notation */ -import chai, { use } from 'chai'; -import sinonChai from 'sinon-chai'; +import chai from 'chai'; import chaiString from 'chai-string'; import { RenderMiddlewareBase } from './render-middleware'; import { @@ -9,7 +8,6 @@ import { QUERY_PARAM_PROTECTION_BYPASS_VERCEL, } from './constants'; -use(sinonChai); const expect = chai.use(chaiString).expect; type Query = { From d7d1f6082217e4f0456525804852c32f686f35a5 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 8 Feb 2024 18:45:40 +0200 Subject: [PATCH 09/10] Removed image whitelisting --- .../src/lib/next-config/plugins/feaas.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js index eb5ed87004..d79aeadee4 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js @@ -3,20 +3,6 @@ */ const feaasPlugin = (nextConfig = {}) => { return Object.assign({}, nextConfig, { - // Enable FEAAS Nextjs Image integration by enabling the remotePatterns option. - images: { - remotePatterns: [ - ...(nextConfig.images?.remotePatterns || []), - { - protocol: 'https', - hostname: '**', - }, - { - protocol: 'http', - hostname: '**', - }, - ], - }, webpack: (config, options) => { if (options.isServer) { // Force use of CommonJS on the server for FEAAS SDK since JSS also uses CommonJS entrypoint to FEAAS SDK. From d2d20a93d1ba809a367a3026c7b47bc6417a25b0 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 8 Feb 2024 22:01:48 +0200 Subject: [PATCH 10/10] Added /feaas-render rewrite --- .../src/lib/next-config/plugins/feaas.js | 9 ++++++++ .../nextjs-xmcloud/src/middleware.ts | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js index d79aeadee4..47d8d6ee82 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/lib/next-config/plugins/feaas.js @@ -3,6 +3,15 @@ */ const feaasPlugin = (nextConfig = {}) => { return Object.assign({}, nextConfig, { + async rewrites() { + return [ + ...await nextConfig.rewrites(), + { + source: '/feaas-render', + destination: '/api/editing/feaas/render', + }, + ]; + }, webpack: (config, options) => { if (options.isServer) { // Force use of CommonJS on the server for FEAAS SDK since JSS also uses CommonJS entrypoint to FEAAS SDK. diff --git a/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts new file mode 100644 index 0000000000..8020e2e530 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs-xmcloud/src/middleware.ts @@ -0,0 +1,21 @@ +import type { NextRequest, NextFetchEvent } from 'next/server'; +import middleware from 'lib/middleware'; + +// eslint-disable-next-line +export default async function (req: NextRequest, ev: NextFetchEvent) { + return middleware(req, ev); +} + +export const config = { + /* + * Match all paths except for: + * 1. /api routes + * 2. /_next (Next.js internals) + * 3. /sitecore/api (Sitecore API routes) + * 4. /- (Sitecore media) + * 5. /healthz (Health check) + * 6. /feaas-render (FEaaS render) + * 7. all root files inside /public + */ + matcher: ['/', '/((?!api/|_next/|feaas-render|healthz|sitecore/api/|-/|favicon.ico|sc_logo.svg).*)'], +};