Skip to content

Commit

Permalink
refactor: move API endpoints to route handlers (#572)
Browse files Browse the repository at this point in the history
* chore: trigger build

* chore: trigger build

* refactor: move API endpoints to route handlers
  • Loading branch information
alexgoff authored Oct 15, 2024
1 parent c624975 commit 93c68e4
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 164 deletions.
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

0 comments on commit 93c68e4

Please sign in to comment.