fix(SW-663): Fixed leaking of live preview hash and removing preview pages

This commit is contained in:
Erik Tiekstra
2024-11-07 12:49:13 +01:00
parent 0465f8e450
commit 953f860e5d
16 changed files with 117 additions and 301 deletions

View File

@@ -0,0 +1,19 @@
import { isPreview, setPreviewData } from "@/lib/previewContext"
import InitLivePreview from "@/components/LivePreview"
import { PageArgs, UIDParams } from "@/types/params"
export default function PreviewPage({
searchParams,
params,
}: PageArgs<UIDParams, URLSearchParams>) {
const shouldInitializePreview = searchParams.isPreview === "true"
const isInitialized = isPreview()
if (searchParams.live_preview) {
setPreviewData({ hash: searchParams.live_preview, uid: params.uid })
}
return shouldInitializePreview && !isInitialized ? <InitLivePreview /> : null
}

View File

@@ -1,3 +1,5 @@
import React from "react"
import styles from "./layout.module.css"
import {
@@ -9,15 +11,18 @@ import {
export default function ContentTypeLayout({
breadcrumbs,
preview,
children,
}: React.PropsWithChildren<
LayoutArgs<LangParams & ContentTypeParams & UIDParams> & {
breadcrumbs: React.ReactNode
preview: React.ReactNode
}
>) {
return (
<div className={styles.container}>
<section className={styles.layout}>
{preview}
{breadcrumbs}
{children}
</section>

View File

@@ -1,9 +0,0 @@
"use client"
export default function Error({ error }: { error: Error }) {
return (
<div>
<h2>Something went wrong!</h2>
</div>
)
}

View File

@@ -1,30 +0,0 @@
import "@/app/globals.css"
import "@scandic-hotels/design-system/style.css"
import TrpcProvider from "@/lib/trpc/Provider"
import InitLivePreview from "@/components/LivePreview"
import { getIntl } from "@/i18n"
import ServerIntlProvider from "@/i18n/Provider"
import { setLang } from "@/i18n/serverContext"
import type { LangParams, LayoutArgs } from "@/types/params"
export default async function RootLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
setLang(params.lang)
const { defaultLocale, locale, messages } = await getIntl()
return (
<html lang={params.lang}>
<body>
<InitLivePreview />
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<TrpcProvider>{children}</TrpcProvider>
</ServerIntlProvider>
</body>
</html>
)
}

View File

@@ -1,8 +0,0 @@
export default function NotFound() {
return (
<main>
<h1>Not found</h1>
<p>Could not find requested resource</p>
</main>
)
}

View File

@@ -1,51 +0,0 @@
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import { notFound } from "next/navigation"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { setLang } from "@/i18n/serverContext"
import type {
ContentTypeParams,
LangParams,
PageArgs,
UIDParams,
} from "@/types/params"
export default async function PreviewPage({
params,
searchParams,
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
setLang(params.lang)
try {
ContentstackLivePreview.setConfigFromParams(searchParams)
if (!searchParams.live_preview) {
return notFound()
}
switch (params.contentType) {
case "content-page":
return <ContentPage />
case "loyalty-page":
return <LoyaltyPage />
case "collection-page":
return <CollectionPage />
case "hotel-page":
return <HotelPage />
default:
console.log({ PREVIEW: params })
const type = params.contentType
console.error(`Unsupported content type given: ${type}`)
notFound()
}
} catch (error) {
// TODO: throw 500
console.error("Error in preview page")
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -1,9 +0,0 @@
"use client"
export default function Error({ error }: { error: Error }) {
return (
<div>
<h2>Something went wrong!</h2>
</div>
)
}

View File

@@ -1,41 +0,0 @@
import Footer from "@/components/Current/Footer"
import LangPopup from "@/components/Current/LangPopup"
import InitLivePreview from "@/components/LivePreview"
import SkipToMainContent from "@/components/SkipToMainContent"
import { setLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, LayoutArgs } from "@/types/params"
export const fetchCache = "default-no-store"
export const metadata: Metadata = {
description: "New web",
title: "Scandic Hotels",
}
export default function RootLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
setLang(params.lang)
return (
<html lang={params.lang}>
<head>
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/_static/css/core.css" />
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/_static/css/scandic.css" />
</head>
<body>
<InitLivePreview />
<LangPopup />
<SkipToMainContent />
{children}
<Footer />
</body>
</html>
)
}

View File

@@ -1,8 +0,0 @@
export default function NotFound() {
return (
<main>
<h1>Not found</h1>
<p>Could not find requested resource</p>
</main>
)
}

View File

@@ -1,46 +0,0 @@
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import { previewRequest } from "@/lib/graphql/previewRequest"
import { GetCurrentBlockPage } from "@/lib/graphql/Query/Current/CurrentBlockPage.graphql"
import ContentPage from "@/components/Current/ContentPage"
import LoadingSpinner from "@/components/Current/LoadingSpinner"
import { setLang } from "@/i18n/serverContext"
import type { PageArgs, PreviewParams } from "@/types/params"
import { LangParams } from "@/types/params"
import type { GetCurrentBlockPageData } from "@/types/requests/currentBlockPage"
export default async function CurrentPreviewPage({
params,
searchParams,
}: PageArgs<LangParams, PreviewParams>) {
setLang(params.lang)
try {
ContentstackLivePreview.setConfigFromParams(searchParams)
if (!searchParams.uri || !searchParams.live_preview) {
return <LoadingSpinner />
}
const response = await previewRequest<GetCurrentBlockPageData>(
GetCurrentBlockPage,
{ locale: params.lang, url: searchParams.uri }
)
if (!response.data?.all_current_blocks_page?.total) {
console.log("#### DATA ####")
console.log(response.data)
console.log("SearchParams URI: ", searchParams.uri)
throw new Error("Not found")
}
return <ContentPage data={response.data} />
} catch (error) {
// TODO: throw 500
console.error("Error in current preview page")
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -5,9 +5,7 @@ import { useEffect } from "react"
export default function InitLivePreview() {
useEffect(() => {
if (!ContentstackLivePreview.livePreview) {
ContentstackLivePreview.init()
}
ContentstackLivePreview.init()
}, [])
return null

View File

@@ -1,6 +1,6 @@
import "server-only"
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
// import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import { ClientError, GraphQLClient } from "graphql-request"
import { Lang } from "@/constants/languages"
@@ -17,47 +17,41 @@ export async function request<T>(
params?: RequestInit
): Promise<Data<T>> {
try {
const previewHash = ContentstackLivePreview.hash
client.setHeaders({
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
...params?.headers,
})
if (previewHash) {
client.setHeader("preview_token", env.CMS_PREVIEW_TOKEN)
client.setHeader("live_preview", previewHash)
} else {
client.requestConfig.cache = params?.cache
client.requestConfig.next = params?.next
client.requestConfig.cache = params?.cache
client.requestConfig.next = params?.next
if (env.PRINT_QUERY) {
const print = (await import("graphql/language/printer")).print
const rawResponse = await client.rawRequest<T>(
print(query as DocumentNode),
variables,
{
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
}
)
/**
* TODO: Send to Monitoring (Logging and Metrics)
*/
console.log({
complexityLimit: rawResponse.headers.get("x-query-complexity"),
})
console.log({
referenceDepth: rawResponse.headers.get("x-reference-depth"),
})
console.log({
resolverCost: rawResponse.headers.get("x-resolver-cost"),
})
return {
data: rawResponse.data,
if (env.PRINT_QUERY) {
const print = (await import("graphql/language/printer")).print
const rawResponse = await client.rawRequest<T>(
print(query as DocumentNode),
variables,
{
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
}
)
/**
* TODO: Send to Monitoring (Logging and Metrics)
*/
console.log({
complexityLimit: rawResponse.headers.get("x-query-complexity"),
})
console.log({
referenceDepth: rawResponse.headers.get("x-reference-depth"),
})
console.log({
resolverCost: rawResponse.headers.get("x-resolver-cost"),
})
return {
data: rawResponse.data,
}
}

View File

@@ -1,42 +0,0 @@
import "server-only"
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import { request as graphqlRequest } from "graphql-request"
import { env } from "@/env/server"
import type { DocumentNode } from "graphql"
import type { Data } from "@/types/request"
export async function previewRequest<T>(
query: string | DocumentNode,
variables?: {}
): Promise<Data<T>> {
try {
const hash = ContentstackLivePreview.hash
if (!hash) {
throw new Error("No hash received")
}
const headers = new Headers({
access_token: env.CMS_ACCESS_TOKEN,
preview_token: env.CMS_PREVIEW_TOKEN,
live_preview: hash,
})
const response = await graphqlRequest<T>({
document: query,
requestHeaders: headers,
url: env.CMS_PREVIEW_URL,
variables,
})
return { data: response }
} catch (error) {
console.error("Error in preview graphql request")
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -1,10 +1,10 @@
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import fetchRetry from "fetch-retry"
import { DocumentNode } from "graphql"
import { GraphQLClient } from "graphql-request"
import { cache } from "react"
import { env } from "@/env/server"
import { getPreviewHash, isPreview } from "@/lib/previewContext"
import { request as _request } from "./_request"
@@ -12,12 +12,12 @@ import { Data } from "@/types/request"
export async function request<T>(
query: string | DocumentNode,
variables?: {},
variables?: Record<string, any>,
params?: RequestInit
): Promise<Data<T>> {
const previewHash = ContentstackLivePreview.hash
const cmsUrl = previewHash ? env.CMS_PREVIEW_URL : env.CMS_URL
const shouldUsePreview = isPreview(variables?.uid)
const previewHash = getPreviewHash()
const cmsUrl = shouldUsePreview ? env.CMS_PREVIEW_URL : env.CMS_URL
const client = new GraphQLClient(cmsUrl, {
fetch: cache(async function (url: URL | RequestInfo, params?: RequestInit) {
@@ -31,5 +31,19 @@ export async function request<T>(
}),
})
return _request(client, query, variables, params)
const mergedParams =
shouldUsePreview && previewHash
? {
...params,
headers: {
...params?.headers,
live_preview: previewHash,
preview_token: env.CMS_PREVIEW_TOKEN,
},
cache: undefined,
next: undefined,
}
: params
return _request(client, query, variables, mergedParams)
}

42
lib/previewContext.ts Normal file
View File

@@ -0,0 +1,42 @@
import { cache } from "react"
interface PreviewData {
hash: string
uid: string
}
const getRef = cache((): { current: PreviewData | undefined } => ({
current: undefined,
}))
/**
* Set the preview hash for the current request
*
* It works kind of like React's context,
* but on the server side, per request.
*
* @param hash
*/
export function setPreviewData(data: PreviewData | undefined) {
console.log("SETTING HASH")
getRef().current = data
}
/**
* Get the preview hash set for the current request
*/
export function getPreviewHash() {
return getRef().current?.hash
}
/**
* Check if the current request is a preview by comparing the uid
*/
export function isPreview(uid?: string) {
const data = getRef().current
if (uid && data?.hash) {
return data.uid === uid
}
return false
}

View File

@@ -19,7 +19,6 @@ export const middleware: NextMiddleware = async (request) => {
const contentTypePathName = pathWithoutTrailingSlash.replace(`/${lang}`, "")
const isPreview = request.nextUrl.pathname.includes("/preview")
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
const { contentType, uid } = await fetchAndCacheEntry(
@@ -41,21 +40,10 @@ export const middleware: NextMiddleware = async (request) => {
const isCurrent = contentType ? contentType.indexOf("current") >= 0 : false
if (isPreview) {
if (isCurrent) {
searchParams.set("uri", contentTypePathName.replace("/preview", ""))
return NextResponse.rewrite(
new URL(`/${lang}/preview-current?${searchParams.toString()}`, nextUrl),
{
request: {
headers,
},
}
)
}
searchParams.set("isPreview", "true")
return NextResponse.rewrite(
new URL(
`/${lang}/preview/${contentType}/${uid}?${searchParams.toString()}`,
`/${lang}/${contentType}/${uid}?${searchParams.toString()}`,
nextUrl
),
{