fix: redirect users to /refresh on unauth and mod webview links
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import { notFound, redirect } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import { Blocks } from "@/components/Loyalty/Blocks"
|
import { Blocks } from "@/components/Loyalty/Blocks/WebView"
|
||||||
import Sidebar from "@/components/Loyalty/Sidebar"
|
import Sidebar from "@/components/Loyalty/Sidebar"
|
||||||
import MaxWidth from "@/components/MaxWidth"
|
import MaxWidth from "@/components/MaxWidth"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import type { LangParams, PageArgs, UriParams } from "@/types/params"
|
import { LangParams, PageArgs, UriParams } from "@/types/params"
|
||||||
|
|
||||||
export default async function AboutScandicFriends({
|
export default async function AboutScandicFriends({
|
||||||
params,
|
params,
|
||||||
@@ -18,26 +18,16 @@ export default async function AboutScandicFriends({
|
|||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loyaltyPage = await serverClient({
|
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get({
|
||||||
onError() {
|
|
||||||
const returnUrl = new URLSearchParams({
|
|
||||||
returnurl: `${params.lang}/webview/${searchParams.uri}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const refreshUrl = `/${params.lang}/webview/refresh?${returnUrl.toString()}`
|
|
||||||
redirect(refreshUrl)
|
|
||||||
},
|
|
||||||
}).contentstack.loyaltyPage.get({
|
|
||||||
href: searchParams.uri,
|
href: searchParams.uri,
|
||||||
locale: params.lang,
|
locale: params.lang,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
{loyaltyPage.sidebar ? <Sidebar blocks={loyaltyPage.sidebar} /> : null}
|
{loyaltyPage.sidebar ? <Sidebar blocks={loyaltyPage.sidebar} /> : null}
|
||||||
|
|
||||||
<MaxWidth className={styles.blocks} tag="main">
|
<MaxWidth className={styles.blocks} tag="main">
|
||||||
<Blocks blocks={loyaltyPage.blocks} />
|
<Blocks blocks={loyaltyPage.blocks} lang={params.lang} />
|
||||||
</MaxWidth>
|
</MaxWidth>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import "@/app/globals.css"
|
import "@/app/globals.css"
|
||||||
import "@scandic-hotels/design-system/style.css"
|
import "@scandic-hotels/design-system/style.css"
|
||||||
|
|
||||||
import { notFound, redirect } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import { overview } from "@/constants/routes/webviews"
|
|
||||||
import { _ } from "@/lib/translation"
|
import { _ } from "@/lib/translation"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
@@ -13,21 +11,7 @@ import Content from "@/components/MyPages/AccountPage/Webview/Content"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import type { LangParams, PageArgs, UriParams } from "@/types/params"
|
import { LangParams, PageArgs, UriParams } from "@/types/params"
|
||||||
|
|
||||||
function getLink(lang: Lang, uri: string) {
|
|
||||||
if (uri === overview[lang]) {
|
|
||||||
return {
|
|
||||||
title: _("Go to points"),
|
|
||||||
href: `/${lang}/webview/my-pages/points`,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: _("Go to membership overview"),
|
|
||||||
href: `/${lang}/webview/my-pages/overview`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function MyPages({
|
export default async function MyPages({
|
||||||
params,
|
params,
|
||||||
@@ -37,34 +21,11 @@ export default async function MyPages({
|
|||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the access token is valid. If not, redirect to the refresh page.
|
const accountPage = await serverClient().contentstack.accountPage.get({
|
||||||
await serverClient({
|
|
||||||
onError(opts) {
|
|
||||||
const returnUrl = new URLSearchParams({
|
|
||||||
returnurl: `${params.lang}/webview/${searchParams.uri}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const refreshUrl = `/${params.lang}/webview/refresh?${returnUrl.toString()}`
|
|
||||||
redirect(refreshUrl)
|
|
||||||
},
|
|
||||||
}).user.get()
|
|
||||||
|
|
||||||
const accountPage = await serverClient({
|
|
||||||
onError() {
|
|
||||||
const returnUrl = new URLSearchParams({
|
|
||||||
returnurl: `${params.lang}/webview/${searchParams.uri}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const refreshUrl = `/${params.lang}/webview/refresh?${returnUrl.toString()}`
|
|
||||||
redirect(refreshUrl)
|
|
||||||
},
|
|
||||||
}).contentstack.accountPage.get({
|
|
||||||
url: searchParams.uri,
|
url: searchParams.uri,
|
||||||
lang: params.lang,
|
lang: params.lang,
|
||||||
})
|
})
|
||||||
|
|
||||||
const link = getLink(params.lang, searchParams.uri)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaxWidth className={styles.blocks} tag="main">
|
<MaxWidth className={styles.blocks} tag="main">
|
||||||
<Content lang={params.lang} content={accountPage.content} />
|
<Content lang={params.lang} content={accountPage.content} />
|
||||||
|
|||||||
3
app/[lang]/webview/refresh/page.tsx
Normal file
3
app/[lang]/webview/refresh/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Refresh() {
|
||||||
|
return <div>Hey you've been refreshed</div>
|
||||||
|
}
|
||||||
66
components/Loyalty/Blocks/WebView/index.tsx
Normal file
66
components/Loyalty/Blocks/WebView/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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 CardGrid from "../CardGrid"
|
||||||
|
|
||||||
|
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.LoyaltyPageBlocksCardGrid:
|
||||||
|
const cardGrid = {
|
||||||
|
...block.card_grid,
|
||||||
|
cards: block.card_grid.cards.map((card) => {
|
||||||
|
return {
|
||||||
|
...card,
|
||||||
|
link: card.link
|
||||||
|
? { ...card.link, href: modWebviewLink(card.link.href, lang) }
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CardGrid card_grid={cardGrid} />
|
||||||
|
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<JsonToHtml
|
||||||
|
nodes={block.content.content.json.children}
|
||||||
|
embeds={block.content.content.embedded_itemsConnection.edges}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
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 <DynamicContentBlock dynamicContent={dynamicContent} />
|
||||||
|
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
|
||||||
|
const shortcuts = block.shortcuts.shortcuts.map((shortcut) => ({
|
||||||
|
...shortcut,
|
||||||
|
url: modWebviewLink(shortcut.url, lang),
|
||||||
|
}))
|
||||||
|
return (
|
||||||
|
<Shortcuts
|
||||||
|
shortcuts={shortcuts}
|
||||||
|
title={block.shortcuts.title}
|
||||||
|
subtitle={block.shortcuts.preamble}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import JsonToHtml from "@/components/JsonToHtml"
|
import JsonToHtml from "@/components/JsonToHtml"
|
||||||
import Overview from "@/components/MyPages/Blocks/Overview"
|
import Overview from "@/components/MyPages/Blocks/Overview"
|
||||||
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
|
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
|
||||||
|
import { modWebviewLink } from "@/utils/webviews"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AccountPageContentProps,
|
AccountPageContentProps,
|
||||||
@@ -31,7 +32,7 @@ export default function Content({ lang, content }: ContentProps) {
|
|||||||
href:
|
href:
|
||||||
item.dynamic_content.link.linkConnection.edges[0].node
|
item.dynamic_content.link.linkConnection.edges[0].node
|
||||||
.original_url ||
|
.original_url ||
|
||||||
`/${lang}${item.dynamic_content.link.linkConnection.edges[0].node.url}`,
|
`/${lang}/webview${item.dynamic_content.link.linkConnection.edges[0].node.url}`,
|
||||||
text: item.dynamic_content.link.link_text,
|
text: item.dynamic_content.link.link_text,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -50,9 +51,15 @@ export default function Content({ lang, content }: ContentProps) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case ContentEntries.AccountPageContentShortcuts:
|
case ContentEntries.AccountPageContentShortcuts:
|
||||||
|
const shortcuts = item.shortcuts.shortcuts.map((shortcut) => {
|
||||||
|
return {
|
||||||
|
...shortcut,
|
||||||
|
url: modWebviewLink(shortcut.url, lang),
|
||||||
|
}
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<Shortcuts
|
<Shortcuts
|
||||||
shortcuts={item.shortcuts.shortcuts}
|
shortcuts={shortcuts}
|
||||||
subtitle={item.shortcuts.preamble}
|
subtitle={item.shortcuts.preamble}
|
||||||
title={item.shortcuts.title}
|
title={item.shortcuts.title}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
import { _ } from "@/lib/translation"
|
import { _ } from "@/lib/translation"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
@@ -6,8 +7,17 @@ import BreadcrumbsWithLink from "./BreadcrumbWithLink"
|
|||||||
|
|
||||||
import styles from "./breadcrumbs.module.css"
|
import styles from "./breadcrumbs.module.css"
|
||||||
|
|
||||||
export default async function Breadcrumbs() {
|
export default async function Breadcrumbs({
|
||||||
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
|
href,
|
||||||
|
locale,
|
||||||
|
}: {
|
||||||
|
href: string
|
||||||
|
locale: Lang
|
||||||
|
}) {
|
||||||
|
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
|
||||||
|
href,
|
||||||
|
locale,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.breadcrumbs}>
|
<nav className={styles.breadcrumbs}>
|
||||||
|
|||||||
16
components/MyPages/Header/Hamburger/hamburger.module.css
Normal file
16
components/MyPages/Header/Hamburger/hamburger.module.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.hamburger {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
background-color: var(--some-black-color, #1c1b1f);
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
11
components/MyPages/Header/Hamburger/index.tsx
Normal file
11
components/MyPages/Header/Hamburger/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import styles from "./hamburger.module.css"
|
||||||
|
|
||||||
|
export default function Hamburger() {
|
||||||
|
return (
|
||||||
|
<button className={styles.hamburger} type="button">
|
||||||
|
<div className={styles.line} />
|
||||||
|
<div className={styles.line} />
|
||||||
|
<div className={styles.line} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/MyPages/Header/LanguageSwitcher/index.tsx
Normal file
17
components/MyPages/Header/LanguageSwitcher/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import styles from "./language.module.css"
|
||||||
|
|
||||||
|
export default function LanguageSwitcher() {
|
||||||
|
return (
|
||||||
|
<div className={styles.switcher}>
|
||||||
|
<Image
|
||||||
|
alt="Swedish flag"
|
||||||
|
height={21}
|
||||||
|
src="/_static/icons/sweden.svg"
|
||||||
|
width={21}
|
||||||
|
/>
|
||||||
|
<span>SV / SEK</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.switcher {
|
||||||
|
align-items: center;
|
||||||
|
display: none;
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
gap: 0.6rem;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 950px) {
|
||||||
|
.switcher {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
components/MyPages/Header/Logo/index.tsx
Normal file
34
components/MyPages/Header/Logo/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
import { GetMyPagesLogo } from "@/lib/graphql/Query/Logo.graphql"
|
||||||
|
import { request } from "@/lib/graphql/request"
|
||||||
|
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import styles from "./logo.module.css"
|
||||||
|
|
||||||
|
import type { LangParams } from "@/types/params"
|
||||||
|
import type { LogoQueryData } from "@/types/requests/myPages/logo"
|
||||||
|
|
||||||
|
export default async function Logo({ lang }: LangParams) {
|
||||||
|
const { data } = await request<LogoQueryData>(GetMyPagesLogo, {
|
||||||
|
locale: lang,
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
!data.all_header.items.length ||
|
||||||
|
!data.all_header.items?.[0].logoConnection.totalCount
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const logo = data.all_header.items[0].logoConnection.edges[0]
|
||||||
|
return (
|
||||||
|
<Link className={styles.link} href="#">
|
||||||
|
<Image
|
||||||
|
alt={logo.node.title}
|
||||||
|
height={logo.node.dimension.height}
|
||||||
|
src={logo.node.url}
|
||||||
|
width={logo.node.dimension.width}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
components/MyPages/Header/Logo/logo.module.css
Normal file
4
components/MyPages/Header/Logo/logo.module.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.link {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
25
components/MyPages/Header/header.module.css
Normal file
25
components/MyPages/Header/header.module.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
box-shadow: 0px 1.0006656646728516px 1.0006656646728516px 0px #0000000d;
|
||||||
|
display: grid;
|
||||||
|
gap: 3rem;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
height: var(--header-height);
|
||||||
|
|
||||||
|
padding: 0 2rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 950px) {
|
||||||
|
.header {
|
||||||
|
background-color: var(--some-grey-color, #ececec);
|
||||||
|
border-bottom: 0.1rem solid var(--some-grey-color, #ccc);
|
||||||
|
box-shadow: none;
|
||||||
|
gap: 3.2rem;
|
||||||
|
grid-template-columns: 1fr 19rem auto auto;
|
||||||
|
padding: 0 2.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
components/MyPages/Header/index.tsx
Normal file
19
components/MyPages/Header/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Hamburger from "./Hamburger"
|
||||||
|
import LanguageSwitcher from "./LanguageSwitcher"
|
||||||
|
import Logo from "./Logo"
|
||||||
|
import User from "./User"
|
||||||
|
|
||||||
|
import styles from "./header.module.css"
|
||||||
|
|
||||||
|
import type { LangParams } from "@/types/params"
|
||||||
|
|
||||||
|
export default function Header({ lang }: LangParams) {
|
||||||
|
return (
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Logo lang={lang} />
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<User />
|
||||||
|
<Hamburger />
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,11 +57,22 @@ export const programOverview = {
|
|||||||
sv: `/sv/webview/om-scandic-friends`,
|
sv: `/sv/webview/om-scandic-friends`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
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 = [
|
export const webviews = [
|
||||||
...Object.values(benefits),
|
...Object.values(benefits),
|
||||||
...Object.values(overview),
|
...Object.values(overview),
|
||||||
...Object.values(points),
|
...Object.values(points),
|
||||||
...Object.values(programOverview),
|
...Object.values(programOverview),
|
||||||
|
...Object.values(refreshUrl),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const myPagesWebviews = [
|
export const myPagesWebviews = [
|
||||||
@@ -71,3 +82,5 @@ export const myPagesWebviews = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const loyaltyPagesWebviews = [...Object.values(programOverview)]
|
export const loyaltyPagesWebviews = [...Object.values(programOverview)]
|
||||||
|
|
||||||
|
export const refreshWebviews = [...Object.values(refreshUrl)]
|
||||||
|
|||||||
16
lib/graphql/Query/Logo.graphql
Normal file
16
lib/graphql/Query/Logo.graphql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#import "../Fragments/Image.graphql"
|
||||||
|
|
||||||
|
query GetMyPagesLogo($locale: String!) {
|
||||||
|
all_header(limit: 1, locale: $locale) {
|
||||||
|
items {
|
||||||
|
logoConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server"
|
import { TRPCError } from "@trpc/server"
|
||||||
|
import { headers } from "next/headers"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
@@ -22,6 +23,16 @@ export function serverClient() {
|
|||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
if (error.code === "UNAUTHORIZED") {
|
if (error.code === "UNAUTHORIZED") {
|
||||||
const lang = ctx?.lang || Lang.en
|
const lang = ctx?.lang || Lang.en
|
||||||
|
if (ctx?.webToken) {
|
||||||
|
const returnUrl = ctx.url
|
||||||
|
const redirectUrl = `/${lang}/webview/refresh?returnurl=${encodeURIComponent(returnUrl)}`
|
||||||
|
console.error(
|
||||||
|
"Unautorized in webview, redirecting to: ",
|
||||||
|
redirectUrl
|
||||||
|
)
|
||||||
|
redirect(redirectUrl)
|
||||||
|
}
|
||||||
|
|
||||||
const pathname = ctx?.pathname || "/"
|
const pathname = ctx?.pathname || "/"
|
||||||
redirect(
|
redirect(
|
||||||
`/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
|
`/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
|
|
||||||
return NextResponse.rewrite(
|
return NextResponse.rewrite(
|
||||||
new URL(
|
new URL(
|
||||||
`/${lang}/preview/${contentType}/${uid}?${searchParams.toString()}`,
|
`/${lang}/preview/${contentType}?${searchParams.toString()}`,
|
||||||
nextUrl
|
nextUrl
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
@@ -53,6 +53,7 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchParams.set("uri", pathNameWithoutLang)
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
searchParams.set("uri", pathNameWithoutLang)
|
searchParams.set("uri", pathNameWithoutLang)
|
||||||
return NextResponse.rewrite(
|
return NextResponse.rewrite(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { findLang } from "@/constants/languages"
|
|||||||
import {
|
import {
|
||||||
loyaltyPagesWebviews,
|
loyaltyPagesWebviews,
|
||||||
myPagesWebviews,
|
myPagesWebviews,
|
||||||
|
refreshWebviews,
|
||||||
webviews,
|
webviews,
|
||||||
} from "@/constants/routes/webviews"
|
} from "@/constants/routes/webviews"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
@@ -19,9 +20,21 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
const lang = findLang(nextUrl.pathname)
|
const lang = findLang(nextUrl.pathname)
|
||||||
|
|
||||||
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}/webview`, "")
|
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}/webview`, "")
|
||||||
|
const headers = new Headers()
|
||||||
|
|
||||||
|
// If user is redirected to /lang/webview/refresh/, the webview token is invalid and we remove the cookie
|
||||||
|
if (refreshWebviews.includes(nextUrl.pathname)) {
|
||||||
|
headers.set(
|
||||||
|
"Set-Cookie",
|
||||||
|
`webviewToken=0; Max-Age=0; Secure; HttpOnly; Path=/; SameSite=Strict;`
|
||||||
|
)
|
||||||
|
return NextResponse.rewrite(new URL(`/${lang}/webview/refresh`, nextUrl), {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
|
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
|
||||||
searchParams.set("uri", pathNameWithoutLang)
|
searchParams.set("uri", pathNameWithoutLang)
|
||||||
|
|
||||||
const webviewToken = request.cookies.get("webviewToken")
|
const webviewToken = request.cookies.get("webviewToken")
|
||||||
if (webviewToken) {
|
if (webviewToken) {
|
||||||
// since the token exists, this is a subsequent visit
|
// since the token exists, this is a subsequent visit
|
||||||
@@ -42,27 +55,33 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
const decryptedData = await decryptData(
|
const decryptedData = await decryptData(
|
||||||
env.WEBVIEW_ENCRYPTION_KEY,
|
env.WEBVIEW_ENCRYPTION_KEY,
|
||||||
initializationVector,
|
initializationVector,
|
||||||
authorization
|
authorization
|
||||||
)
|
)
|
||||||
|
|
||||||
|
headers.set(
|
||||||
|
"Set-Cookie",
|
||||||
|
`webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`
|
||||||
|
)
|
||||||
|
headers.set("Cookie", `webviewToken=${decryptedData}`)
|
||||||
|
|
||||||
if (myPagesWebviews.includes(nextUrl.pathname)) {
|
if (myPagesWebviews.includes(nextUrl.pathname)) {
|
||||||
return NextResponse.rewrite(
|
return NextResponse.rewrite(
|
||||||
new URL(
|
new URL(
|
||||||
@@ -70,10 +89,7 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
nextUrl
|
nextUrl
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
headers: {
|
headers,
|
||||||
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
|
|
||||||
Cookie: `webviewToken=${decryptedData}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
|
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
|
||||||
@@ -83,10 +99,7 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
nextUrl
|
nextUrl
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
headers: {
|
headers,
|
||||||
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
|
|
||||||
Cookie: `webviewToken=${decryptedData}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { headers } from "next/headers"
|
import { cookies, headers } from "next/headers"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ type CreateContextOptions = {
|
|||||||
pathname: string
|
pathname: string
|
||||||
uid?: string | null
|
uid?: string | null
|
||||||
url: string
|
url: string
|
||||||
|
webToken: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Use this helper for:
|
/** Use this helper for:
|
||||||
@@ -23,6 +24,7 @@ export function createContextInner(opts: CreateContextOptions) {
|
|||||||
pathname: opts.pathname,
|
pathname: opts.pathname,
|
||||||
uid: opts.uid,
|
uid: opts.uid,
|
||||||
url: opts.url,
|
url: opts.url,
|
||||||
|
webToken: opts.webToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +35,8 @@ export function createContextInner(opts: CreateContextOptions) {
|
|||||||
export function createContext() {
|
export function createContext() {
|
||||||
const h = headers()
|
const h = headers()
|
||||||
|
|
||||||
// const cookie = cookies()
|
const cookie = cookies()
|
||||||
// const webviewTokenCookie = cookie.get("webviewToken")
|
const webviewTokenCookie = cookie.get("webviewToken")
|
||||||
|
|
||||||
// if (webviewTokenCookie) {
|
|
||||||
// // since the token exists, this is a subsequent visit
|
|
||||||
// // we're done, allow it
|
|
||||||
// return createContextInner({
|
|
||||||
// session: {
|
|
||||||
// token: { access_token: webviewTokenCookie.value },
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const session = await auth()
|
|
||||||
|
|
||||||
return createContextInner({
|
return createContextInner({
|
||||||
auth,
|
auth,
|
||||||
@@ -54,6 +44,7 @@ export function createContext() {
|
|||||||
pathname: h.get("x-pathname")!,
|
pathname: h.get("x-pathname")!,
|
||||||
uid: h.get("x-uid"),
|
uid: h.get("x-uid"),
|
||||||
url: h.get("x-url")!,
|
url: h.get("x-url")!,
|
||||||
|
webToken: webviewTokenCookie?.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
server/routers/contentstack/breadcrumbs/input.ts
Normal file
8
server/routers/contentstack/breadcrumbs/input.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
export const getBreadcrumbsInput = z.object({
|
||||||
|
href: z.string().min(1, { message: "href is required" }),
|
||||||
|
locale: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
|
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import {
|
import {
|
||||||
badRequestError,
|
|
||||||
internalServerError,
|
internalServerError,
|
||||||
notFound,
|
notFound,
|
||||||
} from "@/server/errors/trpc"
|
} from "@/server/errors/trpc"
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
} from "@/utils/generateTag"
|
} from "@/utils/generateTag"
|
||||||
import { removeMultipleSlashes } from "@/utils/url"
|
import { removeMultipleSlashes } from "@/utils/url"
|
||||||
|
|
||||||
|
import { getBreadcrumbsInput } from "./input"
|
||||||
import {
|
import {
|
||||||
getBreadcrumbsSchema,
|
getBreadcrumbsSchema,
|
||||||
validateBreadcrumbsConstenstackSchema,
|
validateBreadcrumbsConstenstackSchema,
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ export const contentstackProcedure = t.procedure.use(async function (opts) {
|
|||||||
})
|
})
|
||||||
export const protectedProcedure = t.procedure.use(async function (opts) {
|
export const protectedProcedure = t.procedure.use(async function (opts) {
|
||||||
const authRequired = opts.meta?.authRequired ?? true
|
const authRequired = opts.meta?.authRequired ?? true
|
||||||
const ctx = await opts.ctx
|
const session = await opts.ctx.auth()
|
||||||
const session = ctx.session
|
|
||||||
if (!authRequired && env.NODE_ENV === "development") {
|
if (!authRequired && env.NODE_ENV === "development") {
|
||||||
console.info(
|
console.info(
|
||||||
`❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
|
`❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
|
||||||
@@ -48,7 +47,7 @@ export const protectedProcedure = t.procedure.use(async function (opts) {
|
|||||||
|
|
||||||
return opts.next({
|
return opts.next({
|
||||||
ctx: {
|
ctx: {
|
||||||
session,
|
session: session || { token: { access_token: opts.ctx.webToken } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export type UIDParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UriParams = {
|
export type UriParams = {
|
||||||
uri: string | string[]
|
uri: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PreviewParams = {
|
export type PreviewParams = {
|
||||||
|
|||||||
10
types/requests/myPages/logo.ts
Normal file
10
types/requests/myPages/logo.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Image } from "../../image"
|
||||||
|
import type { EdgesWithTotalCount } from "../utils/edges"
|
||||||
|
|
||||||
|
export type LogoQueryData = {
|
||||||
|
all_header: {
|
||||||
|
items: {
|
||||||
|
logoConnection: EdgesWithTotalCount<Image>
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
utils/webviews.ts
Normal file
13
utils/webviews.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user