import { type NextMiddleware, NextResponse } from "next/server" import { login } from "@scandic-hotels/common/constants/routes/handleAuth" import { logger } from "@scandic-hotels/common/logger" import { findLang } from "@scandic-hotels/common/utils/languages" import { authRequired, mfaRequired } from "@/constants/routes/authRequired" import { getInternalNextURL, getPublicNextURL } from "@/server/utils" import { auth } from "@/auth" import type { MiddlewareMatcher } from "@/types/middleware" /** * AppRouteHandlerFnContext is the context that is passed to the handler as * the second argument. This is only done for Route handlers (route.js) and * not for middleware. Middleware`s second argument is `event` of type * `NextFetchEvent`. * * Auth.js uses the same pattern for both Route handlers and Middleware, * the auth()-wrapper: * * auth((req) => { ... }) * * But there is a difference between middleware and route handlers, route * handlers get passed a context which middleware do not get (they get a * NextFetchEvent instead). Using the same function for both works runtime * because Auth.js handles this properly. But fails in typings as the second * argument doesn't match for middleware. * * We want to avoid using ts-expect-error because that hides other errors * not related to this typing error and ts-expect-error cannot be scoped either. * * So we type assert this export to NextMiddleware. The lesser of all evils. * * https://github.com/nextauthjs/next-auth/blob/3c035ec62f2f21d7cab65504ba83fb1a9a13be01/packages/next-auth/src/lib/index.ts#L265 * https://authjs.dev/reference/nextjs */ export const middleware = auth(async (request) => { const lang = findLang(request.nextUrl.pathname)! const isLoggedIn = !!request.auth const hasError = request.auth?.error // Inside auth() we need an internal request for rewrites. // @see getInternalNextURL() const nextUrlInternal = getInternalNextURL(request) const nextUrlPublic = getPublicNextURL(request) /** * Function to validate MFA from token data * @returns boolean */ function isMFAInvalid() { const isMFATokenValid = request.auth?.token.mfa_expires_at ? request.auth.token.mfa_expires_at > Date.now() : false return !(request.auth?.token.mfa_scope && isMFATokenValid) } const isMFAPath = mfaRequired.includes(request.nextUrl.pathname) if (isLoggedIn && isMFAPath && isMFAInvalid()) { const headers = new Headers(request.headers) headers.set("x-returnurl", nextUrlPublic.href) headers.set("x-login-source", "mfa") return NextResponse.rewrite(new URL(`/${lang}/login`, nextUrlInternal), { request: { headers, }, }) } if (isLoggedIn && !hasError) { const headers = new Headers(request.headers) headers.set("x-continue", "1") return NextResponse.next({ headers, }) } const headers = new Headers() headers.append( "set-cookie", `redirectTo=${encodeURIComponent(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax` ) const loginUrl = login[lang] const redirectUrl = new URL(loginUrl, nextUrlPublic) const redirectOpts = { headers, } logger.debug(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts) return NextResponse.redirect(redirectUrl, redirectOpts) }) as unknown as NextMiddleware // See comment above export const matcher: MiddlewareMatcher = (request) => { return authRequired.includes(request.nextUrl.pathname) }