feat(WEB-132): add middlewares, support for seamless login and improve lang based routes

This commit is contained in:
Michael Zetterberg
2024-04-08 16:08:35 +02:00
parent 8ab5325fc3
commit 7093a0b2dd
31 changed files with 493 additions and 188 deletions

View File

@@ -0,0 +1,53 @@
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { findLang } from "@/constants/languages"
import { authRequired } from "@/constants/routes/authRequired"
import { login } from "@/constants/routes/handleAuth"
import type { NextMiddleware } from "next/server"
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 { nextUrl } = request
const lang = findLang(nextUrl.pathname)!
const isLoggedIn = !!request.auth
if (isLoggedIn) {
return NextResponse.next()
}
const loginUrl = login[lang]
return NextResponse.redirect(new URL(loginUrl, request.nextUrl))
}) as NextMiddleware // See comment above
export const matcher: MiddlewareMatcher = (request) => {
return authRequired.includes(request.nextUrl.pathname)
}

40
middlewares/cmsContent.ts Normal file
View File

@@ -0,0 +1,40 @@
import { NextResponse } from "next/server"
import { findLang } from "@/constants/languages"
import type { NextMiddleware } from "next/server"
import { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request
const lang = findLang(nextUrl.pathname)
const contentType = "currentContentPage"
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
if (request.nextUrl.pathname.includes("preview")) {
searchParams.set("uri", pathNameWithoutLang.replace("/preview", ""))
return NextResponse.rewrite(
new URL(`/${lang}/preview-current?${searchParams.toString()}`, nextUrl)
)
}
searchParams.set("uri", pathNameWithoutLang)
switch (contentType) {
case "currentContentPage":
return NextResponse.rewrite(
new URL(
`/${lang}/current-content-page?${searchParams.toString()}`,
nextUrl
)
)
default:
return NextResponse.next()
}
}
export const matcher: MiddlewareMatcher = (request) => {
return true
}

View File

@@ -0,0 +1,31 @@
import { NextResponse } from "next/server"
import { findLang } from "@/constants/languages"
import { badRequest } from "@/server/errors/next"
import type { NextMiddleware } from "next/server"
import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = (request) => {
const redirectTo = request.nextUrl.searchParams.get("returnurl")
if (!redirectTo) {
return badRequest()
}
const lang = findLang(request.nextUrl.pathname)!
const headers = new Headers(request.headers)
headers.set("x-redirect-to", redirectTo)
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
request: {
headers,
},
})
}
export const matcher: MiddlewareMatcher = (request) => {
return request.nextUrl.pathname.endsWith("/updatelogin")
}

15
middlewares/ensureLang.ts Normal file
View File

@@ -0,0 +1,15 @@
import { NextResponse } from "next/server"
import { findLang } from "@/constants/languages"
import type { NextMiddleware } from "next/server"
import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = () => {
return new NextResponse("Not found", { status: 404 })
}
export const matcher: MiddlewareMatcher = (request) => {
return !findLang(request.nextUrl.pathname)
}

15
middlewares/handleAuth.ts Normal file
View File

@@ -0,0 +1,15 @@
import { NextResponse } from "next/server"
import { handleAuth } from "@/constants/routes/handleAuth"
import type { NextMiddleware } from "next/server"
import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = () => {
return NextResponse.next()
}
export const matcher: MiddlewareMatcher = (request) => {
return handleAuth.includes(request.nextUrl.pathname)
}

59
middlewares/webView.ts Normal file
View File

@@ -0,0 +1,59 @@
import { NextResponse, type NextMiddleware } from "next/server"
import { findLang } from "@/constants/languages"
import { env } from "@/env/server"
import { badRequest, internalServerError } from "@/server/errors/next"
import { decryptData } from "@/utils/aes"
import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = async (request) => {
const webviewToken = request.cookies.get("webviewToken")
if (webviewToken) {
// since the token exists, this is a subsequent visit
// we're done, allow it
return NextResponse.next()
}
// Authorization header is required for webviews
// It should be base64 encoded
const authorization = request.headers.get("Authorization")!
if (!authorization) {
return badRequest()
}
// Initialization vector header is required for webviews
// It should be base64 encoded
const initializationVector = request.headers.get("X-AES-IV")!
if (!initializationVector) {
return badRequest()
}
try {
const decryptedData = await decryptData(
env.WEBVIEW_ENCRYPTION_KEY,
initializationVector,
authorization
)
// Pass the webview token via cookie to the page
return NextResponse.next({
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly;`,
},
})
} catch (e) {
if (e instanceof Error) {
console.error(`${e.name}: ${e.message}`)
}
return badRequest()
}
}
export const matcher: MiddlewareMatcher = (request) => {
const { nextUrl } = request
const lang = findLang(nextUrl.pathname)
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
return pathNameWithoutLang.startsWith("/webview/")
}