Skip to content

Commit

Permalink
[Next.js] Deployment Protection Bypass support for Editing (#1634)
Browse files Browse the repository at this point in the history
* Added support for deployment protection bypass query string parameters (or any query string parameters, really) on the editing/render endpoint. Currently, this is setup for Sitecore and Vercel protection bypass query params, but could easily be extended to handle others.

* updated CHANGELOG
  • Loading branch information
ambrauer authored Oct 19, 2023
1 parent e8e5877 commit 35a590d
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 103 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Our versioning strategy is as follows:
### 🎉 New Features & Improvements

* `[templates/nextjs]` Enable client-only BYOC component imports. Client-only components can be imported through src/byoc-imports/index.client.ts. Hybrid (server render with client hydration) components can be imported through src/byoc-imports/index.hybrid.ts. BYOC scaffold logic is also moved from nextjs-sxa addon into base template ([#1628](https://github.com/Sitecore/jss/pull/1628))

* `[templates/nextjs]` Import SitecoreForm component into sample nextjs app ([#1628](https://github.com/Sitecore/jss/pull/1628))
* `[sitecore-jss-nextjs]` (Vercel/Sitecore) Deployment Protection Bypass support for editing integration. ([#1634](https://github.com/Sitecore/jss/pull/1634))

### 🐛 Bug Fixes

Expand Down
3 changes: 3 additions & 0 deletions packages/sitecore-jss-nextjs/src/editing/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const QUERY_PARAM_EDITING_SECRET = 'secret';
export const QUERY_PARAM_PROTECTION_BYPASS_SITECORE = 'x-sitecore-protection-bypass';
export const QUERY_PARAM_PROTECTION_BYPASS_VERCEL = 'x-vercel-protection-bypass';
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, use } from 'chai';
import { NextApiRequest, NextApiResponse } from 'next';
import { QUERY_PARAM_EDITING_SECRET } from './editing-data-service';
import { QUERY_PARAM_EDITING_SECRET } from './constants';
import { EditingData } from './editing-data';
import { EditingDataCache } from './editing-data-cache';
import { EditingDataMiddleware } from './editing-data-middleware';
Expand Down Expand Up @@ -42,7 +42,7 @@ const mockCache = (data?: EditingData) => {
const cache = {} as EditingDataCache;
cache.set = spy();
cache.get = spy(() => {
return data;
return Promise.resolve(data);
});
return cache;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { EditingDataCache, editingDataDiskCache } from './editing-data-cache';
import { EditingData, isEditingData } from './editing-data';
import { QUERY_PARAM_EDITING_SECRET } from './editing-data-service';
import { QUERY_PARAM_EDITING_SECRET } from './constants';
import { getJssEditingSecret } from '../utils/utils';

export interface EditingDataMiddlewareConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { EditingDataCache } from './editing-data-cache';
import {
ServerlessEditingDataService,
BasicEditingDataService,
QUERY_PARAM_EDITING_SECRET,
generateKey,
} from './editing-data-service';
import { QUERY_PARAM_EDITING_SECRET } from './constants';
import sinonChai from 'sinon-chai';
import { spy } from 'sinon';

Expand Down Expand Up @@ -93,6 +93,29 @@ describe('ServerlessEditingDataService', () => {
});
});

it('should invoke PUT request with extra params and return in preview data', async () => {
const data = {
path: '/styleguide',
} as EditingData;
const key = '1234key';
const serverUrl = 'https://test.com';
const paramFoo = 'foo';
const paramBar = 'bar';
const params = { paramFoo, paramBar };
const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${secret}&paramFoo=${paramFoo}&paramBar=${paramBar}`;

const fetcher = mockFetcher();

const service = new ServerlessEditingDataService({ dataFetcher: fetcher });
service['generateKey'] = () => key;

return service.setEditingData(data, serverUrl, params).then((previewData) => {
expect(previewData.params).to.equal(params);
expect(fetcher.put).to.have.been.calledOnce;
expect(fetcher.put).to.have.been.calledWithExactly(expectedUrl, data);
});
});

it('should use custom apiRoute', async () => {
const data = {
layoutData: { sitecore: { route: { itemId: 'd6ac9d26-9474-51cf-982d-4f8d44951229' } } },
Expand All @@ -115,24 +138,23 @@ describe('ServerlessEditingDataService', () => {
});
});

it('should URI encode secret', async () => {
it('should URI encode secret and params', async () => {
const superSecret = ';,/?:@&=+$';
const encodedSecret = encodeURIComponent(superSecret);
process.env.JSS_EDITING_SECRET = superSecret;
const data = {
layoutData: { sitecore: { route: { itemId: 'd6ac9d26-9474-51cf-982d-4f8d44951229' } } },
} as EditingData;
const key = '1234key';
const serverUrl = 'https://test.com';
const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${encodeURIComponent(
superSecret
)}`;
const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${encodedSecret}&param=${encodedSecret}`;

const fetcher = mockFetcher();

const service = new ServerlessEditingDataService({ dataFetcher: fetcher });
service['generateKey'] = () => key;

return service.setEditingData(data, serverUrl).then(() => {
return service.setEditingData(data, serverUrl, { param: superSecret }).then(() => {
expect(fetcher.put).to.have.been.calledOnce;
expect(fetcher.put).to.have.been.calledWithExactly(expectedUrl, data);
});
Expand All @@ -159,6 +181,28 @@ describe('ServerlessEditingDataService', () => {
expect(fetcher.get).to.have.been.calledWith(expectedUrl);
});

it('should invoke GET request with extra params', async () => {
const data = {
path: '/styleguide',
} as EditingData;
const key = '1234key';
const serverUrl = 'https://test.com';
const paramFoo = 'foo';
const paramBar = 'bar';
const params = { paramFoo, paramBar };
const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${secret}&paramFoo=${paramFoo}&paramBar=${paramBar}`;

const fetcher = mockFetcher(data);

const service = new ServerlessEditingDataService({ dataFetcher: fetcher });
service['generateKey'] = () => key;

const editingData = await service.getEditingData({ key, serverUrl, params });
expect(editingData).to.equal(data);
expect(fetcher.get).to.have.been.calledOnce;
expect(fetcher.get).to.have.been.calledWith(expectedUrl);
});

it('should return undefined if serverUrl missing', async () => {
const data = {
path: '/styleguide',
Expand All @@ -181,7 +225,7 @@ describe('BasicEditingDataService', () => {
const cache = {} as EditingDataCache;
cache.set = spy();
cache.get = spy(() => {
return data;
return Promise.resolve(data);
});
return cache;
};
Expand Down
34 changes: 27 additions & 7 deletions packages/sitecore-jss-nextjs/src/editing/editing-data-service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { QUERY_PARAM_EDITING_SECRET } from './constants';
import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss';
import { EditingData } from './editing-data';
import { EditingDataCache, editingDataDiskCache } from './editing-data-cache';
import { getJssEditingSecret } from '../utils/utils';
import { PreviewData } from 'next';

export const QUERY_PARAM_EDITING_SECRET = 'secret';

/**
* Data for Next.js Preview (Editing) mode
*/
export interface EditingPreviewData {
key: string;
serverUrl?: string;
params?: { [key: string]: string };
}

/**
Expand All @@ -24,7 +24,11 @@ export interface EditingDataService {
* @param {string} serverUrl The server url e.g. which can be used for further API requests
* @returns The {@link EditingPreviewData} containing the information to use for retrieval
*/
setEditingData(data: EditingData, serverUrl: string): Promise<EditingPreviewData>;
setEditingData(
data: EditingData,
serverUrl: string,
params?: { [key: string]: string }
): Promise<EditingPreviewData>;
/**
* Retrieves Sitecore editor payload data
* @param {PreviewData} previewData Editing preview data containing the information to use for retrieval
Expand Down Expand Up @@ -149,13 +153,18 @@ export class ServerlessEditingDataService implements EditingDataService {
* @param {string} serverUrl The server url to use for subsequent data API requests
* @returns {Promise} The {@link EditingPreviewData} containing the generated key and serverUrl to use for retrieval
*/
async setEditingData(data: EditingData, serverUrl: string): Promise<EditingPreviewData> {
async setEditingData(
data: EditingData,
serverUrl: string,
params?: { [key: string]: string }
): Promise<EditingPreviewData> {
const key = this.generateKey(data);
const url = this.getUrl(serverUrl, key);
const url = this.getUrl(serverUrl, key, params);

const previewData = {
key,
serverUrl,
params,
} as EditingPreviewData;

debug.editing('storing editing data for %o: %o', previewData, data);
Expand All @@ -174,20 +183,31 @@ export class ServerlessEditingDataService implements EditingDataService {
if (!editingPreviewData?.serverUrl) {
return undefined;
}
const url = this.getUrl(editingPreviewData.serverUrl, editingPreviewData.key);
const url = this.getUrl(
editingPreviewData.serverUrl,
editingPreviewData.key,
editingPreviewData.params
);

debug.editing('retrieving editing data for %o', previewData);
return this.dataFetcher.get<EditingData>(url).then((response: { data: EditingData }) => {
return response.data;
});
}

protected getUrl(serverUrl: string, key: string): string {
protected getUrl(serverUrl: string, key: string, params?: { [key: string]: string }): string {
// Example URL format:
// http://localhost:3000/api/editing/data/52961eea-bafd-5287-a532-a72e36bd8a36-qkb4e3fv5x?secret=1234secret
const apiRoute = this.apiRoute?.replace('[key]', key);
const url = new URL(apiRoute, serverUrl);
url.searchParams.append(QUERY_PARAM_EDITING_SECRET, getJssEditingSecret());
if (params) {
for (const key in params) {
if ({}.hasOwnProperty.call(params, key)) {
url.searchParams.append(key, params[key]);
}
}
}
return url.toString();
}
}
Expand Down
Loading

0 comments on commit 35a590d

Please sign in to comment.