Skip to content

Commit

Permalink
feat: support x-contentful-preview-secret for hobby accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
jsdalton committed Jun 3, 2024
1 parent b537b3a commit dab58a8
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 6 deletions.
18 changes: 18 additions & 0 deletions lib/app-router/handlers/enable-draft.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ describe('handler', () => {
expect(result).toHaveProperty('status', 401);
});

describe('when a x-contentful-preview-secret is provided as a query param', () => {
beforeEach(() => {
vi.stubEnv('VERCEL_AUTOMATION_BYPASS_SECRET', '');
vi.stubEnv('CONTENTFUL_PREVIEW_SECRET', bypassToken);
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-contentful-preview-secret=${bypassToken}`;
request = new NextRequest(url);
});

it('redirects safely to the provided path and DOES NOT pass through the token and bypass cookie query params', async () => {
const result = await GET(request);
expect(result).to.be.undefined;
expect(draftModeMock.enable).toHaveBeenCalled();
expect(vi.mocked(redirect)).toHaveBeenCalledWith(
`https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/blogs/my-cat`,
);
});
});

describe('when a x-vercel-protection-bypass token is provided as a query param', () => {
beforeEach(() => {
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=${bypassToken}`;
Expand Down
8 changes: 7 additions & 1 deletion lib/app-router/handlers/enable-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export async function enableDraftHandler(
path,
host,
bypassToken: bypassTokenFromQuery,
contentfulPreviewSecret: contentfulPreviewSecretFromQuery,
} = parseRequestUrl(request.url);

// if we're in development, we don't need to check for a bypass token, and we can just enable draft mode
Expand All @@ -31,6 +32,9 @@ export async function enableDraftHandler(
if (bypassTokenFromQuery) {
bypassToken = bypassTokenFromQuery;
aud = host;
} else if (contentfulPreviewSecretFromQuery) {
bypassToken = contentfulPreviewSecretFromQuery;
aud = host;
} else {
// if we don't have a bypass token from the query we fall back to the _vercel_jwt cookie to find
// the correct authorization bypass elements
Expand All @@ -53,7 +57,9 @@ export async function enableDraftHandler(
aud = vercelJwt.aud;
}

if (bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
// hobby Vercel accounts may not have a VERCEL_AUTOMATION_BYPASS_SECRET, so we fallback to checking the value against the CONTENTFUL_PREVIEW_SECRET
// env var, which is supported as a workaround for these accounts
if ((bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) && (contentfulPreviewSecretFromQuery !== process.env.CONTENTFUL_PREVIEW_SECRET)) {
return new Response(
'The bypass token you are authorized with does not match the bypass secret for this deployment. You might need to redeploy or go back and try the link again.',
{ status: 403 },
Expand Down
19 changes: 19 additions & 0 deletions lib/pages-router/handlers/enable-draft.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ describe('handler', () => {
expect(apiResponseSpy.send).toHaveBeenCalledWith(expect.any(String));
});

describe('when a x-contentful-preview-secret is provided as a query param', () => {
beforeEach(() => {
vi.stubEnv('VERCEL_AUTOMATION_BYPASS_SECRET', '');
vi.stubEnv('CONTENTFUL_PREVIEW_SECRET', bypassToken);
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-contentful-preview-secret=${bypassToken}`;
request = makeNextApiRequest(url);
});

it('redirects safely to the provided path and DOES NOT passes through the token and bypass cookie query params', async () => {
const result = await handler(request, response);
expect(result).to.be.undefined;
expect(apiResponseSpy.setDraftMode).toHaveBeenCalledWith({ enable: true });
expect(apiResponseSpy.redirect).toHaveBeenCalledWith(
`https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/blogs/my-cat`,
);
});
});


describe('when a x-vercel-protection-bypass token is provided as a query param', () => {
beforeEach(() => {
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=${bypassToken}`;
Expand Down
8 changes: 7 additions & 1 deletion lib/pages-router/handlers/enable-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const enableDraftHandler: NextApiHandler = async (
path,
host,
bypassToken: bypassTokenFromQuery,
contentfulPreviewSecret: contentfulPreviewSecretFromQuery,
} = parseNextApiRequest(request);

// if we're in development, we don't need to check for a bypass token, and we can just enable draft mode
Expand All @@ -29,6 +30,9 @@ export const enableDraftHandler: NextApiHandler = async (
if (bypassTokenFromQuery) {
bypassToken = bypassTokenFromQuery;
aud = host;
} else if (contentfulPreviewSecretFromQuery) {
bypassToken = contentfulPreviewSecretFromQuery;
aud = host;
} else {
// if x-vercel-protection-bypass not provided in query, we defer to parsing the _vercel_jwt cookie
// which bundlees the bypass token value in its payload
Expand All @@ -52,7 +56,9 @@ export const enableDraftHandler: NextApiHandler = async (
aud = vercelJwt.aud;
}

if (bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
// hobby Vercel accounts may not have a VERCEL_AUTOMATION_BYPASS_SECRET, so we fallback to checking the value against the CONTENTFUL_PREVIEW_SECRET
// env var, which is supported as a workaround for these accounts
if ((bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) && (contentfulPreviewSecretFromQuery !== process.env.CONTENTFUL_PREVIEW_SECRET)) {
response.status(403).send(
'The bypass token you are authorized with does not match the bypass secret for this deployment. You might need to redeploy or go back and try the link again.'
)
Expand Down
3 changes: 2 additions & 1 deletion lib/utils/url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { buildRedirectUrl, parseNextApiRequest, parseRequestUrl } from './url';
import { makeNextApiRequest } from '../../test/helpers';

const requestUrl = `https://my.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=foo`;
const requestUrl = `https://my.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=foo&x-contentful-preview-secret=bar`;

describe('parseNextApiRequest', () => {
const request = makeNextApiRequest(requestUrl);
Expand All @@ -12,6 +12,7 @@ describe('parseNextApiRequest', () => {
expect(result).toHaveProperty('origin', 'https://my.vercel.app')
expect(result).toHaveProperty('host', 'my.vercel.app')
expect(result).toHaveProperty('bypassToken', 'foo')
expect(result).toHaveProperty('contentfulPreviewSecret', 'bar')
expect(result).toHaveProperty('path', '/blogs/my-cat')
})
})
Expand Down
7 changes: 4 additions & 3 deletions lib/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface ParsedRequestUrl {
host: string;
path: string;
bypassToken: string;
contentfulPreviewSecret: string;
}

export const parseNextApiRequest = (
Expand All @@ -16,8 +17,7 @@ export const parseNextApiRequest = (
const protocol = request.headers['x-forwarded-proto'] || 'https'
const requestUrl = request.url && new URL(request.url, `${protocol}://${hostHeader}`).toString()

const { origin, path, host, bypassToken } = parseRequestUrl(requestUrl)
return { origin, path, host, bypassToken };
return parseRequestUrl(requestUrl)
}

export const parseRequestUrl = (
Expand All @@ -28,12 +28,13 @@ export const parseRequestUrl = (

const rawPath = searchParams.get('path') || '';
const bypassToken = searchParams.get('x-vercel-protection-bypass') || '';
const contentfulPreviewSecret = searchParams.get('x-contentful-preview-secret') || '';

// to allow query parameters to be passed through to the redirected URL, the original `path` should already be
// URI encoded, and thus must be decoded here
const path = decodeURIComponent(rawPath);

return { origin, path, host, bypassToken };
return { origin, path, host, bypassToken, contentfulPreviewSecret };
};

export const buildRedirectUrl = ({
Expand Down

0 comments on commit dab58a8

Please sign in to comment.