feat(WEB-132): add middlewares, support for seamless login and improve lang based routes
This commit is contained in:
@@ -16,3 +16,4 @@ NEXTAUTH_REDIRECT_PROXY_URL="http://localhost:3000/api/auth"
|
|||||||
NEXTAUTH_SECRET=""
|
NEXTAUTH_SECRET=""
|
||||||
NEXTAUTH_URL="http://localhost:3000/api/auth"
|
NEXTAUTH_URL="http://localhost:3000/api/auth"
|
||||||
REVALIDATE_SECRET=""
|
REVALIDATE_SECRET=""
|
||||||
|
WEBVIEW_ENCRYPTION_KEY="MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { redirect } from "next/navigation"
|
import { auth, signIn } from "@/auth"
|
||||||
|
|
||||||
import { auth } from "@/auth"
|
|
||||||
|
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
@@ -14,7 +12,10 @@ export default async function ProtectedLayout({
|
|||||||
* protected route group is actually protected.
|
* protected route group is actually protected.
|
||||||
*/
|
*/
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return redirect(`/${params.lang}/login`)
|
await signIn("curity", undefined, {
|
||||||
|
ui_locales: params.lang,
|
||||||
|
})
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { signIn } from "@/auth"
|
|
||||||
import { pageNames } from "@/constants/myPages"
|
|
||||||
|
|
||||||
import type { LangParams, Params } from "@/types/params"
|
|
||||||
|
|
||||||
export default async function Page({ params }: Params<LangParams>) {
|
|
||||||
async function login() {
|
|
||||||
"use server"
|
|
||||||
await signIn("curity", {
|
|
||||||
redirectTo: `/${params.lang}/${pageNames[params.lang]}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<form action={login}>
|
|
||||||
<button type="submit">Sign In</button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
47
app/[lang]/(live)/(public)/login/route.ts
Normal file
47
app/[lang]/(live)/(public)/login/route.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { AuthError } from "next-auth"
|
||||||
|
|
||||||
|
import { signIn } from "@/auth"
|
||||||
|
import { badRequest } from "@/server/errors/next"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: { lang: Lang } }
|
||||||
|
) {
|
||||||
|
const redirectTo =
|
||||||
|
request.headers.get("x-redirect-to") ||
|
||||||
|
request.nextUrl.searchParams.get("redirectTo") ||
|
||||||
|
undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||||
|
* automatically redirecting to it inside of `signIn`.
|
||||||
|
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||||
|
*/
|
||||||
|
const url = await signIn(
|
||||||
|
"curity",
|
||||||
|
{
|
||||||
|
redirectTo,
|
||||||
|
redirect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ui_locales: context.params.lang,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return NextResponse.redirect(url)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
console.log({ signInAuthError: error })
|
||||||
|
} else {
|
||||||
|
console.log({ signInError: error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return badRequest()
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import SkipToMainContent from "@/components/SkipToMainContent"
|
|||||||
|
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
import VwoScript from "@/components/Current/NewVWOScript"
|
|
||||||
|
|
||||||
export const fetchCache = "default-no-store"
|
export const fetchCache = "default-no-store"
|
||||||
|
|
||||||
@@ -40,10 +39,6 @@ export default function RootLayout({
|
|||||||
id="Cookiebot"
|
id="Cookiebot"
|
||||||
src="https://consent.cookiebot.com/uc.js"
|
src="https://consent.cookiebot.com/uc.js"
|
||||||
/>
|
/>
|
||||||
{/* <Script
|
|
||||||
data-cookieconsent="ignore"
|
|
||||||
src="/_static/dist/js/head.js?85c84c9e24ae8da3e7af"
|
|
||||||
/> */}
|
|
||||||
<Script
|
<Script
|
||||||
data-cookieconsent="ignore"
|
data-cookieconsent="ignore"
|
||||||
src="/_static/dist/js/inline.js?00133e5a37de35c51a5d"
|
src="/_static/dist/js/inline.js?00133e5a37de35c51a5d"
|
||||||
@@ -64,11 +59,6 @@ export default function RootLayout({
|
|||||||
data-cookieconsent="ignore"
|
data-cookieconsent="ignore"
|
||||||
src="/_static/dist/js/ng/main.js?1705409330990"
|
src="/_static/dist/js/ng/main.js?1705409330990"
|
||||||
/>
|
/>
|
||||||
{/* <Script
|
|
||||||
data-cookieconsent="ignore"
|
|
||||||
src="/_static/dist/js/main-ng.js?336b801d6b38eff10884"
|
|
||||||
strategy="lazyOnload"
|
|
||||||
/> */}
|
|
||||||
<Script id="ensure-datalayer">{`
|
<Script id="ensure-datalayer">{`
|
||||||
window.datalayer = window.datalayer || {}
|
window.datalayer = window.datalayer || {}
|
||||||
`}</Script>
|
`}</Script>
|
||||||
|
|||||||
17
app/[lang]/webview/layout.tsx
Normal file
17
app/[lang]/webview/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Webview",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
|
return (
|
||||||
|
<html lang={params.lang}>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
15
app/[lang]/webview/test/page.tsx
Normal file
15
app/[lang]/webview/test/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Hello World from Webview",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WebViewTestPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>Hello From WebView Test Page!</h1>
|
||||||
|
</header>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
auth.ts
34
auth.ts
@@ -19,6 +19,12 @@ const customProvider = {
|
|||||||
url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize`,
|
url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize`,
|
||||||
params: {
|
params: {
|
||||||
scope: ["openid"],
|
scope: ["openid"],
|
||||||
|
/**
|
||||||
|
* The `acr_values` param is used to make Curity display the proper login
|
||||||
|
* page for Scandic. Without the parameter Curity presents some choices
|
||||||
|
* to the user which we do not want.
|
||||||
|
*/
|
||||||
|
acr_values: "acr",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
@@ -39,10 +45,6 @@ const customProvider = {
|
|||||||
} satisfies OIDCConfig<User>
|
} satisfies OIDCConfig<User>
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
// basePath: "/api/auth",
|
|
||||||
// pages: {
|
|
||||||
// signIn: "/auth/login",
|
|
||||||
// },
|
|
||||||
providers: [customProvider],
|
providers: [customProvider],
|
||||||
redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL,
|
redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL,
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
@@ -72,19 +74,27 @@ export const config = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return session
|
return session
|
||||||
},
|
},
|
||||||
async redirect({ baseUrl, url }) {
|
async redirect({ baseUrl, url }) {
|
||||||
console.log("****** REDIRECT *******")
|
|
||||||
console.log({ baseUrl })
|
|
||||||
console.log({ url })
|
|
||||||
console.log("****** END - REDIRECT *******")
|
|
||||||
// Allows relative callback URLs
|
|
||||||
if (url.startsWith("/")) {
|
if (url.startsWith("/")) {
|
||||||
|
// Allows relative callback URLs
|
||||||
return `${baseUrl}${url}`
|
return `${baseUrl}${url}`
|
||||||
} else if (new URL(url).origin === baseUrl) {
|
} else {
|
||||||
// Allows callback URLs on the same origin
|
// Assume absolute URL
|
||||||
return url
|
try {
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
if (parsedUrl.hostname.endsWith(".scandichotels.com")) {
|
||||||
|
// Allows **.scandichotels.com
|
||||||
|
return url
|
||||||
|
} else if (parsedUrl.origin === baseUrl) {
|
||||||
|
// Allows callback URLs on the same origin
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return baseUrl
|
return baseUrl
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Due to these records being used in next.config.js, and that is required
|
|
||||||
* to be a js file, we use jsdoc to type these.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('@/constants/languages').Lang} Lang
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {Record.<Lang, string>} */
|
|
||||||
export const pageNames = {
|
|
||||||
da: "mine-sider",
|
|
||||||
de: "mein-profil",
|
|
||||||
en: "my-pages",
|
|
||||||
fi: "minun-sivujani",
|
|
||||||
no: "mine-sider",
|
|
||||||
sv: "mina-sidor",
|
|
||||||
}
|
|
||||||
11
constants/routes/authRequired.ts
Normal file
11
constants/routes/authRequired.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { myPages, profile } from "./myPages"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are routes in code we know requires auth
|
||||||
|
*
|
||||||
|
* Some of these are rewritten in next.config.js
|
||||||
|
*/
|
||||||
|
export const authRequired = [
|
||||||
|
...Object.values(myPages),
|
||||||
|
...Object.values(profile),
|
||||||
|
]
|
||||||
25
constants/routes/handleAuth.js
Normal file
25
constants/routes/handleAuth.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* These are routes for login, logout, signup, etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
export const login = {
|
||||||
|
da: "/da/log-pa",
|
||||||
|
de: "/de/anmeldung",
|
||||||
|
en: "/en/login",
|
||||||
|
fi: "/fi/kirjaudu-sisaan",
|
||||||
|
no: "/no/logg-inn",
|
||||||
|
sv: "/sv/logga-in",
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
export const logout = {
|
||||||
|
da: "/da/log-ud",
|
||||||
|
de: "/de/ausloggen",
|
||||||
|
en: "/en/logout",
|
||||||
|
fi: "/fi/kirjautua-ulos",
|
||||||
|
no: "/no/logg-ut",
|
||||||
|
sv: "/sv/logga-ut",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleAuth = [...Object.values(login), ...Object.values(logout)]
|
||||||
28
constants/routes/myPages.js
Normal file
28
constants/routes/myPages.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @file Due to these records being used in next.config.js, and that is required
|
||||||
|
* to be a js file, we use jsdoc to type these.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are routes that define code entries for My pages
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
export const myPages = {
|
||||||
|
da: "/da/mine-sider",
|
||||||
|
de: "/de/mein-profil",
|
||||||
|
en: "/en/my-pages",
|
||||||
|
fi: "/fi/minun-sivujani",
|
||||||
|
no: "/no/mine-sider",
|
||||||
|
sv: "/sv/mina-sidor",
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
export const profile = {
|
||||||
|
da: `${myPages.da}/profil-da`,
|
||||||
|
de: `${myPages.de}/profile-de`,
|
||||||
|
en: `${myPages.en}/profile-en`,
|
||||||
|
fi: `${myPages.fi}/profile-fi`,
|
||||||
|
no: `${myPages.no}/profile-no`,
|
||||||
|
sv: `${myPages.sv}/profile-sv`,
|
||||||
|
}
|
||||||
2
env/server.ts
vendored
2
env/server.ts
vendored
@@ -22,6 +22,7 @@ export const env = createEnv({
|
|||||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
PRINT_QUERY: z.boolean().default(false),
|
PRINT_QUERY: z.boolean().default(false),
|
||||||
REVALIDATE_SECRET: z.string(),
|
REVALIDATE_SECRET: z.string(),
|
||||||
|
WEBVIEW_ENCRYPTION_KEY: z.string(),
|
||||||
},
|
},
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
@@ -44,5 +45,6 @@ export const env = createEnv({
|
|||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
PRINT_QUERY: process.env.PRINT_QUERY,
|
PRINT_QUERY: process.env.PRINT_QUERY,
|
||||||
REVALIDATE_SECRET: process.env.REVALIDATE_SECRET,
|
REVALIDATE_SECRET: process.env.REVALIDATE_SECRET,
|
||||||
|
WEBVIEW_ENCRYPTION_KEY: process.env.WEBVIEW_ENCRYPTION_KEY,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
115
middleware.ts
115
middleware.ts
@@ -1,101 +1,28 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextMiddleware } from "next/server"
|
||||||
|
|
||||||
import { auth } from "@/auth"
|
import * as handleAuth from "./middlewares/handleAuth"
|
||||||
|
import * as authRequired from "./middlewares/authRequired"
|
||||||
|
import * as currentWebLogin from "./middlewares/currentWebLogin"
|
||||||
|
import * as ensureLang from "./middlewares/ensureLang"
|
||||||
|
import * as cmsContent from "@/middlewares/cmsContent"
|
||||||
|
import * as webView from "@/middlewares/webView"
|
||||||
|
|
||||||
import { findLang } from "@/constants/languages"
|
export const middleware: NextMiddleware = async (request, event) => {
|
||||||
import { pageNames } from "@/constants/myPages"
|
const middlewares = [
|
||||||
|
ensureLang,
|
||||||
|
currentWebLogin,
|
||||||
|
authRequired,
|
||||||
|
handleAuth,
|
||||||
|
webView,
|
||||||
|
cmsContent,
|
||||||
|
]
|
||||||
|
|
||||||
import { protectedRoutes } from "@/routes/protected"
|
for (let i = 0; i < middlewares.length; ++i) {
|
||||||
|
const middleware = middlewares[i]
|
||||||
|
|
||||||
import type { NextAuthRequest } from "next-auth"
|
if (middleware.matcher(request)) {
|
||||||
|
return await middleware.middleware(request, event)
|
||||||
export async function publiceMiddleware(request: NextRequest) {
|
}
|
||||||
const { nextUrl } = request
|
|
||||||
const lang = findLang(nextUrl.pathname)
|
|
||||||
|
|
||||||
if (nextUrl.pathname.startsWith(`/${lang}/login`)) {
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unreachable atm
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authedMiddlewareFunction(request: NextAuthRequest) {
|
|
||||||
const { nextUrl } = request
|
|
||||||
const lang = findLang(nextUrl.pathname)!
|
|
||||||
const isLoggedIn = !!request.auth
|
|
||||||
if (isLoggedIn) {
|
|
||||||
/**
|
|
||||||
* Temporary hard rewrite to my pages
|
|
||||||
*/
|
|
||||||
return NextResponse.rewrite(new URL(`/${lang}/${pageNames[lang]}`, nextUrl))
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* Redirect to Loginpage
|
|
||||||
* (Loginpage most likely to be removed)
|
|
||||||
*/
|
|
||||||
return NextResponse.redirect(new URL(`/${lang}/login`, nextUrl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const authedMiddleware = auth(authedMiddlewareFunction)
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
|
||||||
const { nextUrl } = request
|
|
||||||
|
|
||||||
const lang = findLang(nextUrl.pathname)
|
|
||||||
if (!lang) {
|
|
||||||
return Response.json("Not found!!!", { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProtectedRoute = protectedRoutes.includes(nextUrl.pathname)
|
|
||||||
if (isProtectedRoute) {
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* 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. Using the
|
|
||||||
* same function for both works runtime because second argument is just
|
|
||||||
* undefined for middleware and Auth.js handles this properly. But fails in
|
|
||||||
* typings as the second argument doesn't exist for middleware.
|
|
||||||
*
|
|
||||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec62f2f21d7cab65504ba83fb1a9a13be01/packages/next-auth/src/lib/index.ts#L265
|
|
||||||
* https://authjs.dev/reference/nextjs
|
|
||||||
*/
|
|
||||||
// @ts-expect-error: see above
|
|
||||||
return authedMiddleware(request)
|
|
||||||
} else {
|
|
||||||
return publiceMiddleware(request)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
middlewares/authRequired.ts
Normal file
53
middlewares/authRequired.ts
Normal 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
40
middlewares/cmsContent.ts
Normal 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
|
||||||
|
}
|
||||||
31
middlewares/currentWebLogin.ts
Normal file
31
middlewares/currentWebLogin.ts
Normal 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
15
middlewares/ensureLang.ts
Normal 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
15
middlewares/handleAuth.ts
Normal 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
59
middlewares/webView.ts
Normal 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/")
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import createJiti from "jiti"
|
import createJiti from "jiti"
|
||||||
import { pageNames } from "./constants/myPages.js"
|
|
||||||
const jiti = createJiti(new URL(import.meta.url).pathname)
|
|
||||||
|
|
||||||
|
import { login } from "./constants/routes/handleAuth.js"
|
||||||
|
import { myPages, profile } from "./constants/routes/myPages.js"
|
||||||
|
|
||||||
|
const jiti = createJiti(new URL(import.meta.url).pathname)
|
||||||
jiti("./env/server")
|
jiti("./env/server")
|
||||||
jiti("./env/client")
|
jiti("./env/client")
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
poweredByHeader: false,
|
||||||
eslint: { ignoreDuringBuilds: true },
|
eslint: { ignoreDuringBuilds: true },
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
@@ -46,11 +49,23 @@ const nextConfig = {
|
|||||||
rewrites() {
|
rewrites() {
|
||||||
return {
|
return {
|
||||||
beforeFiles: [
|
beforeFiles: [
|
||||||
{ source: `/da/${pageNames.da}`, destination: "/da/my-pages" },
|
{ source: login.da, destination: "/da/login" },
|
||||||
{ source: `/de/${pageNames.de}`, destination: "/de/my-pages" },
|
{ source: login.de, destination: "/de/login" },
|
||||||
{ source: `/fi/${pageNames.fi}`, destination: "/fi/my-pages" },
|
{ source: login.fi, destination: "/fi/login" },
|
||||||
{ source: `/no/${pageNames.no}`, destination: "/no/my-pages" },
|
{ source: login.no, destination: "/no/login" },
|
||||||
{ source: `/sv/${pageNames.sv}`, destination: "/sv/my-pages" },
|
{ source: login.sv, destination: "/sv/login" },
|
||||||
|
|
||||||
|
{ source: myPages.da, destination: "/da/my-pages" },
|
||||||
|
{ source: myPages.de, destination: "/de/my-pages" },
|
||||||
|
{ source: myPages.fi, destination: "/fi/my-pages" },
|
||||||
|
{ source: myPages.no, destination: "/no/my-pages" },
|
||||||
|
{ source: myPages.sv, destination: "/sv/my-pages" },
|
||||||
|
|
||||||
|
{ source: profile.da, destination: "/da/my-pages/profile" },
|
||||||
|
{ source: profile.de, destination: "/de/my-pages/profile" },
|
||||||
|
{ source: profile.fi, destination: "/fi/my-pages/profile" },
|
||||||
|
{ source: profile.no, destination: "/no/my-pages/profile" },
|
||||||
|
{ source: profile.sv, destination: "/sv/my-pages/profile" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "rm -rf .next && next dev",
|
"predev": "rm -rf .next",
|
||||||
|
"dev": "next dev",
|
||||||
"lint": "next lint && tsc",
|
"lint": "next lint && tsc",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"start": "node .next/standalone/server.js",
|
"start": "node .next/standalone/server.js",
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { pageNames } from "@/constants/myPages"
|
|
||||||
|
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
/* Authenticated routes */
|
|
||||||
export const protectedRoutes: string[] = [
|
|
||||||
...Object.keys(pageNames).map(
|
|
||||||
(locale) => `/${locale}/${pageNames[locale as Lang]}`
|
|
||||||
),
|
|
||||||
]
|
|
||||||
13
server/errors/next.ts
Normal file
13
server/errors/next.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
|
export function badRequest() {
|
||||||
|
return new NextResponse("Bad request", {
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function internalServerError() {
|
||||||
|
return new NextResponse("Internal Server Error", {
|
||||||
|
status: 500,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { badRequestError, internalServerError } from "@/server/errors"
|
import { badRequestError, internalServerError } from "@/server/errors/trpc"
|
||||||
import { protectedProcedure, router } from "@/server/trpc"
|
import { protectedProcedure, router } from "@/server/trpc"
|
||||||
import { getUserSchema } from "./output"
|
import { getUserSchema } from "./output"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { initTRPC } from "@trpc/server"
|
|||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { transformer } from "./transformer"
|
import { transformer } from "./transformer"
|
||||||
import { unauthorizedError } from "./errors"
|
import { unauthorizedError } from "./errors/trpc"
|
||||||
|
|
||||||
import type { Context } from "./context"
|
import type { Context } from "./context"
|
||||||
import type { Meta } from "@/types/trpc/meta"
|
import type { Meta } from "@/types/trpc/meta"
|
||||||
|
|||||||
9
types/auth.d.ts
vendored
9
types/auth.d.ts
vendored
@@ -1,5 +1,4 @@
|
|||||||
import "next-auth"
|
import "next-auth"
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
// Module augmentation
|
// Module augmentation
|
||||||
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
||||||
@@ -22,14 +21,6 @@ declare module "next-auth" {
|
|||||||
* Returned by `useSession`, `auth`, contains information about the active session.
|
* Returned by `useSession`, `auth`, contains information about the active session.
|
||||||
*/
|
*/
|
||||||
interface Session {}
|
interface Session {}
|
||||||
|
|
||||||
/**
|
|
||||||
* NextAuthRequest isn't exported by next-auth so we declare a copy
|
|
||||||
* of how they do it to support or switch in middleware.ts
|
|
||||||
*/
|
|
||||||
interface NextAuthRequest extends NextRequest {
|
|
||||||
auth: Session | null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "next-auth/jwt" {
|
declare module "next-auth/jwt" {
|
||||||
|
|||||||
3
types/middleware.ts
Normal file
3
types/middleware.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
export type MiddlewareMatcher = (request: NextRequest) => boolean
|
||||||
3
types/routes.ts
Normal file
3
types/routes.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
export type LangRoute = Record<Lang, string>
|
||||||
41
utils/aes.ts
Normal file
41
utils/aes.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
function base64ToUint8Array(base64String: string) {
|
||||||
|
const binaryString = atob(base64String)
|
||||||
|
const byteArray = new Uint8Array(binaryString.length)
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
byteArray[i] = binaryString.charCodeAt(i)
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
function utf8ToUint8Array(utf8String: string) {
|
||||||
|
return new TextEncoder().encode(utf8String)
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint8ArrayToUtf8(uint8Array: Uint8Array) {
|
||||||
|
return new TextDecoder().decode(uint8Array)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptData(
|
||||||
|
keyBase64: string,
|
||||||
|
ivBase64: string,
|
||||||
|
encryptedDataBase64: string
|
||||||
|
): Promise<string> {
|
||||||
|
const keyBuffer = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
base64ToUint8Array(keyBase64),
|
||||||
|
"AES-CBC",
|
||||||
|
false,
|
||||||
|
["decrypt"]
|
||||||
|
)
|
||||||
|
|
||||||
|
const encryptedDataBuffer = base64ToUint8Array(encryptedDataBase64)
|
||||||
|
const ivBuffer = base64ToUint8Array(ivBase64)
|
||||||
|
const decryptedDataBuffer = await crypto.subtle.decrypt(
|
||||||
|
{ name: "AES-CBC", iv: ivBuffer },
|
||||||
|
keyBuffer,
|
||||||
|
encryptedDataBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
const decryptedData = uint8ArrayToUtf8(new Uint8Array(decryptedDataBuffer))
|
||||||
|
return decryptedData
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user