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/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
|
||||
|
||||
@@ -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 { getIntl } from "../i18n"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { ClientComponent } from "./ClientComponent"
|
||||
|
||||
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