Skip to content

Commit

Permalink
1.3.1 (#702)
Browse files Browse the repository at this point in the history
<!--
Filling out this template is required. Any PR that does not include
enough information to be reviewed may be closed at a maintainers'
discretion. All new code requires documentation and tests to ensure
against regressions.
-->

### Description of the Change
<!--
We must be able to understand the design of your change from this
description. The maintainer reviewing this PR may not have worked with
this code recently, so please provide as much detail as possible.

Where possible, please also include:
- verification steps to ensure your change has the desired effects and
has not introduced any regressions
- any benefits that will be realized
- any alternative implementations or possible drawbacks that you
considered
- screenshots or screencasts
-->

<!-- Enter any applicable Issue number(s) here that will be
closed/resolved by this PR. -->
Closes #

### How to test the Change
<!-- Please provide steps on how to test or validate that the change in
this PR works as described. -->

### Changelog Entry
<!--
Please include a summary for this PR, noting whether this is something
being Added / Changed / Deprecated / Removed / Fixed / or Security
related. You can replace the sample entries after this comment block
with the single changelog entry line for this PR. -->
> Added - New feature
> Changed - Existing functionality
> Deprecated - Soon-to-be removed feature
> Removed - Feature
> Fixed - Bug fix
> Security - Vulnerability


### Credits
<!-- Please list any and all contributors on this PR so that they can be
added to this projects CREDITS.md file. -->
Props @username, @username2, ...


### Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you are unsure about any of these, please ask for
clarification. We are here to help! -->
- [ ] I agree to follow this project's [**Code of
Conduct**](https://github.com/10up/.github/blob/trunk/CODE_OF_CONDUCT.md).
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my change.
- [ ] All new and existing tests pass.
  • Loading branch information
nicholasio authored Feb 26, 2024
2 parents e67821f + d61aad6 commit e75b127
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 27 deletions.
7 changes: 7 additions & 0 deletions .changeset/honest-bags-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@headstartwp/headstartwp": patch
"@headstartwp/core": patch
"@headstartwp/next": patch
---

Add ability to preview using an alternative authorization header
21 changes: 20 additions & 1 deletion docs/documentation/01-Getting Started/headless-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,23 @@ This option control how redirects are handled. There are 2 supported methods of

## debug

You can enable log debugging for both requests and redirects. `debug.requests` will enable logging all API requests made by the framework and `debug.redirects` will log all attempts to detect and fetch a redirect from WordPress.
You can enable log debugging for both requests and redirects. `debug.requests` will enable logging all API requests made by the framework and `debug.redirects` will log all attempts to detect and fetch a redirect from WordPress.

## preview

### alternativeAuthorizationHeader

Tells HeadstartWP to use an alternative header (`X-HeadstartWP-Authorization`) instead of the default `Authorization` header for making authenticated preview requests.

Make sure you have HeadstartWP plugin >= 1.0.1, `@headstartwp/core` >= 1.3.1 and `@headstartwp/next`>= 1.3.1 to use this setting.

```js
module.exports = {
// other configs.
// ...

preview: {
alternativeAuthorizationHeader: true
}
}
```
25 changes: 21 additions & 4 deletions docs/documentation/06-WordPress Integration/previews.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ slug: /wordpress-integration/previews

# Previews

The preview feature requires the 10up's headless WordPress plugin installed. The preview functionality is built on top of [Next.js preview API](https://nextjs.org/docs/advanced-features/preview-mode). It uses a short-lived JWT token generated on the WordPress side that can only be used for previewing, this means it is not necessary to set up a hardcoded secret between WP and Next.js.
The preview feature requires the HeadstartWP plugin installed. The preview functionality is built on top of [Next.js preview API](https://nextjs.org/docs/advanced-features/preview-mode). It uses a short-lived JWT token generated on the WordPress side that can only be used for previewing, this means it is not necessary to set up a hardcoded secret between WP and Next.js.

For previews to work, make sure the frontend URL is entered in WP settings as per instructions in [Installing WordPress Plugin](/learn/getting-started/installing-wordpress-plugin).

The logic for generating the JWT token and redirecting to the preview endpoint can be seen [here](https://github.com/10up/headstartwp/blob/develop/wp/headless-wp/includes/classes/Preview/preview.php).
The logic for generating the JWT token and redirecting it to the preview endpoint can be seen [here](https://github.com/10up/headstartwp/blob/develop/wp/headless-wp/includes/classes/Preview/preview.php).
```php
$token = PreviewToken::generate(
[
Expand Down Expand Up @@ -42,7 +42,7 @@ Below is a summary of the preview workflow.

## Usage

The Next.js project **must** expose a `api/preview` endpoint that uses the [previewHandler](/api/modules/headstartwp_next/#previewhandler).
The Next.js project **must** expose an `api/preview` endpoint that uses the [previewHandler](/api/modules/headstartwp_next/#previewhandler).

```javascript
//src/pages/api/preview.js
Expand Down Expand Up @@ -152,4 +152,21 @@ The JWT token expires after 5 min by default, after this period, open another pr

**I'm unable to preview a custom post type**

Make sure you defined the right `single` property when registering the custom post type. See [headless config docs](/learn/getting-started/headless-config/#customposttypes). The `single` property must match the route prefix for the custom post type.
Make sure you defined the right `single` property when registering the custom post type. See [headless config docs](/learn/getting-started/headless-config/#customposttypes). The `single` property must match the route prefix for the custom post type.

**I have a custom authentication using the Authorization header, how can I use the preview functionality?**

Make sure you have HeadstartWP plugin >= 1.0.1, `@headstartwp/core` >= 1.3.1 and `@headstartwp/next`>= 1.3.1. Then in your `headstartwp.config.js` add the following config:

```js
module.exports = {
// other configs.
// ...

preview: {
alternativeAuthorizationHeader: true
}
}
```

This will tell HeadstartWP to use an alternative header (`X-HeadstartWP-Authorization`) instead of the default `Authorization` header.
46 changes: 42 additions & 4 deletions packages/core/src/data/strategies/AbstractFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ export interface FetchOptions {
*/
bearerToken?: string;

/**
* The preview token to use for the request.
*
* These are tokens issued by the HeadstartWP plugin and is used to authenticate previews
*/
previewToken?: string;

/**
* Flag to enable using the alternative authorization header.
*
* This can be useful if you have separate authentication on your project.
*/
alternativePreviewAuthorizationHeader?: boolean;

/**
* Whether to burst cache by appending a timestamp to the query
*/
Expand Down Expand Up @@ -224,6 +238,21 @@ export abstract class AbstractFetchStrategy<E, Params extends EndpointParams, R
};
}

getPreviewHeaderName(options: Partial<FetchOptions> = {}) {
return options.alternativePreviewAuthorizationHeader
? 'X-HeadstartWP-Authorization'
: 'Authorization';
}

getPreviewAuthHeader(options: Partial<FetchOptions> = {}) {
let previewAuthHeader = '';
if (options.previewToken) {
previewAuthHeader = `Bearer ${options.previewToken}`;
}

return previewAuthHeader;
}

getAuthHeader(options: Partial<FetchOptions> = {}) {
let bearerAuthHeader = '';
if (options.bearerToken) {
Expand Down Expand Up @@ -266,13 +295,22 @@ export abstract class AbstractFetchStrategy<E, Params extends EndpointParams, R
const { burstCache = false } = options;

const args = {};

const headers: Record<string, string> = {};
const authHeader = this.getAuthHeader(options);

if (authHeader) {
headers.Authorization = authHeader;
}

const previewAuthHeader = this.getPreviewAuthHeader(options);

if (options.previewToken) {
headers[this.getPreviewHeaderName(options)] = previewAuthHeader;
}

if (Object.keys(headers).length > 0) {
// @ts-expect-error
args.headers = {
Authorization: authHeader,
};
args.headers = headers;
}

const result = await apiGet(`${this.baseURL}${url}`, args, burstCache);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/data/strategies/SinglePostFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,10 @@ export class SinglePostFetchStrategy<
const { burstCache = false } = options;

if (params.authToken) {
options.bearerToken = params.authToken;
options.previewToken = params.authToken;
}

const authHeader = this.getAuthHeader(options);
const authHeader = this.getPreviewAuthHeader(options);

let error;
if (params.revision && params.id) {
Expand All @@ -288,7 +288,7 @@ export class SinglePostFetchStrategy<
`${this.baseURL}${this.getEndpoint()}/revisions?per_page=1`,
{
headers: {
Authorization: authHeader,
[this.getPreviewHeaderName(options)]: authHeader,
},
},
burstCache,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class VerifyTokenFetchStrategy extends AbstractFetchStrategy<

async fetcher(url: string, params: VerifyTokenParams, options: Partial<FetchOptions> = {}) {
if (params.authToken) {
options.bearerToken = params.authToken;
options.previewToken = params.authToken;
}

return super.fetcher(url, params, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,48 @@ describe('SinglePostFetchStrategy', () => {
);
});

it('handle revisions with alternative headers', async () => {
const samplePostRevision = { title: 'test', id: 1, link: '/post-name' };
const sampleHeaders = {
'x-wp-totalpages': 1,
'x-wp-total': 1,
};

apiGetMock.mockResolvedValue({
headers: sampleHeaders,
json: [samplePostRevision],
});

const params = fetchStrategy.getParamsFromURL('/post-name');
const revisionParams = { ...params, id: 1, revision: true, authToken: 'test token' };

await fetchStrategy.fetcher(
fetchStrategy.buildEndpointURL(revisionParams),
revisionParams,
{
alternativePreviewAuthorizationHeader: true,
},
);

expect(apiGetMock).toHaveBeenNthCalledWith(
1,
'/wp-json/wp/v2/posts/1/revisions?per_page=1',
{
headers: { 'X-HeadstartWP-Authorization': 'Bearer test token' },
},
false,
);

expect(apiGetMock).toHaveBeenNthCalledWith(
2,
'/wp-json/wp/v2/posts/1',
{
headers: { 'X-HeadstartWP-Authorization': 'Bearer test token' },
},
false,
);
});

it('handle draft posts', async () => {
const samplePost = { title: 'test', id: 1 };
const sampleHeaders = {
Expand Down Expand Up @@ -281,6 +323,35 @@ describe('SinglePostFetchStrategy', () => {
);
});

it('handle draft posts with alternative headers', async () => {
const samplePost = { title: 'test', id: 1 };
const sampleHeaders = {
'x-wp-totalpages': 1,
'x-wp-total': 1,
};

apiGetMock.mockResolvedValue({
headers: sampleHeaders,
json: samplePost,
});

const params = fetchStrategy.getParamsFromURL('/post-name');
const draftParams = { ...params, id: 10, authToken: 'test token' };

await fetchStrategy.fetcher(fetchStrategy.buildEndpointURL(draftParams), draftParams, {
alternativePreviewAuthorizationHeader: true,
});

expect(apiGetMock).toHaveBeenNthCalledWith(
1,
'/wp-json/wp/v2/posts/10',
{
headers: { 'X-HeadstartWP-Authorization': 'Bearer test token' },
},
false,
);
});

it('throws errors with bad arguments', async () => {
apiGetMock.mockImplementation(async (url) => {
const isBookEndpoint = url.includes('/wp/v2/book');
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/react/hooks/__tests__/useFetchPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@ describe('useFetchPost', () => {
});
});

it('fetches draft posts with authToken and alternativePreviewAuthorizationHeader', async () => {
// 57 is a hardcoded draft post in msw
const { result } = renderHook(
() =>
useFetchPost(
{ id: DRAFT_POST_ID, authToken: VALID_AUTH_TOKEN },
{
fetchStrategyOptions: {
alternativePreviewAuthorizationHeader: true,
},
},
),
{
wrapper,
},
);

await waitFor(() => {
expect(result.current.error).toBeFalsy();
expect(result.current.data?.post.id).toBe(57);
});
});

it('errors if fetches revisions without authToken', async () => {
const { result } = renderHook(() => useFetchPost({ id: 57, revision: true }), {
wrapper,
Expand All @@ -136,6 +159,32 @@ describe('useFetchPost', () => {
});
});

it('fetches revisions with authToken and alternativePreviewAuthorizationHeader', async () => {
const { result } = renderHook(
() =>
useFetchPost(
{ id: 64, revision: true, authToken: 'Fake Auth Token' },
{
fetchStrategyOptions: {
alternativePreviewAuthorizationHeader: true,
},
},
),
{
wrapper,
},
);

await waitFor(() => {
expect(result.current.error).toBeFalsy();
expect(result.current.data?.post.id).toBe(64);
expect(result.current.data?.post.slug).toBe('ipsum-repudiandae-est-nam');
// ensure fields that don't exists in revisions are returned
expect(result.current.data?.post.format).toBe('standard');
expect(result.current.data?.post?.terms?.category[0]?.slug).toBe('news');
});
});

it('keeps backwards compatibility with swr options and that a warning is made', async () => {
// eslint-disable-next-line no-console
console.warn = jest.fn();
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export type Integrations = {
polylang?: PolylangIntegration;
};

export type PreviewConfig = {
/**
* Flag to enable using the alternative authorization header.
*
* This can be useful if you have separate JWT-based authentication on your project.
*/
alternativeAuthorizationHeader?: boolean;
};

export type HeadlessConfig = {
host?: string;
locale?: string;
Expand All @@ -68,6 +77,7 @@ export type HeadlessConfig = {
useWordPressPlugin?: boolean;
integrations?: Integrations;
sites?: HeadlessConfig[];
preview?: PreviewConfig;
debug?: {
requests?: boolean;
redirects?: boolean;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function getHeadstartWPConfig() {
hostUrl,
integrations,
debug,
preview,
} = __10up__HEADLESS_CONFIG;

const defaultTaxonomies: CustomTaxonomies = [
Expand Down Expand Up @@ -79,6 +80,7 @@ export function getHeadstartWPConfig() {
useWordPressPlugin: useWordPressPlugin || false,
integrations,
debug,
preview,
sites: (sites || []).map((site) => {
// if host is not defined but hostUrl is, infer host from hostUrl
if (typeof site.host === 'undefined' && typeof site.hostUrl !== 'undefined') {
Expand Down Expand Up @@ -119,6 +121,7 @@ export function getSite(site?: HeadlessConfig) {
redirectStrategy: site?.redirectStrategy || settings.redirectStrategy || 'none',
useWordPressPlugin: site?.useWordPressPlugin || settings.useWordPressPlugin || false,
integrations: site?.integrations || settings.integrations,
preview: site?.preview || settings.preview,
};

return headlessConfig;
Expand Down
Loading

0 comments on commit e75b127

Please sign in to comment.