diff --git a/.env.local.example b/.env.local.example index 1ffe6074b..911804e9c 100644 --- a/.env.local.example +++ b/.env.local.example @@ -16,3 +16,4 @@ NEXTAUTH_REDIRECT_PROXY_URL="http://localhost:3000/api/auth" NEXTAUTH_SECRET="" NEXTAUTH_URL="http://localhost:3000/api/auth" REVALIDATE_SECRET="" +WEBVIEW_ENCRYPTION_KEY="MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=" diff --git a/app/[lang]/(live)/(protected)/layout.tsx b/app/[lang]/(live)/(protected)/layout.tsx index 60abbe46f..28849ab99 100644 --- a/app/[lang]/(live)/(protected)/layout.tsx +++ b/app/[lang]/(live)/(protected)/layout.tsx @@ -1,6 +1,4 @@ -import { redirect } from "next/navigation" - -import { auth } from "@/auth" +import { auth, signIn } from "@/auth" import type { LangParams, LayoutArgs } from "@/types/params" @@ -14,7 +12,10 @@ export default async function ProtectedLayout({ * protected route group is actually protected. */ if (!session) { - return redirect(`/${params.lang}/login`) + await signIn("curity", undefined, { + ui_locales: params.lang, + }) + return null } return <>{children} diff --git a/app/[lang]/(live)/(public)/login/page.tsx b/app/[lang]/(live)/(public)/login/page.tsx deleted file mode 100644 index 4c9e43010..000000000 --- a/app/[lang]/(live)/(public)/login/page.tsx +++ /dev/null @@ -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) { - async function login() { - "use server" - await signIn("curity", { - redirectTo: `/${params.lang}/${pageNames[params.lang]}`, - }) - } - - return ( -
-
- -
-
- ) -} diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts new file mode 100644 index 000000000..8b6ed8105 --- /dev/null +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -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() +} diff --git a/app/[lang]/(live-current)/layout.tsx b/app/[lang]/(live-current)/layout.tsx index 9538114ad..fc1c07719 100644 --- a/app/[lang]/(live-current)/layout.tsx +++ b/app/[lang]/(live-current)/layout.tsx @@ -8,7 +8,6 @@ import SkipToMainContent from "@/components/SkipToMainContent" import type { Metadata } from "next" import type { LangParams, LayoutArgs } from "@/types/params" -import VwoScript from "@/components/Current/NewVWOScript" export const fetchCache = "default-no-store" @@ -40,10 +39,6 @@ export default function RootLayout({ id="Cookiebot" src="https://consent.cookiebot.com/uc.js" /> - {/* diff --git a/app/[lang]/webview/layout.tsx b/app/[lang]/webview/layout.tsx new file mode 100644 index 000000000..5409013a8 --- /dev/null +++ b/app/[lang]/webview/layout.tsx @@ -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>) { + return ( + + {children} + + ) +} diff --git a/app/[lang]/webview/test/page.tsx b/app/[lang]/webview/test/page.tsx new file mode 100644 index 000000000..e482db7a6 --- /dev/null +++ b/app/[lang]/webview/test/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Hello World from Webview", +} + +export default function WebViewTestPage() { + return ( +
+
+

Hello From WebView Test Page!

+
+
+ ) +} diff --git a/auth.ts b/auth.ts index 495c050c8..f61ca286c 100644 --- a/auth.ts +++ b/auth.ts @@ -19,6 +19,12 @@ const customProvider = { url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize`, params: { 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: { @@ -39,10 +45,6 @@ const customProvider = { } satisfies OIDCConfig export const config = { - // basePath: "/api/auth", - // pages: { - // signIn: "/auth/login", - // }, providers: [customProvider], redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL, trustHost: true, @@ -72,19 +74,27 @@ export const config = { }, } } + return session }, async redirect({ baseUrl, url }) { - console.log("****** REDIRECT *******") - console.log({ baseUrl }) - console.log({ url }) - console.log("****** END - REDIRECT *******") - // Allows relative callback URLs if (url.startsWith("/")) { + // Allows relative callback URLs return `${baseUrl}${url}` - } else if (new URL(url).origin === baseUrl) { - // Allows callback URLs on the same origin - return url + } else { + // Assume absolute 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 }, diff --git a/constants/myPages.js b/constants/myPages.js deleted file mode 100644 index ee6822aac..000000000 --- a/constants/myPages.js +++ /dev/null @@ -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.} */ -export const pageNames = { - da: "mine-sider", - de: "mein-profil", - en: "my-pages", - fi: "minun-sivujani", - no: "mine-sider", - sv: "mina-sidor", -} diff --git a/constants/routes/authRequired.ts b/constants/routes/authRequired.ts new file mode 100644 index 000000000..99574989a --- /dev/null +++ b/constants/routes/authRequired.ts @@ -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), +] diff --git a/constants/routes/handleAuth.js b/constants/routes/handleAuth.js new file mode 100644 index 000000000..042ffba6d --- /dev/null +++ b/constants/routes/handleAuth.js @@ -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)] diff --git a/constants/routes/myPages.js b/constants/routes/myPages.js new file mode 100644 index 000000000..0f4e98f55 --- /dev/null +++ b/constants/routes/myPages.js @@ -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`, +} diff --git a/env/server.ts b/env/server.ts index cf2d21066..738feb01f 100644 --- a/env/server.ts +++ b/env/server.ts @@ -22,6 +22,7 @@ export const env = createEnv({ NODE_ENV: z.enum(["development", "test", "production"]), PRINT_QUERY: z.boolean().default(false), REVALIDATE_SECRET: z.string(), + WEBVIEW_ENCRYPTION_KEY: z.string(), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -44,5 +45,6 @@ export const env = createEnv({ NODE_ENV: process.env.NODE_ENV, PRINT_QUERY: process.env.PRINT_QUERY, REVALIDATE_SECRET: process.env.REVALIDATE_SECRET, + WEBVIEW_ENCRYPTION_KEY: process.env.WEBVIEW_ENCRYPTION_KEY, }, }) diff --git a/middleware.ts b/middleware.ts index 9c0c98bc3..d048ffd2d 100644 --- a/middleware.ts +++ b/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" -import { pageNames } from "@/constants/myPages" +export const middleware: NextMiddleware = async (request, event) => { + 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" - -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) + if (middleware.matcher(request)) { + return await middleware.middleware(request, event) + } } } diff --git a/middlewares/authRequired.ts b/middlewares/authRequired.ts new file mode 100644 index 000000000..836cb0979 --- /dev/null +++ b/middlewares/authRequired.ts @@ -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) +} diff --git a/middlewares/cmsContent.ts b/middlewares/cmsContent.ts new file mode 100644 index 000000000..097322576 --- /dev/null +++ b/middlewares/cmsContent.ts @@ -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 +} diff --git a/middlewares/currentWebLogin.ts b/middlewares/currentWebLogin.ts new file mode 100644 index 000000000..6a9751183 --- /dev/null +++ b/middlewares/currentWebLogin.ts @@ -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") +} diff --git a/middlewares/ensureLang.ts b/middlewares/ensureLang.ts new file mode 100644 index 000000000..513c2b6c7 --- /dev/null +++ b/middlewares/ensureLang.ts @@ -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) +} diff --git a/middlewares/handleAuth.ts b/middlewares/handleAuth.ts new file mode 100644 index 000000000..44f127a89 --- /dev/null +++ b/middlewares/handleAuth.ts @@ -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) +} diff --git a/middlewares/webView.ts b/middlewares/webView.ts new file mode 100644 index 000000000..99dcbec28 --- /dev/null +++ b/middlewares/webView.ts @@ -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/") +} diff --git a/next.config.js b/next.config.js index 930cbafb1..2ee608830 100644 --- a/next.config.js +++ b/next.config.js @@ -1,12 +1,15 @@ 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/client") /** @type {import('next').NextConfig} */ const nextConfig = { + poweredByHeader: false, eslint: { ignoreDuringBuilds: true }, images: { remotePatterns: [ @@ -46,11 +49,23 @@ const nextConfig = { rewrites() { return { beforeFiles: [ - { source: `/da/${pageNames.da}`, destination: "/da/my-pages" }, - { source: `/de/${pageNames.de}`, destination: "/de/my-pages" }, - { source: `/fi/${pageNames.fi}`, destination: "/fi/my-pages" }, - { source: `/no/${pageNames.no}`, destination: "/no/my-pages" }, - { source: `/sv/${pageNames.sv}`, destination: "/sv/my-pages" }, + { source: login.da, destination: "/da/login" }, + { source: login.de, destination: "/de/login" }, + { source: login.fi, destination: "/fi/login" }, + { source: login.no, destination: "/no/login" }, + { 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" }, ], } }, diff --git a/package.json b/package.json index a8fafe234..3ac6c5946 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "build": "next build", - "dev": "rm -rf .next && next dev", + "predev": "rm -rf .next", + "dev": "next dev", "lint": "next lint && tsc", "prepare": "husky install", "start": "node .next/standalone/server.js", diff --git a/routes/protected.ts b/routes/protected.ts deleted file mode 100644 index d73bad4bf..000000000 --- a/routes/protected.ts +++ /dev/null @@ -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]}` - ), -] diff --git a/server/errors/next.ts b/server/errors/next.ts new file mode 100644 index 000000000..9fd128c1d --- /dev/null +++ b/server/errors/next.ts @@ -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, + }) +} diff --git a/server/errors.ts b/server/errors/trpc.ts similarity index 100% rename from server/errors.ts rename to server/errors/trpc.ts diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index fbf983060..83ec961e7 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,4 +1,4 @@ -import { badRequestError, internalServerError } from "@/server/errors" +import { badRequestError, internalServerError } from "@/server/errors/trpc" import { protectedProcedure, router } from "@/server/trpc" import { getUserSchema } from "./output" diff --git a/server/trpc.ts b/server/trpc.ts index 2615eae63..53aa09f96 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -2,7 +2,7 @@ import { initTRPC } from "@trpc/server" import { env } from "@/env/server" import { transformer } from "./transformer" -import { unauthorizedError } from "./errors" +import { unauthorizedError } from "./errors/trpc" import type { Context } from "./context" import type { Meta } from "@/types/trpc/meta" diff --git a/types/auth.d.ts b/types/auth.d.ts index 13a946865..1b6db41a6 100644 --- a/types/auth.d.ts +++ b/types/auth.d.ts @@ -1,5 +1,4 @@ import "next-auth" -import type { NextRequest } from "next/server" // Module augmentation // 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. */ 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" { diff --git a/types/middleware.ts b/types/middleware.ts new file mode 100644 index 000000000..ac9e53699 --- /dev/null +++ b/types/middleware.ts @@ -0,0 +1,3 @@ +import type { NextRequest } from "next/server" + +export type MiddlewareMatcher = (request: NextRequest) => boolean diff --git a/types/routes.ts b/types/routes.ts new file mode 100644 index 000000000..a2d25c8bc --- /dev/null +++ b/types/routes.ts @@ -0,0 +1,3 @@ +import { Lang } from "@/constants/languages" + +export type LangRoute = Record diff --git a/utils/aes.ts b/utils/aes.ts new file mode 100644 index 000000000..620d82017 --- /dev/null +++ b/utils/aes.ts @@ -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 { + 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 +}