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

refactor: move API endpoints to route handlers #572

Merged
merged 3 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/api/charming-overlords/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { OAuth2Client } from "google-auth-library";
import { NextRequest, NextResponse } from "next/server";

const GOOGLE_APP_ID = process.env.NEXT_PUBLIC_GOOGLE_APP_ID;
const GOOGLE_APP_SECRET = process.env.GOOGLE_APP_SECRET;

const oAuth2Client = new OAuth2Client(
GOOGLE_APP_ID,
GOOGLE_APP_SECRET,
"postmessage"
);

export async function POST(request: NextRequest): Promise<NextResponse> {
const { code } = await request.json();

if (!code) {
return new NextResponse("Missing authorization code", { status: 422 });
}

try {
const {
tokens: { id_token: idToken },
} = await oAuth2Client.getToken(code);

return NextResponse.json({ idToken }, { status: 200 });
} catch (err) {
return new NextResponse("Not authorized", { status: 401 });
}
}
88 changes: 88 additions & 0 deletions app/api/preview/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from "next/server";
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
import { gql } from "@urql/core";
import { fallbackLng } from "@/lib/i18n/settings";
import queryAPI from "@/lib/api/client/query";
import { getLocaleString, getSiteFromLocale } from "@/lib/helpers/site";

const PREVIEW_SECRET_TOKEN = process.env.CRAFT_SECRET_TOKEN;
const CRAFT_HOMEPAGE_URI = "__home__";

const Query = gql(`
query PagePreviewQuery($site: [String], $uri: [String]) {
entry(site: $site, uri: $uri) {
__typename
uri
title
}
}
`);

function isCraftPreview(params: URLSearchParams): boolean {
return (
!!params.get("x-craft-preview") || !!params.get("x-craft-live-preview")
);
}

