Merge branch 'develop' into feat/SW-185-implement-footer-navigation
This commit is contained in:
@@ -20,13 +20,17 @@ export default async function ProtectedLayout({
|
||||
h.get("x-url") ?? h.get("x-pathname") ?? overview[getLang()]
|
||||
)
|
||||
|
||||
const redirectURL = `/${getLang()}/login?redirectTo=${redirectTo}`
|
||||
|
||||
if (!session) {
|
||||
redirect(`/${getLang()}/login?redirectTo=${redirectTo}`)
|
||||
console.log(`[layout:protected] no session, redirecting to: ${redirectURL}`)
|
||||
redirect(redirectURL)
|
||||
}
|
||||
|
||||
const user = await serverClient().user.get()
|
||||
if (!user || "error" in user) {
|
||||
redirect(`/${getLang()}/login?redirectTo=${redirectTo}`)
|
||||
console.log(`[layout:protected] no user, redirecting to: ${redirectURL}`)
|
||||
redirect(redirectURL)
|
||||
}
|
||||
|
||||
return children
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { createActionURL } from "@auth/core"
|
||||
import { headers as nextHeaders } from "next/headers"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { AuthError } from "next-auth"
|
||||
|
||||
@@ -16,11 +14,35 @@ export async function GET(
|
||||
let redirectTo: string = ""
|
||||
|
||||
const returnUrl = request.headers.get("x-returnurl")
|
||||
const isSeamless = request.headers.get("x-logout-source") === "seamless"
|
||||
|
||||
if (returnUrl) {
|
||||
// Seamless logout request from Current web
|
||||
redirectTo = returnUrl
|
||||
console.log(
|
||||
`[logout] source: ${request.headers.get("x-logout-source") || "normal"}`
|
||||
)
|
||||
|
||||
const redirectToSearchParamValue =
|
||||
request.nextUrl.searchParams.get("redirectTo")
|
||||
const redirectToFallback = "/"
|
||||
|
||||
if (isSeamless) {
|
||||
if (returnUrl) {
|
||||
redirectTo = returnUrl
|
||||
} else {
|
||||
console.log(
|
||||
`[login] missing returnUrl, using fallback: ${redirectToFallback}`
|
||||
)
|
||||
redirectTo = redirectToFallback
|
||||
}
|
||||
} else {
|
||||
redirectTo = redirectToSearchParamValue || redirectToFallback
|
||||
|
||||
// Make relative URL to absolute URL
|
||||
if (redirectTo.startsWith("/")) {
|
||||
console.log(`[logout] make redirectTo absolute, from ${redirectTo}`)
|
||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
||||
console.log(`[logout] make redirectTo absolute, to ${redirectTo}`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Initiate the seamless logout flow
|
||||
let redirectUrlValue
|
||||
@@ -45,6 +67,9 @@ export async function GET(
|
||||
break
|
||||
}
|
||||
const redirectUrl = new URL(redirectUrlValue)
|
||||
console.log(
|
||||
`[logout] creating redirect to seamless logout: ${redirectUrl}`
|
||||
)
|
||||
redirectTo = redirectUrl.toString()
|
||||
} catch (e) {
|
||||
console.error(
|
||||
@@ -55,37 +80,25 @@ export async function GET(
|
||||
}
|
||||
|
||||
try {
|
||||
redirectTo = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}`
|
||||
console.log(`[logout] final redirectUrl: ${redirectTo}`)
|
||||
console.log({ logout_env: process.env })
|
||||
|
||||
/**
|
||||
* Passing `redirect: false` to `signOut` will return a result object
|
||||
* instead of automatically redirecting inside of `signOut`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L104
|
||||
*/
|
||||
console.log({ logout_NEXTAUTH_URL: process.env.NEXTAUTH_URL })
|
||||
console.log({ logout_env: process.env })
|
||||
|
||||
const headers = new Headers(nextHeaders())
|
||||
const signOutURL = createActionURL(
|
||||
"signout",
|
||||
// @ts-expect-error `x-forwarded-proto` is not nullable, next.js sets it by default
|
||||
headers.get("x-forwarded-proto"),
|
||||
headers,
|
||||
process.env
|
||||
)
|
||||
|
||||
console.log({ logout_signOutURL: signOutURL })
|
||||
|
||||
// Redirect to Curity logout
|
||||
const curityLogoutUrl = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}`
|
||||
|
||||
console.log({ logout_redirectTo: curityLogoutUrl })
|
||||
|
||||
const redirectUrlObj = await signOut({
|
||||
redirectTo: curityLogoutUrl,
|
||||
redirectTo,
|
||||
redirect: false,
|
||||
})
|
||||
|
||||
if (redirectUrlObj) {
|
||||
console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`)
|
||||
return NextResponse.redirect(redirectUrlObj.redirect)
|
||||
} else {
|
||||
console.error(`[logout] missing redirectUrlObj reponse from signOut()`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -34,7 +34,6 @@ export default async function MyPages({
|
||||
<p>{formatMessage({ id: "No content published" })}</p>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<TrackingSDK pageData={tracking} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -14,17 +14,6 @@
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
column-gap: var(--Spacing-x1);
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
justify-items: flex-end;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
gap: var(--Spacing-x3);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { env } from "@/env/server"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { CreditCard, Delete } from "@/components/Icons"
|
||||
import AddCreditCardButton from "@/components/Profile/AddCreditCardButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CreditCardList from "@/components/Profile/CreditCardList"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
@@ -33,40 +31,8 @@ export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
|
||||
})}
|
||||
</Body>
|
||||
</article>
|
||||
{creditCards?.length ? (
|
||||
<div className={styles.cardContainer}>
|
||||
{creditCards.map((card, idx) => (
|
||||
<CreditCardRow
|
||||
key={idx}
|
||||
cardType={card.attribute.cardType}
|
||||
truncatedNumber={card.attribute.truncatedNumber}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<AddCreditCardButton
|
||||
redirectUrl={`${env.PUBLIC_URL}/api/web/add-card-callback/${lang}`}
|
||||
/>
|
||||
<CreditCardList initialData={creditCards} />
|
||||
<AddCreditCardButton />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function CreditCardRow({
|
||||
truncatedNumber,
|
||||
cardType,
|
||||
}: {
|
||||
truncatedNumber: string
|
||||
cardType: string
|
||||
}) {
|
||||
const maskedCardNumber = `**** ${truncatedNumber.slice(12, 16)}`
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<CreditCard color="black" />
|
||||
<Body textTransform="bold">{cardType}</Body>
|
||||
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
|
||||
<Button variant="icon" theme="base" intent="text">
|
||||
<Delete color="burgundy" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ export default function ProfileLayout({
|
||||
{profile}
|
||||
<Divider color="burgundy" opacity={8} />
|
||||
{creditCards}
|
||||
{communication}
|
||||
{/* TODO: Implement communication preferences flow. Hidden until decided on where to send user. */}
|
||||
{/* {communication} */}
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import ContentPage from "@/components/ContentType/ContentPage"
|
||||
import HotelPage from "@/components/ContentType/HotelPage/HotelPage"
|
||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage/LoyaltyPage"
|
||||
import HotelPage from "@/components/ContentType/HotelPage"
|
||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x4);
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x4);
|
||||
width: 100%;
|
||||
max-width: 365px;
|
||||
}
|
||||
@media screen and (min-width: 1367px) {
|
||||
.section {
|
||||
max-width: 525px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection"
|
||||
import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection"
|
||||
import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection"
|
||||
import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
export default function BookingConfirmationPage() {
|
||||
const { email, hotel, stay, summary } = tempConfirmationData
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<section className={styles.section}>
|
||||
<IntroSection email={email} />
|
||||
<StaySection hotel={hotel} stay={stay} />
|
||||
<SummarySection summary={summary} />
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -11,32 +11,51 @@ export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: { lang: Lang } }
|
||||
) {
|
||||
let redirectHeaders: Headers | undefined = undefined
|
||||
let redirectTo: string
|
||||
|
||||
const returnUrl = request.headers.get("x-returnurl")
|
||||
const isMFA = request.headers.get("x-mfa-login")
|
||||
|
||||
// This is to support seamless login when using magic link login
|
||||
const isMagicLinkUpdateLogin = !!request.headers.get("x-magic-link")
|
||||
|
||||
if (!env.PUBLIC_URL) {
|
||||
throw internalServerError("No value for env.PUBLIC_URL")
|
||||
}
|
||||
|
||||
if (returnUrl) {
|
||||
// Seamless login request from Current web
|
||||
redirectTo = returnUrl
|
||||
let redirectHeaders: Headers | undefined = undefined
|
||||
let redirectTo: string
|
||||
|
||||
const returnUrl = request.headers.get("x-returnurl")
|
||||
const isSeamless = request.headers.get("x-login-source") === "seamless"
|
||||
const isMFA = request.headers.get("x-login-source") === "mfa"
|
||||
const isSeamlessMagicLink =
|
||||
request.headers.get("x-login-source") === "seamless-magiclink"
|
||||
|
||||
console.log(
|
||||
`[login] source: ${request.headers.get("x-login-source") || "normal"}`
|
||||
)
|
||||
|
||||
const redirectToCookieValue = request.cookies.get("redirectTo")?.value // Cookie gets set by authRequired middleware
|
||||
const redirectToSearchParamValue =
|
||||
request.nextUrl.searchParams.get("redirectTo")
|
||||
const redirectToFallback = "/"
|
||||
|
||||
console.log(`[login] redirectTo cookie value: ${redirectToCookieValue}`)
|
||||
console.log(
|
||||
`[login] redirectTo search param value: ${redirectToSearchParamValue}`
|
||||
)
|
||||
|
||||
if (isSeamless || isSeamlessMagicLink) {
|
||||
if (returnUrl) {
|
||||
redirectTo = returnUrl
|
||||
} else {
|
||||
console.log(
|
||||
`[login] missing returnUrl, using fallback: ${redirectToFallback}`
|
||||
)
|
||||
redirectTo = redirectToFallback
|
||||
}
|
||||
} else {
|
||||
// Normal login request from New web
|
||||
redirectTo =
|
||||
request.cookies.get("redirectTo")?.value || // Cookie gets set by authRequired middleware
|
||||
request.nextUrl.searchParams.get("redirectTo") ||
|
||||
"/"
|
||||
redirectToCookieValue || redirectToSearchParamValue || redirectToFallback
|
||||
|
||||
// Make relative URL to absolute URL
|
||||
if (redirectTo.startsWith("/")) {
|
||||
console.log(`[login] make redirectTo absolute, from ${redirectTo}`)
|
||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
||||
console.log(`[login] make redirectTo absolute, to ${redirectTo}`)
|
||||
}
|
||||
|
||||
// Clean up cookie from authRequired middleware
|
||||
@@ -70,7 +89,11 @@ export async function GET(
|
||||
break
|
||||
}
|
||||
const redirectUrl = new URL(redirectUrlValue)
|
||||
console.log(`[login] creating redirect to seamless login: ${redirectUrl}`)
|
||||
redirectUrl.searchParams.set("returnurl", redirectTo)
|
||||
console.log(
|
||||
`[login] returnurl for seamless login: ${redirectUrl.searchParams.get("returnurl")}`
|
||||
)
|
||||
redirectTo = redirectUrl.toString()
|
||||
|
||||
/** Set cookie with redirect Url to appropriately redirect user when using magic link login */
|
||||
@@ -82,25 +105,20 @@ export async function GET(
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Unable to create URL for seamless login, proceeding without it."
|
||||
"[login] unable to create URL for seamless login, proceeding without it.",
|
||||
e
|
||||
)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||
* automatically redirecting to it inside of `signIn`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||
*/
|
||||
console.log({ login_NEXTAUTH_URL: process.env.NEXTAUTH_URL })
|
||||
console.log(`[login] final redirectUrl: ${redirectTo}`)
|
||||
console.log({ login_env: process.env })
|
||||
|
||||
console.log({ login_redirectTo: redirectTo })
|
||||
const params = {
|
||||
/** Record<string, any> is next-auth typings */
|
||||
const params: Record<string, any> = {
|
||||
ui_locales: context.params.lang,
|
||||
scope: ["openid", "profile"].join(" "),
|
||||
scope: ["openid", "profile"],
|
||||
/**
|
||||
* The `acr_values` param is used to make Curity display the proper login
|
||||
* page for Scandic. Without the parameter Curity presents some choices
|
||||
@@ -117,18 +135,25 @@ export async function GET(
|
||||
// This is new param set for differentiate between the Magic link login of New web and current web
|
||||
version: "2",
|
||||
}
|
||||
|
||||
if (isMFA) {
|
||||
// Append profile_update scope for MFA
|
||||
params.scope = params.scope + " profile_udpate"
|
||||
params.scope.push("profile_update")
|
||||
/**
|
||||
* The below acr value is required as for New Web same Curity Client is used for MFA
|
||||
* while in current web it is being setup using different Curity Client
|
||||
*/
|
||||
params.acr_values =
|
||||
"urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web"
|
||||
} else if (isMagicLinkUpdateLogin) {
|
||||
} else if (isSeamlessMagicLink) {
|
||||
params.acr_values = "abc"
|
||||
}
|
||||
params.scope = params.scope.join(" ")
|
||||
/**
|
||||
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||
* automatically redirecting to it inside of `signIn`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||
*/
|
||||
const redirectUrl = await signIn(
|
||||
"curity",
|
||||
{
|
||||
@@ -139,9 +164,13 @@ export async function GET(
|
||||
)
|
||||
|
||||
if (redirectUrl) {
|
||||
return NextResponse.redirect(redirectUrl, {
|
||||
const redirectOpts = {
|
||||
headers: redirectHeaders,
|
||||
})
|
||||
}
|
||||
console.log(`[login] redirecting to: ${redirectUrl}`, redirectOpts)
|
||||
return NextResponse.redirect(redirectUrl, redirectOpts)
|
||||
} else {
|
||||
console.error(`[login] missing redirectUrl reponse from signIn()`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -12,39 +12,54 @@ export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: { lang: Lang } }
|
||||
) {
|
||||
let redirectTo: string
|
||||
|
||||
// Set redirect url from the magicLinkRedirect Cookie which is set when intiating login
|
||||
redirectTo =
|
||||
request.cookies.get("magicLinkRedirectTo")?.value ||
|
||||
"/" + context.params.lang
|
||||
|
||||
if (!env.PUBLIC_URL) {
|
||||
throw internalServerError("No value for env.PUBLIC_URL")
|
||||
}
|
||||
|
||||
const loginKey = request.nextUrl.searchParams.get("loginKey")
|
||||
if (!loginKey) {
|
||||
console.log(
|
||||
`[verifymagiclink] missing required loginKey, aborting bad request`
|
||||
)
|
||||
return badRequest()
|
||||
}
|
||||
|
||||
let redirectTo: string
|
||||
|
||||
console.log(`[verifymagiclink] verifying callback`)
|
||||
|
||||
const redirectToCookieValue = request.cookies.get(
|
||||
"magicLinkRedirectTo"
|
||||
)?.value // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login
|
||||
const redirectToFallback = "/"
|
||||
|
||||
console.log(
|
||||
`[verifymagiclink] magicLinkRedirectTo cookie value: ${redirectToCookieValue}`
|
||||
)
|
||||
|
||||
redirectTo = redirectToCookieValue || redirectToFallback
|
||||
|
||||
// Make relative URL to absolute URL
|
||||
if (redirectTo.startsWith("/")) {
|
||||
console.log(
|
||||
`[verifymagiclink] make redirectTo absolute, from ${redirectTo}`
|
||||
)
|
||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
||||
console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`)
|
||||
}
|
||||
|
||||
// Update Seamless login url as Magic link login has a different authenticator in Curity
|
||||
redirectTo = redirectTo.replace("updatelogin", "updateloginemail")
|
||||
|
||||
const loginKey = request.nextUrl.searchParams.get("loginKey")
|
||||
|
||||
if (!loginKey) {
|
||||
return badRequest()
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[verifymagiclink] final redirectUrl: ${redirectTo}`)
|
||||
|
||||
/**
|
||||
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||
* automatically redirecting to it inside of `signIn`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||
*/
|
||||
console.log({ login_redirectTo: redirectTo })
|
||||
let redirectUrl = await signIn(
|
||||
const redirectUrl = await signIn(
|
||||
"curity",
|
||||
{
|
||||
redirectTo,
|
||||
@@ -61,7 +76,12 @@ export async function GET(
|
||||
)
|
||||
|
||||
if (redirectUrl) {
|
||||
console.log(`[verifymagiclink] redirecting to: ${redirectUrl}`)
|
||||
return NextResponse.redirect(redirectUrl)
|
||||
} else {
|
||||
console.error(
|
||||
`[verifymagiclink] missing redirectUrl reponse from signIn()`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -15,14 +15,9 @@ import { getIntl } from "@/i18n"
|
||||
import ServerIntlProvider from "@/i18n/Provider"
|
||||
import { getLang, setLang } from "@/i18n/serverContext"
|
||||
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "New web",
|
||||
title: "Scandic Hotels",
|
||||
}
|
||||
export { generateMetadata } from "@/utils/generateMetadata"
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
@@ -35,8 +30,8 @@ export default async function RootLayout({
|
||||
>) {
|
||||
setLang(params.lang)
|
||||
preloadUserTracking()
|
||||
|
||||
const { defaultLocale, locale, messages } = await getIntl()
|
||||
|
||||
return (
|
||||
<html lang={getLang()}>
|
||||
<head>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function NotFound({ params }: PageArgs<LangParams>) {
|
||||
setLang(params.lang)
|
||||
export default async function NotFound() {
|
||||
const { formatMessage } = await getIntl()
|
||||
return (
|
||||
<main>
|
||||
|
||||
Reference in New Issue
Block a user