Merged in feat/sw-3197-add-url-to-path (pull request #2577)

feat(SW-3197): Add required middleware and url to path in partner-sas

* Add url to path and required middleware


Approved-by: Matilda Landström
This commit is contained in:
Anton Gunnarsson
2025-07-30 08:31:58 +00:00
parent c516860466
commit 9bbea3f16f
16 changed files with 324 additions and 9 deletions

View File

@@ -1,14 +1,13 @@
import "@scandic-hotels/design-system/style.css"
import "@scandic-hotels/design-system/fonts.css"
import "@/public/_static/css/design-system-new-deprecated.css"
import "./globals.css"
import { Lang } from "@scandic-hotels/common/constants/language"
import { TrpcProvider } from "@scandic-hotels/trpc/Provider"
import { getMessages } from "../i18n"
import ClientIntlProvider from "../i18n/Provider"
import { setLang } from "../i18n/serverContext"
import { getMessages } from "@/i18n"
import ClientIntlProvider from "@/i18n/Provider"
import { setLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
@@ -27,8 +26,7 @@ type RootLayoutProps = {
}
export default async function RootLayout(props: RootLayoutProps) {
// const params = await props.params
const params = { lang: Lang.sv }
const params = await props.params
const { children } = props

View File

@@ -0,0 +1,4 @@
export default function NotFoundPage() {
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
return <div>Not found, forgot lang in url?</div>
}

View File

@@ -0,0 +1,3 @@
.layout {
font-family: var(--typography-Body-Regular-fontFamily);
}

View File

@@ -0,0 +1,16 @@
import styles from "./page.module.css"
import type { Lang } from "@scandic-hotels/common/constants/language"
export default async function MiddlewareError(props: {
params: Promise<{ lang: Lang; status: string }>
}) {
const params = await props.params
return (
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
<div className={styles.layout}>
Middleware error {params.lang} {params.status}
</div>
)
}

View File

@@ -0,0 +1,4 @@
export default function NotFoundPage() {
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
return <div>Not Found, missing lang in url?</div>
}

View File

@@ -4,7 +4,8 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { serverClient } from "@/lib/trpc"
import { getIntl } from "../i18n"
import { getIntl } from "@/i18n"
import { ClientComponent } from "./ClientComponent"
import styles from "./page.module.css"

View File

@@ -1,2 +0,0 @@
body {
}

18
apps/partner-sas/env/server.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
/**
* Due to t3-env only checking typeof window === "undefined"
* and Netlify running Deno, window is never "undefined"
* https://github.com/t3-oss/t3-env/issues/154
*/
isServer: typeof window === "undefined" || "Deno" in window,
server: {
PUBLIC_URL: z.string().default(""),
},
emptyStringAsUndefined: true,
runtimeEnv: {
PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL,
},
})

View File

@@ -0,0 +1,113 @@
import * as Sentry from "@sentry/nextjs"
import { type NextMiddleware, NextResponse } from "next/server"
import { Lang } from "@scandic-hotels/common/constants/language"
import { logger } from "@scandic-hotels/common/logger"
import { findLang } from "@scandic-hotels/common/utils/languages"
import * as invalidUrl from "@/middlewares/invalidUrl"
import * as trailingSlash from "@/middlewares/trailingSlash"
import { getDefaultRequestHeaders } from "@/middlewares/utils"
export const middleware: NextMiddleware = async (request, event) => {
// auth() overrides the request origin, we need the original for internal rewrites
// @see getInternalNextURL()
request.headers.set("x-sh-origin", request.nextUrl.origin)
const headers = getDefaultRequestHeaders(request)
const lang = findLang(request.nextUrl.pathname)
if (!lang) {
// Lang is required for all our middleware.
// Without it we shortcircuit early.
// Default to English if no lang is found.
headers.set("x-lang", Lang.en)
return NextResponse.rewrite(
new URL(`/${Lang.en}/middleware-error/404`, request.nextUrl),
{
request: {
headers,
},
status: 404,
statusText: "Not found",
}
)
}
// Note that the order of middlewares is important since that is the order they are matched by.
const middlewares: { middleware: NextMiddleware; matcher: any }[] = [
invalidUrl,
trailingSlash,
// authRequired,
// handleAuth,
// bookingFlow,
// cmsContent,
]
try {
for (let i = 0; i < middlewares.length; ++i) {
const middleware = middlewares[i]
if (middleware.matcher(request)) {
const result = await middleware.middleware(request, event)
const _continue = result?.headers.get("x-continue")
if (_continue) {
continue
}
// Clean up internal headers
result?.headers.delete("x-sh-origin")
return result
}
}
} catch (e) {
if (e instanceof NextResponse && e.status) {
const cause = await e.json()
logger.error(`NextResponse Error in middleware`, cause)
Sentry.captureException(cause)
return NextResponse.rewrite(
new URL(`/${lang}/middleware-error/${e.status}`, request.nextUrl),
{
request: {
headers,
},
status: e.status,
statusText: e.statusText,
}
)
}
logger.error(`Error in middleware`, e)
Sentry.captureException(e)
return NextResponse.rewrite(
new URL(`/${lang}/middleware-error/500`, request.nextUrl),
{
request: {
headers,
},
status: 500,
statusText: "Internal Server Error",
}
)
}
// Follow through with normal App router rules.
return NextResponse.next({
request: {
headers,
},
})
}
export const config = {
/**
* Copied from Clerk to protect all routes by default and handle
* public routes inside middleware.
* (https://clerk.com/docs/quickstarts/nextjs?utm_source=sponsorship&utm_medium=youtube&utm_campaign=code-with-antonio&utm_content=12-31-2023#add-authentication-to-your-app)
*/
matcher: ["/((?!.+\\.[\\w]+$|_next|_static|.netlify|api|trpc|sitemap).*)"],
}

View File

@@ -0,0 +1,16 @@
import { type NextMiddleware, NextResponse } from "next/server"
import { getDefaultRequestHeaders } from "./utils"
import type { MiddlewareMatcher } from "./types"
export const middleware: NextMiddleware = async (request) => {
const headers = getDefaultRequestHeaders(request)
return NextResponse.next({
headers,
})
}
export const matcher: MiddlewareMatcher = (request) => {
return request.nextUrl.pathname.includes("*")
}

View File

@@ -0,0 +1,25 @@
import { type NextMiddleware, NextResponse } from "next/server"
import { getPublicNextURL } from "@/server/utils"
import { getDefaultRequestHeaders } from "./utils"
import type { MiddlewareMatcher } from "./types"
export const middleware: NextMiddleware = async (request) => {
const headers = getDefaultRequestHeaders(request)
const newUrl = new URL(
request.nextUrl.pathname.slice(0, -1),
getPublicNextURL(request)
)
return NextResponse.redirect(newUrl, {
headers,
status: 308,
})
}
export const matcher: MiddlewareMatcher = (request) => {
return request.nextUrl.pathname.endsWith("/")
}

View File

@@ -0,0 +1,3 @@
import type { NextRequest } from "next/server"
export type MiddlewareMatcher = (request: NextRequest) => boolean

View File

@@ -0,0 +1,22 @@
import { findLang } from "@scandic-hotels/common/utils/languages"
import { removeTrailingSlash } from "@scandic-hotels/common/utils/url"
import { getPublicNextURL } from "@/server/utils"
import type { NextRequest } from "next/server"
export function getDefaultRequestHeaders(request: NextRequest) {
const lang = findLang(request.nextUrl.pathname)!
const nextUrlPublic = getPublicNextURL(request)
const headers = new Headers(request.headers)
headers.set("x-lang", lang)
headers.set(
"x-pathname",
removeTrailingSlash(
request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "")
)
)
headers.set("x-url", removeTrailingSlash(nextUrlPublic.href))
return headers
}