export async function GET(request: NextRequest): Promise<NextResponse> {
const { searchParams } = request.nextUrl;
const secret = searchParams.get("secret");
const previewToken = searchParams.get("token") || undefined;
const site = getSiteFromLocale(
(searchParams.get("site") || fallbackLng).toLowerCase()
);
const locale = getLocaleString(site);
const uri = searchParams.get("uri");

// Check that this request came from Craft
if (!isCraftPreview) {
return new NextResponse("Invalid client", { status: 401 });
}

// Check the secret and next parameters
// This secret should only be known to this route handler and the CMS
if (secret !== PREVIEW_SECRET_TOKEN) {
return new NextResponse("Invalid token", { status: 401 });
}

if (!uri) {
return new NextResponse("URI missing", { status: 422 });
}

const { data } = await queryAPI({
query: Query,
variables: {
site: [site],
uri: [uri],
},
previewToken,
fetchOptions: { cache: "no-store" },
});

// If the uri doesn't exist prevent draft mode from being enabled
if (!data?.entry?.uri) {
return new NextResponse("Invalid uri", { status: 422 });
}

// Enable Draft Mode by setting the cookie
draftMode().enable();

const segments = [locale];

if (data.entry.uri !== CRAFT_HOMEPAGE_URI) {
segments.push(data.entry.uri);
}

const params = new URLSearchParams({});

if (previewToken) {
params.set("preview", previewToken);
}

// Redirect to the path from the fetched entry
// We don't redirect to searchParams.uri as that might lead to open redirect vulnerabilities
const redirectPath = `/${segments.join("/")}?${params.toString()}`;

redirect(redirectPath, "replace");
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import tags from "@/lib/api/client/tags";
import { fallbackLng, languages } from "@/lib/i18n/settings";
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
import tags from "@/lib/api/client/tags";
import { fallbackLng, languages } from "@/lib/i18n/settings";

const REVALIDATE_SECRET_TOKEN = process.env.CRAFT_REVALIDATE_SECRET_TOKEN;
const CRAFT_HOMEPAGE_URI = "__home__";
Expand Down
4 changes: 4 additions & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { loadEnvConfig } from "@next/env";

const { defineConfig } = require("cypress");
const projectDir = process.cwd();

module.exports = defineConfig({
experimentalModifyObstructiveThirdPartyCode: true,
Expand All @@ -13,4 +16,5 @@ module.exports = defineConfig({
bundler: "webpack",
},
},
env: loadEnvConfig(projectDir).combinedEnv,
});
22 changes: 22 additions & 0 deletions cypress/e2e/api/charming-overlords.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
context("POST /charming-overlords", () => {
it("rejects requests without a code", () => {
cy.request({
url: "/api/charming-overlords",
method: "POST",
body: {},
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.eq(422);
});
});
it("rejects invalid codes", () => {
cy.request({
url: "/api/charming-overlords",
body: { code: "somecode" },
method: "POST",
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.eq(401);
});
});
});
65 changes: 65 additions & 0 deletions cypress/e2e/api/preview.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const url = "/api/preview";
const uri = "news";
const invalidUri = "badnews";
const secret = Cypress.env("CRAFT_SECRET_TOKEN");
const token = "previewToken";

context("GET /preview", () => {
it("rejects requests that do not come from Craft", () => {
cy.request({
url,
method: "GET",
failOnStatusCode: false,
}).then(({ status }) => {
expect(status).to.eq(401);
});
});
it("rejects requests that do not have a secret", () => {
const params = new URLSearchParams({ "x-craft-preview": true });
cy.request({
url: `${url}?${params.toString()}`,
method: "GET",
failOnStatusCode: false,
}).then(({ status }) => {
expect(status).to.eq(401);
});
});
it("rejects requests that do not have a preview URI", () => {
const params = new URLSearchParams({ "x-craft-preview": true, secret });
cy.request({
url: `${url}?${params.toString()}`,
method: "GET",
failOnStatusCode: false,
}).then(({ status }) => {
expect(status).to.eq(422);
});
});
it("rejects requests that have an invalid URI", () => {
const params = new URLSearchParams({
"x-craft-preview": true,
secret,
uri: invalidUri,
});
cy.request({
url: `${url}?${params.toString()}`,
method: "GET",
failOnStatusCode: false,
}).then(({ status }) => {
expect(status).to.eq(422);
});
});
it("redirects to a preview page", () => {
const params = new URLSearchParams({
"x-craft-preview": true,
secret,
uri,
token,
});
cy.request({
url: `${url}?${params.toString()}`,
method: "GET",
}).then(({ redirects, headers }) => {
expect(redirects.length).to.be.gt(0);
});
});
});
37 changes: 37 additions & 0 deletions cypress/e2e/api/revalidate.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const url = "/api/revalidate";
const uri = "news";
const secret = Cypress.env("CRAFT_REVALIDATE_SECRET_TOKEN");

context("GET /revalidate", () => {
it("rejects missing URI segments", () => {
cy.request({
url,
method: "GET",
}).then(({ body }) => {
const { revalidated, message } = body;
expect(revalidated).to.eq(false);
expect(message).to.eq("Missing path to revalidate");
});
});
it("rejects missing secret", () => {
const params = new URLSearchParams({ uri });
cy.request({
url: `${url}?${params.toString()}`,
method: "GET",
}).then(({ body }) => {
const { revalidated, message } = body;
expect(revalidated).to.eq(false);
expect(message).to.eq("Invalid token");
});
});
it("revalidates", () => {
const params = new URLSearchParams({ uri, secret });
cy.request({
url: `${url}?${params.toString()}`,
method: "GET",
}).then(({ body }) => {
const { revalidated } = body;
expect(revalidated).to.eq(true);
});
});
});
4 changes: 0 additions & 4 deletions helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export function fileSize(size) {
);
}

export function isCraftPreview(query) {
return query["x-craft-preview"] || query["x-craft-live-preview"];
}

export function wait(seconds) {
return new Promise((resolve) => {
setTimeout(resolve, seconds * 1000);
Expand Down
25 changes: 0 additions & 25 deletions pages/api/charming-overlords.js

This file was deleted.

84 changes: 0 additions & 84 deletions pages/api/preview.js

This file was deleted.

Loading