-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: move API endpoints to route handlers (#572)
* chore: trigger build * chore: trigger build * refactor: move API endpoints to route handlers
- Loading branch information
Showing
11 changed files
with
247 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} |
4 changes: 2 additions & 2 deletions
4
app/api/app-revalidate/route.ts → app/api/revalidate/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.