Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Next.js] [BYOC] Component Builder integration endpoint #1729

Merged
merged 11 commits into from
Feb 8, 2024
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ Our versioning strategy is as follows:
* `[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-nextjs]` Add protected endpoint which provides configuration information (the sitecore packages used by the app and all registered components) to be used to determine feature compatibility on Pages side. ([#1724](https://github.com/Sitecore/jss/pull/1724))
* `[templates/nextjs]` `[templates/nextjs-styleguide]` Modify all GraphQLRequestClient import statements so that it gets imported from the /graphql submodule ([#1728](https://github.com/Sitecore/jss/pull/1728))
* `[sitecore-jss-nextjs]` `[templates/nextjs]` [BYOC] Component Builder integration endpoint ([#1729](https://github.com/Sitecore/jss/pull/1729))

### 🐛 Bug Fixes

* `[templates/nextjs]` `[templates/nextjs-styleguide]` Modify all GraphQLRequestClient import statements so that it gets imported from the /graphql submodule ([#1728](https://github.com/Sitecore/jss/pull/1728))
* `[sitecore-jss-nextjs]` Internal link in RichText is broken when nested tags are added ([#1718](https://github.com/Sitecore/jss/pull/1718))
* `[templates/node-headless-ssr-proxy]` `[node-headless-ssr-proxy]` Add sc_site qs parameter to Layout Service requests by default ([#1660](https://github.com/Sitecore/jss/pull/1660))
* `[templates/nextjs-sxa]` Fixed Image component when there is using Banner variant which set property background-image when image is empty. ([#1689](https://github.com/Sitecore/jss/pull/1689)) ([#1692](https://github.com/Sitecore/jss/pull/1692))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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).*)'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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. 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.
*/

// Wire up the FEAASRenderMiddleware handler
const handler = new FEAASRenderMiddleware().getHandler();

export default handler;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 && <FEAAS.Component src={feaasSrc} />}
{/** Render all the registered components */}
<BYOC />
</>
);
};

export const getServerSideProps: GetServerSideProps = async (context) => {
return {
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,
};
};

export default FEAASRender;
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/
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 } from './render-middleware';

export interface EditingRenderMiddlewareConfig {
/**
Expand Down Expand Up @@ -52,7 +49,7 @@ export interface EditingRenderMiddlewareConfig {
* 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;
Expand All @@ -62,6 +59,8 @@ export class EditingRenderMiddleware {
* @param {EditingRenderMiddlewareConfig} [config] Editing render middleware config
*/
constructor(config?: EditingRenderMiddlewareConfig) {
super();

this.editingDataService = config?.editingDataService ?? editingDataService;
this.dataFetcher = config?.dataFetcher ?? new AxiosDataFetcher({ debugger: debug.editing });
this.resolvePageUrl = config?.resolvePageUrl ?? this.defaultResolvePageUrl;
Expand All @@ -76,28 +75,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<void> => {
const { method, query, body, headers } = req;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/* 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 {
QUERY_PARAM_EDITING_SECRET,
QUERY_PARAM_PROTECTION_BYPASS_SITECORE,
QUERY_PARAM_PROTECTION_BYPASS_VERCEL,
} from './constants';
import { FEAASRenderMiddleware } from './feaas-render-middleware';
import { spy } from 'sinon';
import sinonChai from 'sinon-chai';

use(sinonChai);

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.send = spy(() => {
return res;
});
res.setPreviewData = spy(() => {
return res;
});
res.setHeader = spy(() => {
return res;
});
res.redirect = spy(() => {
return res;
});

return res;
};

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 query = {} as Query;
query[QUERY_PARAM_EDITING_SECRET] = secret;

const req = mockRequest(query);
const res = mockResponse();

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.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 query = {
feaasSrc: 'https://feaas.blob.core.windows.net/components/xxx/xyz/responsive/staged',
} as Query;
query[QUERY_PARAM_EDITING_SECRET] = secret;

const req = mockRequest(query);
const res = mockResponse();

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.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', async () => {
const query = {} as Query;
query[QUERY_PARAM_EDITING_SECRET] = secret;

const req = mockRequest(query);
const res = mockResponse();

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({});
expect(res.status).to.have.been.calledOnce;
expect(res.status).to.have.been.calledWith(500);
expect(res.send).to.have.been.calledOnce;
expect(res.send).to.have.been.calledWith('<html><body>Error: Test Error</body></html>');
});

it('should respondWith 405 for unsupported method', async () => {
const req = mockRequest({}, 'POST');
const res = mockResponse();

const middleware = new FEAASRenderMiddleware();
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.send).to.have.been.calledOnce;
expect(res.send).to.have.been.calledWith(
"<html><body>Invalid request method 'POST'</body></html>"
);
});

it('should respond with 401 for missing secret', async () => {
const query = {} as Query;
const req = mockRequest(query);
const res = mockResponse();

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.send).to.have.been.calledOnce;
expect(res.send).to.have.been.calledWith('<html><body>Missing or invalid secret</body></html>');
});

it('should respond with 401 for invalid secret', async () => {
const query = {} as Query;
query[QUERY_PARAM_EDITING_SECRET] = 'nope';
const req = mockRequest(query);
const res = mockResponse();

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.send).to.have.been.calledOnce;
expect(res.send).to.have.been.calledWith('<html><body>Missing or invalid secret</body></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 pageUrl = '/some/path/feaas/render';

const middleware = new FEAASRenderMiddleware({
pageUrl,
});
const handler = middleware.getHandler();

await handler(req, res);

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 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 req = mockRequest(query);
const res = mockResponse();

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.redirect).to.have.been.calledOnce;
expect(res.redirect).to.have.been.calledWith(
'/feaas/render?x-sitecore-protection-bypass=token1234Sitecore&x-vercel-protection-bypass=token1234Vercel'
);
});
});
Loading