diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index d9d38fe0e..935f1addd 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -1,4 +1,4 @@ -import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts" +import { firaMono, firaSans } from "@/app/fonts" import styles from "./layout.module.css" diff --git a/app/[lang]/(live)/error.tsx b/app/[lang]/(live)/error.tsx index 0cdfc5a7d..b05823085 100644 --- a/app/[lang]/(live)/error.tsx +++ b/app/[lang]/(live)/error.tsx @@ -7,7 +7,7 @@ import { findLang } from "@/constants/languages" import { login } from "@/constants/routes/handleAuth" import { SESSION_EXPIRED } from "@/server/errors/trpc" -import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts" +import { firaMono, firaSans } from "@/app/fonts" import styles from "./error.module.css" diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 40605c7cf..b3e6c387f 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -5,11 +5,10 @@ import Script from "next/script" import TrpcProvider from "@/lib/trpc/Provider" +import { biroScriptPlus, firaMono, firaSans } from "@/app/fonts" import AdobeScript from "@/components/Current/AdobeScript" import VwoScript from "@/components/Current/VwoScript" -import { biroScriptPlus, firaMono, firaSans } from "./fonts" - import type { Metadata } from "next" import type { LangParams, LayoutArgs } from "@/types/params" diff --git a/app/[lang]/(live)/middleware-error/[status]/page.tsx b/app/[lang]/(live)/middleware-error/[status]/page.tsx index d05de5699..24c9b2818 100644 --- a/app/[lang]/(live)/middleware-error/[status]/page.tsx +++ b/app/[lang]/(live)/middleware-error/[status]/page.tsx @@ -1,4 +1,4 @@ -import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts" +import { firaMono, firaSans } from "@/app/fonts" import styles from "./page.module.css" diff --git a/app/[lang]/webview/[contentType]/[uid]/page.tsx b/app/[lang]/webview/[contentType]/[uid]/page.tsx new file mode 100644 index 000000000..8a3fdfe9e --- /dev/null +++ b/app/[lang]/webview/[contentType]/[uid]/page.tsx @@ -0,0 +1,26 @@ +import { notFound } from "next/navigation" + +import AccountPage from "@/components/ContentType/Webviews/AccountPage" +import LoyaltyPage from "@/components/ContentType/Webviews/LoyaltyPage" + +import { + ContentTypeWebviewParams, + LangParams, + PageArgs, + UIDParams, +} from "@/types/params" + +export default async function ContentTypePage({ + params, +}: PageArgs) { + switch (params.contentType) { + case "loyalty-page": + return + case "account-page": + return + default: + const type: never = params.contentType + console.error(`Unsupported content type given: ${type}`) + notFound() + } +} diff --git a/app/[lang]/webview/layout.module.css b/app/[lang]/webview/layout.module.css new file mode 100644 index 000000000..423a25ccc --- /dev/null +++ b/app/[lang]/webview/layout.module.css @@ -0,0 +1,5 @@ +.layout { + font-family: var(--ff-fira-sans); + background-color: var(--Scandic-Brand-Warm-White); + min-height: 100dvh; +} diff --git a/app/[lang]/webview/layout.tsx b/app/[lang]/webview/layout.tsx index 684d28aa8..bab3eb073 100644 --- a/app/[lang]/webview/layout.tsx +++ b/app/[lang]/webview/layout.tsx @@ -1,3 +1,12 @@ +import "@/app/globals.css" +import "@scandic-hotels/design-system/style.css" + +import TrpcProvider from "@/lib/trpc/Provider" + +import { biroScriptPlus, firaMono, firaSans } from "@/app/fonts" + +import styles from "./layout.module.css" + import type { Metadata } from "next" import type { LangParams, LayoutArgs } from "@/types/params" @@ -12,7 +21,11 @@ export default function RootLayout({ }: React.PropsWithChildren>) { return ( - {children} + + {children} + ) } diff --git a/app/[lang]/webview/refresh/page.module.css b/app/[lang]/webview/refresh/page.module.css new file mode 100644 index 000000000..e34ab4222 --- /dev/null +++ b/app/[lang]/webview/refresh/page.module.css @@ -0,0 +1,6 @@ +.container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} diff --git a/app/[lang]/webview/refresh/page.tsx b/app/[lang]/webview/refresh/page.tsx new file mode 100644 index 000000000..d26c6360b --- /dev/null +++ b/app/[lang]/webview/refresh/page.tsx @@ -0,0 +1,11 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +import styles from "./page.module.css" + +export default function Refresh() { + return ( +
+ +
+ ) +} diff --git a/app/[lang]/webview/test/page.tsx b/app/[lang]/webview/test/page.tsx deleted file mode 100644 index e482db7a6..000000000 --- a/app/[lang]/webview/test/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Metadata } from "next" - -export const metadata: Metadata = { - title: "Hello World from Webview", -} - -export default function WebViewTestPage() { - return ( -
-
-

Hello From WebView Test Page!

-
-
- ) -} diff --git a/app/[lang]/(live)/fonts.ts b/app/fonts.ts similarity index 85% rename from app/[lang]/(live)/fonts.ts rename to app/fonts.ts index 73cbdf3a1..ca626579f 100644 --- a/app/[lang]/(live)/fonts.ts +++ b/app/fonts.ts @@ -16,7 +16,7 @@ export const firaSans = Fira_Sans({ export const biroScriptPlus = localFont({ src: [ { - path: "../../../public/_static/fonts/biro-script-plus/Biro-Script-Plus.ttf", + path: "../public/_static/fonts/biro-script-plus/Biro-Script-Plus.ttf", style: "normal", weight: "500", }, diff --git a/app/global-error.tsx b/app/global-error.tsx index 306af9bfe..196810c66 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,6 +1,6 @@ "use client" -import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts" +import { firaMono, firaSans } from "@/app/fonts" import styles from "./global-error.module.css" diff --git a/components/ContentType/Webviews/AccountPage.tsx b/components/ContentType/Webviews/AccountPage.tsx new file mode 100644 index 000000000..d93b28565 --- /dev/null +++ b/components/ContentType/Webviews/AccountPage.tsx @@ -0,0 +1,26 @@ +import "@/app/globals.css" +import "@scandic-hotels/design-system/style.css" + +import { overview } from "@/constants/routes/webviews" +import { serverClient } from "@/lib/trpc/server" + +import MaxWidth from "@/components/MaxWidth" +import Content from "@/components/MyPages/AccountPage/Webview/Content" +import LinkToOverview from "@/components/Webviews/LinkToOverview" + +import styles from "./accountPage.module.css" + +import { LangParams } from "@/types/params" + +export default async function MyPages({ lang }: LangParams) { + const accountPage = await serverClient().contentstack.accountPage.get() + + const linkToOverview = `/${lang}/webview${accountPage.url}` !== overview[lang] + + return ( + + {linkToOverview ? : null} + + + ) +} diff --git a/components/ContentType/Webviews/LoyaltyPage.tsx b/components/ContentType/Webviews/LoyaltyPage.tsx new file mode 100644 index 000000000..12c3e0eb3 --- /dev/null +++ b/components/ContentType/Webviews/LoyaltyPage.tsx @@ -0,0 +1,29 @@ +import { serverClient } from "@/lib/trpc/server" + +import { Blocks } from "@/components/Loyalty/Blocks/WebView" +import Sidebar from "@/components/Loyalty/Sidebar" +import MaxWidth from "@/components/MaxWidth" +import LinkToOverview from "@/components/Webviews/LinkToOverview" + +import styles from "./loyaltyPage.module.css" + +import { LangParams } from "@/types/params" + +export default async function AboutScandicFriends({ lang }: LangParams) { + const loyaltyPage = await serverClient().contentstack.loyaltyPage.get() + return ( +
+ + + {loyaltyPage.sidebar ? ( +
+ +
+ ) : null} + + + + +
+ ) +} diff --git a/components/ContentType/Webviews/accountPage.module.css b/components/ContentType/Webviews/accountPage.module.css new file mode 100644 index 000000000..1144d0dc1 --- /dev/null +++ b/components/ContentType/Webviews/accountPage.module.css @@ -0,0 +1,5 @@ +.blocks { + display: grid; + gap: var(--Spacing-x5); + padding: var(--Spacing-x2); +} diff --git a/components/ContentType/Webviews/loyaltyPage.module.css b/components/ContentType/Webviews/loyaltyPage.module.css new file mode 100644 index 000000000..016506517 --- /dev/null +++ b/components/ContentType/Webviews/loyaltyPage.module.css @@ -0,0 +1,10 @@ +.content { + display: grid; + padding: var(--Spacing-x2); + gap: var(--Spacing-x5); +} + +.sidebar { + margin-left: calc(var(--Spacing-x2) * -1); + margin-right: calc(var(--Spacing-x2) * -1); +} diff --git a/components/LoadingSpinner/loading.module.css b/components/LoadingSpinner/loading.module.css index dd10b4d09..ae97df077 100644 --- a/components/LoadingSpinner/loading.module.css +++ b/components/LoadingSpinner/loading.module.css @@ -26,7 +26,7 @@ width: 3px; height: 9px; border-radius: 20%; - background: var(--Brand-Main-Strong); + background: var(--Scandic-Brand-Burgundy); } .spinner div:nth-child(1) { diff --git a/components/Loyalty/Blocks/WebView/index.tsx b/components/Loyalty/Blocks/WebView/index.tsx new file mode 100644 index 000000000..420e8fff5 --- /dev/null +++ b/components/Loyalty/Blocks/WebView/index.tsx @@ -0,0 +1,54 @@ +import JsonToHtml from "@/components/JsonToHtml" +import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent" +import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" +import { modWebviewLink } from "@/utils/webviews" + +import CardsGrid from "../CardsGrid" + +import type { BlocksProps } from "@/types/components/loyalty/blocks" +import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums" +import { LangParams } from "@/types/params" + +export function Blocks({ lang, blocks }: BlocksProps & LangParams) { + return blocks.map((block) => { + switch (block.__typename) { + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid: + return + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent: + return ( +
+ +
+ ) + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent: + const dynamicContent = { + ...block.dynamic_content, + link: block.dynamic_content.link + ? { + ...block.dynamic_content.link, + href: modWebviewLink(block.dynamic_content.link.href, lang), + } + : undefined, + } + + return + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts: + const shortcuts = block.shortcuts.shortcuts.map((shortcut) => ({ + ...shortcut, + url: modWebviewLink(shortcut.url, lang), + })) + return ( + + ) + default: + return null + } + }) +} diff --git a/components/MyPages/AccountPage/Webview/Content.tsx b/components/MyPages/AccountPage/Webview/Content.tsx new file mode 100644 index 000000000..64bbf898b --- /dev/null +++ b/components/MyPages/AccountPage/Webview/Content.tsx @@ -0,0 +1,101 @@ +import JsonToHtml from "@/components/JsonToHtml" +import Overview from "@/components/MyPages/Blocks/Overview" +import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" +import { modWebviewLink } from "@/utils/webviews" + +import CurrentBenefitsBlock from "../../Blocks/Benefits/CurrentLevel" +import NextLevelBenefitsBlock from "../../Blocks/Benefits/NextLevel" +import CurrentPointsBalance from "../../Blocks/Points/CurrentPointsBalance" +import EarnAndBurn from "../../Blocks/Points/EarnAndBurn" + +import { + AccountPageContentProps, + ContentProps, +} from "@/types/components/myPages/myPage/accountPage" +import { + ContentEntries, + DynamicContentComponents, +} from "@/types/components/myPages/myPage/enums" + +function DynamicComponent({ component, props }: AccountPageContentProps) { + switch (component) { + case DynamicContentComponents.membership_overview: + return + case DynamicContentComponents.current_benefits: + return + case DynamicContentComponents.next_benefits: + return + case DynamicContentComponents.my_points: + return + case DynamicContentComponents.expiring_points: + // TODO: Add once available + // return + return null + case DynamicContentComponents.earn_and_burn: + return + default: + return null + } +} + +export default function Content({ lang, content }: ContentProps) { + return ( + <> + {content.map((item) => { + switch (item.__typename) { + case ContentEntries.AccountPageContentDynamicContent: + const link = item.dynamic_content.link.linkConnection.edges.length + ? { + href: + item.dynamic_content.link.linkConnection.edges[0].node + .original_url || + `/${lang}/webview${item.dynamic_content.link.linkConnection.edges[0].node.url}`, + text: item.dynamic_content.link.link_text, + } + : null + + const componentProps = { + lang, + title: item.dynamic_content.title, + // TODO: rename preamble to subtitle in Contentstack? + subtitle: item.dynamic_content.preamble, + ...(link && { link }), + } + return ( + + ) + case ContentEntries.AccountPageContentShortcuts: + const shortcuts = item.shortcuts.shortcuts.map((shortcut) => { + return { + ...shortcut, + url: modWebviewLink(shortcut.url, lang), + } + }) + return ( + + ) + case ContentEntries.AccountPageContentTextContent: + return ( +
+ +
+ ) + default: + return null + } + })} + + ) +} diff --git a/components/MyPages/Header/User/index.tsx b/components/MyPages/Header/User/index.tsx deleted file mode 100644 index 0a6e97b5b..000000000 --- a/components/MyPages/Header/User/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { serverClient } from "@/lib/trpc/server" - -import styles from "./user.module.css" - -export default async function User() { - const user = await serverClient().user.get() - - return ( -
- {user.firstName[0].toUpperCase()} - {user.lastName[0].toUpperCase()} - 1 -
- ) -} diff --git a/components/MyPages/Header/User/user.module.css b/components/MyPages/Header/User/user.module.css deleted file mode 100644 index c93b73f11..000000000 --- a/components/MyPages/Header/User/user.module.css +++ /dev/null @@ -1,43 +0,0 @@ -.user { - align-items: center; - background-color: var(--some-black-color, #000); - border-radius: 50%; - color: var(--some-white-color, #fff); - display: flex; - font-family: var(--ff-fira-sans); - font-size: 1.2rem; - font-weight: 600; - height: 3.5rem; - justify-content: center; - position: relative; - width: 3.5rem; -} - -.alert { - align-items: center; - background-color: var(--some-red-color, #ed2027); - border-radius: 50%; - display: flex; - font-size: 1rem; - height: 2rem; - justify-content: center; - position: absolute; - right: -1rem; - top: -0.5rem; - width: 2rem; -} - -@media screen and (min-width: 950px) { - .user { - height: 2.8rem; - width: 2.8rem; - } - - .alert { - font-size: 0.6rem; - height: 1rem; - right: -0.2rem; - top: -0.1rem; - width: 1rem; - } -} diff --git a/components/Webviews/LinkToOverview/index.tsx b/components/Webviews/LinkToOverview/index.tsx new file mode 100644 index 000000000..206a5db87 --- /dev/null +++ b/components/Webviews/LinkToOverview/index.tsx @@ -0,0 +1,18 @@ +import { ArrowLeft } from "react-feather" + +import { overview } from "@/constants/routes/webviews" +import { _ } from "@/lib/translation" + +import Link from "@/components/TempDesignSystem/Link" + +import styles from "./linkToOverview.module.css" + +import { LangParams } from "@/types/params" + +export default function LinkToOverview({ lang }: LangParams) { + return ( + + {_("Go back to overview")} + + ) +} diff --git a/components/Webviews/LinkToOverview/linkToOverview.module.css b/components/Webviews/LinkToOverview/linkToOverview.module.css new file mode 100644 index 000000000..83ab1619e --- /dev/null +++ b/components/Webviews/LinkToOverview/linkToOverview.module.css @@ -0,0 +1,7 @@ +.overviewLink { + font-size: var(--Spacing-x2); + color: var(--Scandic-Brand-Burgundy, #4d001b); + display: flex; + align-items: center; + gap: 1rem; +} diff --git a/constants/routes/webviews.ts b/constants/routes/webviews.ts new file mode 100644 index 000000000..4100ead0f --- /dev/null +++ b/constants/routes/webviews.ts @@ -0,0 +1,71 @@ +const myPages = { + da: "/da/webview/mine-sider", + de: "/de/webview/mein-profil", + en: "/en/webview/my-pages", + fi: "/fi/webview/minun-sivujani", + no: "/no/webview/mine-sider", + sv: "/sv/webview/mina-sidor", +} + +export const overview = { + da: `${myPages.da}/oversigt`, + de: `${myPages.de}/uberblick`, + en: `${myPages.en}/overview`, + fi: `${myPages.fi}/yleiskatsaus`, + no: `${myPages.no}/oversikt`, + sv: `${myPages.sv}/oversikt`, +} + +export const benefits = { + da: `${myPages.da}/fordele`, + de: `${myPages.de}/vorteile`, + en: `${myPages.en}/benefits`, + fi: `${myPages.fi}/etuja`, + no: `${myPages.no}/fordeler`, + sv: `${myPages.sv}/formaner`, +} + +export const points = { + da: `${myPages.da}/points`, + de: `${myPages.de}/points`, + en: `${myPages.en}/points`, + fi: `${myPages.fi}/points`, + no: `${myPages.no}/points`, + sv: `${myPages.sv}/points`, +} + +export const programOverview = { + da: `/da/webview/about-scandic-friends`, + de: `/de/webview/about-scandic-friends`, + en: `/en/webview/about-scandic-friends`, + fi: `/fi/webview/about-scandic-friends`, + no: `/no/webview/om-scandic-friends`, + sv: `/sv/webview/om-scandic-friends`, +} + +const refreshUrl = { + da: `/da/webview/refresh`, + de: `/de/webview/refresh`, + en: `/en/webview/refresh`, + fi: `/fi/webview/refresh`, + no: `/no/webview/refresh`, + sv: `/sv/webview/refresh`, +} + +export const webviews = [ + ...Object.values(benefits), + ...Object.values(overview), + ...Object.values(points), + ...Object.values(programOverview), + ...Object.values(refreshUrl), +] + +export const myPagesWebviews = [ + ...Object.values(benefits), + ...Object.values(overview), + ...Object.values(points), +] + +export const loyaltyPagesWebviews = [...Object.values(programOverview)] + +export const refreshWebviews = [...Object.values(refreshUrl)] diff --git a/lib/graphql/Query/LoyaltyPage.graphql b/lib/graphql/Query/LoyaltyPage.graphql index 0ea366416..231c0aa68 100644 --- a/lib/graphql/Query/LoyaltyPage.graphql +++ b/lib/graphql/Query/LoyaltyPage.graphql @@ -127,15 +127,6 @@ query GetLoyaltyPage($locale: String!, $uid: String!) { } } } - web { - breadcrumbs { - title - parents { - href - title - } - } - } system { uid created_at diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index 77b34c41d..a6467fb91 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server" import { redirect } from "next/navigation" import { Lang } from "@/constants/languages" +import { webviews } from "@/constants/routes/webviews" import { appRouter } from "@/server" import { createContext } from "@/server/context" import { internalServerError } from "@/server/errors/next" @@ -21,8 +22,28 @@ export function serverClient() { if (error instanceof TRPCError) { if (error.code === "UNAUTHORIZED") { - const lang = ctx?.lang || Lang.en - const pathname = ctx?.pathname || "/" + let lang = Lang.en + let pathname = "/" + let fullUrl = "/" + + if (ctx) { + lang = ctx.lang + pathname = ctx.pathname + fullUrl = ctx.url + } + + const fullPathname = new URL(fullUrl).pathname + + if (webviews.includes(fullPathname)) { + const redirectUrl = `/${lang}/webview/refresh?returnurl=${encodeURIComponent(fullUrl)}` + console.error( + "Unautorized in webview, redirecting to: ", + redirectUrl + ) + + redirect(redirectUrl) + } + redirect( `/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}` ) diff --git a/middlewares/utils.ts b/middlewares/utils.ts index 209dbf5c8..feba8f258 100644 --- a/middlewares/utils.ts +++ b/middlewares/utils.ts @@ -7,7 +7,10 @@ export function getDefaultRequestHeaders(request: NextRequest) { const headers = new Headers(request.headers) headers.set("x-lang", lang) - headers.set("x-pathname", request.nextUrl.pathname.replace(`/${lang}`, "")) + headers.set( + "x-pathname", + request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "") + ) headers.set("x-url", request.nextUrl.href) return headers diff --git a/middlewares/webView.ts b/middlewares/webView.ts index 7b6fadee6..d97d0f74c 100644 --- a/middlewares/webView.ts +++ b/middlewares/webView.ts @@ -1,48 +1,129 @@ import { type NextMiddleware, NextResponse } from "next/server" import { findLang } from "@/constants/languages" +import { + loyaltyPagesWebviews, + myPagesWebviews, + refreshWebviews, + webviews, +} from "@/constants/routes/webviews" import { env } from "@/env/server" -import { badRequest, internalServerError } from "@/server/errors/next" +import { badRequest, notFound } from "@/server/errors/next" import { decryptData } from "@/utils/aes" +import { resolve as resolveEntry } from "@/utils/entry" + +import { getDefaultRequestHeaders } from "./utils" import type { MiddlewareMatcher } from "@/types/middleware" export const middleware: NextMiddleware = async (request) => { + const { nextUrl } = request + const lang = findLang(nextUrl.pathname) + + // If user is redirected to /lang/webview/refresh/, the webview token is invalid and we remove the cookie + if (refreshWebviews.includes(nextUrl.pathname)) { + return NextResponse.rewrite( + new URL( + `/${lang}/webview/refresh?${nextUrl.searchParams.toString()}`, + nextUrl + ), + { + headers: { + "Set-Cookie": `webviewToken=0; Max-Age=0; Secure; HttpOnly; Path=/; SameSite=Strict;`, + }, + } + ) + } + + const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}/webview`, "") + + const { uid } = await resolveEntry(pathNameWithoutLang, lang) + if (!uid) { + throw notFound( + `Unable to resolve CMS entry for locale "${lang}": ${pathNameWithoutLang}` + ) + } + const headers = getDefaultRequestHeaders(request) + headers.set("x-uid", uid) + const webviewToken = request.cookies.get("webviewToken") if (webviewToken) { // since the token exists, this is a subsequent visit // we're done, allow it - return NextResponse.next() - } - - // Authorization header is required for webviews - // It should be base64 encoded - const authorization = request.headers.get("Authorization")! - if (!authorization) { - return badRequest() - } - - // Initialization vector header is required for webviews - // It should be base64 encoded - const initializationVector = request.headers.get("X-AES-IV")! - if (!initializationVector) { - return badRequest() + if (myPagesWebviews.includes(nextUrl.pathname)) { + return NextResponse.rewrite( + new URL(`/${lang}/webview/account-page/${uid}`, nextUrl), + { + request: { + headers, + }, + } + ) + } else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) { + return NextResponse.rewrite( + new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl), + { + request: { + headers, + }, + } + ) + } else { + return notFound() + } } try { + // Authorization header is required for webviews + // It should be base64 encoded + const authorization = request.headers.get("X-Authorization")! + if (!authorization) { + console.error("Authorization header is missing") + return badRequest("Authorization header is missing") + } + + // Initialization vector header is required for webviews + // It should be base64 encoded + const initializationVector = request.headers.get("X-AES-IV")! + if (!initializationVector) { + console.error("initializationVector header is missing") + return badRequest("initializationVector header is missing") + } + const decryptedData = await decryptData( env.WEBVIEW_ENCRYPTION_KEY, initializationVector, authorization ) - // Pass the webview token via cookie to the page - return NextResponse.next({ - headers: { - "Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly;`, - }, - }) + headers.append("Cookie", `webviewToken=${decryptedData}`) + + if (myPagesWebviews.includes(nextUrl.pathname)) { + return NextResponse.rewrite( + new URL(`/${lang}/webview/account-page/${uid}`, nextUrl), + { + headers: { + "Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`, + }, + request: { + headers, + }, + } + ) + } else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) { + return NextResponse.rewrite( + new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl), + { + headers: { + "Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`, + }, + request: { + headers, + }, + } + ) + } } catch (e) { if (e instanceof Error) { console.error(`${e.name}: ${e.message}`) @@ -54,7 +135,6 @@ export const middleware: NextMiddleware = async (request) => { export const matcher: MiddlewareMatcher = (request) => { const { nextUrl } = request - const lang = findLang(nextUrl.pathname) - const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "") - return pathNameWithoutLang.startsWith("/webview/") + + return webviews.includes(nextUrl.pathname) } diff --git a/server/context.ts b/server/context.ts index c81fe1024..e58e1ba6f 100644 --- a/server/context.ts +++ b/server/context.ts @@ -1,15 +1,21 @@ -import { headers } from "next/headers" +import { cookies, headers } from "next/headers" +import { type Session } from "next-auth" import { Lang } from "@/constants/languages" import { auth } from "@/auth" +import { unauthorizedError } from "./errors/trpc" + +typeof auth + type CreateContextOptions = { - auth: typeof auth + auth: () => Promise lang: Lang pathname: string uid?: string | null url: string + webToken?: string } /** Use this helper for: @@ -23,6 +29,7 @@ export function createContextInner(opts: CreateContextOptions) { pathname: opts.pathname, uid: opts.uid, url: opts.url, + webToken: opts.webToken, } } @@ -33,12 +40,24 @@ export function createContextInner(opts: CreateContextOptions) { export function createContext() { const h = headers() + const cookie = cookies() + const webviewTokenCookie = cookie.get("webviewToken") + return createContextInner({ - auth, + auth: async () => { + const session = await auth() + const webToken = webviewTokenCookie?.value + if (!session?.token && !webToken) { + throw unauthorizedError() + } + + return session || ({ token: { access_token: webToken } } as Session) + }, lang: h.get("x-lang") as Lang, pathname: h.get("x-pathname")!, uid: h.get("x-uid"), url: h.get("x-url")!, + webToken: webviewTokenCookie?.value, }) } diff --git a/server/trpc.ts b/server/trpc.ts index e09d79215..e29f5e477 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -30,7 +30,6 @@ export const contentstackProcedure = t.procedure.use(async function (opts) { export const protectedProcedure = t.procedure.use(async function (opts) { const authRequired = opts.meta?.authRequired ?? true const session = await opts.ctx.auth() - if (!authRequired && env.NODE_ENV === "development") { console.info( `❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌` @@ -42,10 +41,6 @@ export const protectedProcedure = t.procedure.use(async function (opts) { throw sessionExpiredError() } - if (!session?.user) { - throw unauthorizedError() - } - return opts.next({ ctx: { session, diff --git a/types/params.ts b/types/params.ts index da6dd75b9..0ea8f134a 100644 --- a/types/params.ts +++ b/types/params.ts @@ -20,6 +20,10 @@ export type ContentTypeParams = { contentType: "loyalty-page" | "content-page" } +export type ContentTypeWebviewParams = { + contentType: "loyalty-page" | "account-page" +} + export type UIDParams = { uid: string } diff --git a/utils/webviews.ts b/utils/webviews.ts new file mode 100644 index 000000000..e6c28b90d --- /dev/null +++ b/utils/webviews.ts @@ -0,0 +1,13 @@ +import { Lang } from "@/constants/languages" +import { webviews } from "@/constants/routes/webviews" + +export function modWebviewLink(url: string, lang: Lang) { + const urlWithoutLang = url.replace(`/${lang}`, "") + + const webviewUrl = `/${lang}/webview${urlWithoutLang}` + if (webviews.includes(webviewUrl) || url.startsWith("/webview")) { + return webviewUrl + } else { + return url + } +}