View File

@@ -0,0 +1,94 @@
import { NextRequest } from "next/server"
import { logger } from "@scandic-hotels/common/logger"
import { env } from "@/env/server"
/**
* Use this function when you want to create URLs that are public facing, for
* example for redirects or redirectTo query parameters.
* Dedicated environments are behind Akamai (test, stage, production). They have
* env.PUBLIC_URL set.
* All other environment like deploy previews and branch deployments are not
* behind Akamai and therefore do not have env.PUBLIC_URL set.
* We need this approach because Netlify uses x-forwarded-host internally and
* strips it from ever reaching our code.
* TODO: Replace this approach with custom header in Akamai that mirrors the
* value in x-forwarded-host which would not get stripped by Netlify.
* @param request The incoming request.
* @returns NextURL The public facing URL instance for the given request.
*/
export function getPublicNextURL(request: NextRequest) {
if (env.PUBLIC_URL) {
const publicNextURL = request.nextUrl.clone()
// Akamai in front of Netlify for dedicated environments
// require us to rewrite the incoming host and hostname
// to match the public URL used to visit Akamai.
const url = new URL(env.PUBLIC_URL)
publicNextURL.host = url.host
publicNextURL.hostname = url.hostname
return publicNextURL
}
return request.nextUrl
}
/**
* Use this function when you want the public facing URL for the given request.
* Read about the motivation in getPublicNextURL above.
* @see getPublicNextURL
* @param request The incoming request.
* @returns string The public facing origin for the given request.
*/
export function getPublicURL(request: NextRequest) {
if (env.PUBLIC_URL) {
return env.PUBLIC_URL
}
return request.nextUrl.origin
}
/**
* Use this function when you want to create URLs that are internal (behind Akamai),
* for example for rewrites. Mainly used for middleware wrapped in auth().
* The auth() function overrides the origin of the incoming request. It sets it
* to the origin of AUTH_URL/NEXTAUTH_URL. This means we cannot use the augmented
* request from auth() for rewrites, as those will point to auth url origin
* (in front of Akamai) and not the origin of the incoming request (behind Akamai).
* This results in rewrites going over the internet instead of going through the
* internal routing at Netlify.
* For dedicated environments (test, stage and production) we are behind Akamai.
* For those we have set a value for AUTH_URL/NEXTAUTH_URL, they point to the
* PUBLIC_URL. For rewrites we need the internal origin inside Netlify.
* In middleware.ts we copy the incoming origin to a header 'x-sh-origin'. We try
* and use that first, if not present we assume the internal origin is the value
* of the host header, as that is what Netlify used for routing to this deployment.
* @param request The incoming request.
* @returns NextURL The internal request, in Netlify behind Akamai.
*/
export function getInternalNextURL(request: NextRequest) {
const { href, origin } = request.nextUrl
const originHeader = request.headers.get("x-sh-origin")
if (originHeader) {
logger.debug(`[internalNextUrl] using x-sh-origin header`, {
origin,
originHeader,
newOrigin: href.replace(origin, originHeader),
})
return new NextRequest(href.replace(origin, originHeader), request).nextUrl
}
const hostHeader = request.headers.get("host")
if (hostHeader) {
const inputHostOrigin = `${request.nextUrl.protocol}//${hostHeader}`
logger.debug(`[internalNextUrl] using host header`, {
origin,
hostHeader,
hostOrigin: inputHostOrigin,
newOrigin: href.replace(origin, inputHostOrigin),
})
const { origin: hostOrigin } = new URL(inputHostOrigin)
return new NextRequest(href.replace(origin, hostOrigin), request).nextUrl
}
logger.debug(`[internalNextUrl] falling back to incoming request`)
return request.nextUrl
}