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:
@@ -1,14 +1,13 @@
|
|||||||
import "@scandic-hotels/design-system/style.css"
|
import "@scandic-hotels/design-system/style.css"
|
||||||
import "@scandic-hotels/design-system/fonts.css"
|
import "@scandic-hotels/design-system/fonts.css"
|
||||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||||
import "./globals.css"
|
|
||||||
|
|
||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
import { TrpcProvider } from "@scandic-hotels/trpc/Provider"
|
import { TrpcProvider } from "@scandic-hotels/trpc/Provider"
|
||||||
|
|
||||||
import { getMessages } from "../i18n"
|
import { getMessages } from "@/i18n"
|
||||||
import ClientIntlProvider from "../i18n/Provider"
|
import ClientIntlProvider from "@/i18n/Provider"
|
||||||
import { setLang } from "../i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
|
|
||||||
@@ -27,8 +26,7 @@ type RootLayoutProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout(props: RootLayoutProps) {
|
export default async function RootLayout(props: RootLayoutProps) {
|
||||||
// const params = await props.params
|
const params = await props.params
|
||||||
const params = { lang: Lang.sv }
|
|
||||||
|
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.layout {
|
||||||
|
font-family: var(--typography-Body-Regular-fontFamily);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
apps/partner-sas/app/[lang]/not-found.tsx
Normal file
4
apps/partner-sas/app/[lang]/not-found.tsx
Normal 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>
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
|
|
||||||
import { serverClient } from "@/lib/trpc"
|
import { serverClient } from "@/lib/trpc"
|
||||||
|
|
||||||
import { getIntl } from "../i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
import { ClientComponent } from "./ClientComponent"
|
import { ClientComponent } from "./ClientComponent"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
body {
|
|
||||||
}
|
|
||||||
18
apps/partner-sas/env/server.ts
vendored
Normal file
18
apps/partner-sas/env/server.ts
vendored
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
113
apps/partner-sas/middleware.ts
Normal file
113
apps/partner-sas/middleware.ts
Normal 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).*)"],
|
||||||
|
}
|
||||||
16
apps/partner-sas/middlewares/invalidUrl.ts
Normal file
16
apps/partner-sas/middlewares/invalidUrl.ts
Normal 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("*")
|
||||||
|
}
|
||||||
25
apps/partner-sas/middlewares/trailingSlash.ts
Normal file
25
apps/partner-sas/middlewares/trailingSlash.ts
Normal 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("/")
|
||||||
|
}
|
||||||
3
apps/partner-sas/middlewares/types.ts
Normal file
3
apps/partner-sas/middlewares/types.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
export type MiddlewareMatcher = (request: NextRequest) => boolean
|
||||||
22
apps/partner-sas/middlewares/utils.ts
Normal file
22
apps/partner-sas/middlewares/utils.ts
Normal 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
|
||||||
|
}
|
||||||
94
apps/partner-sas/server/utils.ts
Normal file
94
apps/partner-sas/server/utils.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user