Enhancing next-auth and i18n functionality with middleware in the latest version of next.js (13) using the app

Currently, I have successfully set up next-auth in my next.js 13 project with app router, and it is functioning as expected. My next step is to incorporate internationalization into my app following the guidelines provided in the next.js documentation. However, I am facing challenges when trying to integrate the two functionalities seamlessly.

Middleware Setup for Next-Auth

I need all routes under /user/ to be secure and protected.

export { default } from "next-auth/middleware"
export const config = { matcher: ["/(user/.*)"] }

Middleware for Next-Auth and Internationalization (i18n)

Here is the progress I've made so far. The i18n implementation appears to be functioning correctly, but the routes under /user/ are not adequately protected.

import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { NextResponse } from 'next/server'

export { default } from "next-auth/middleware"

let locales = ['en', 'de']
let defaultLocale = 'en'

function getLocale(request) {
    let headers = { 'accept-language': 'en' }
    let languages = new Negotiator({ headers }).languages()
    return match(languages, locales, defaultLocale) // -> 'en'
}

export function middleware(request) {
    // Check if there is any supported locale in the pathname
    const pathname = request.nextUrl.pathname
    const pathnameIsMissingLocale = locales.every(
        (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
    )

    // Redirect if there is no locale
    if (pathnameIsMissingLocale) {
        const locale = getLocale(request)

        // e.g. incoming request is /products
        // The new URL is now /en/products
        return NextResponse.redirect(
            new URL(`/${locale}/${pathname}`, request.url)
        )
    }
}

export const config = {
    // '/(\w{2}/user/.*)' from nextauth (\w{2} because of /en/ or /de/); '/((?!_next).*)' from i18n
    matcher: ['/(\w{2}/user/.*)', '/((?!_next).*)'],
}

I am seeking guidance on how to effectively combine these two functionalities?

Answer №1

After rearranging things, I've finally discovered a more effective solution that suits my needs.

"next-auth": "5.0.0-beta.5",
"next-i18n-router": "^5.2.0",

Simply utilize the NextAuth middleware configuration:

middleware.ts

import NextAuth from 'next-auth'
import { authConfig } from './auth.config'

export default NextAuth(authConfig).auth

export const config = {
  matcher: '/((?!api|static|.*\\..*|_next).*)'
}

Then combine authorized handler and i18nRouter in the auth.config file:

auth.config.ts

import { i18nRouter } from 'next-i18n-router'
import type { NextAuthConfig } from 'next-auth'
import { i18nConfig } from '@/locale/config'

const getLocaleFromPath = (pathname: string) => {
  const localeFromPathRegex = new RegExp(`^/(${i18nConfig.locales.join('|')})?`)
  const localeFromPath = pathname.match(localeFromPathRegex)?.[1]
  return { locale: localeFromPath, path: localeFromPath ? `/${localeFromPath}` : '' }
}

const checkCurrentRoute = (pathname: string, locale?: string) => {
  const checkPathnameRegex = (pattern: string | RegExp) => {
    const rootRegex = new RegExp(pattern)
    return Boolean(pathname.match(rootRegex))
  }

  return {
    root: checkPathnameRegex(`^/(${locale})?$`),
    dashboard: checkPathnameRegex(`^(/${locale})?/dashboard.*`),
    login: checkPathnameRegex(`^(/${locale})?/login.*`)
  }
}

export const authConfig = {
  pages: {
    signIn: '/login'
  },
  callbacks: {
    authorized({ auth, request }) {
      const { nextUrl } = request

      const locale = getLocaleFromPath(nextUrl.pathname)
      const dashboardUrl = new URL(`${locale.path}/dashboard`, nextUrl)

      const { root: isOnRoot, dashboard: isOnDashboard, login: isOnLogin } = checkCurrentRoute(nextUrl.pathname, locale.locale)

      const isLoggedIn = !!auth?.user

      if (isOnRoot || (isLoggedIn && !isOnDashboard)) {
        // If on root or logged in but not on dashboard, redirect to dashboard
        return Response.redirect(dashboardUrl)
      }

      if ((isOnLogin && !isLoggedIn) || (isOnDashboard && isLoggedIn)) {
        // Not logged in but on login OR logged in and on dashboard => allow access
        return i18nRouter(request, i18nConfig)
      }

      // Not logged in and not on login or dashboard => redirect to login page
      return false
    }
  },

  providers: []
} satisfies NextAuthConfig

@/locale/config.ts

export const i18nConfig = {
  locales: ['en', 'es'],
  defaultLocale: 'en'
}

This approach has proven successful for me. By manually extracting the locale from the pathname, I can reuse it in redirections. If no unauthorized redirection is necessary, simply return the i18nRouter so i18n remains enabled.

For additional configuration details, I followed this guide: NextAuth tutorial

Answer №2

My approach:

middleware.js

export function middleware(request) {
  let locales = ['en', 'fr'];
  let defaultLocale = 'en';
  // Checking for supported locale in the pathname
  const { pathname } = request.nextUrl;
  const isPathAllowed = ['/img', 'img', '/api'].some((allowedPath) =>
    pathname.startsWith(`${allowedPath}`),
  );
  if (isPathAllowed) return;
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
  );

  if (pathnameHasLocale) return;

  // Redirecting if there is no locale
  const locale = defaultLocale;
  request.nextUrl.pathname = `/${locale}${pathname}`;
  // Example: incoming request is /products
  // The new URL will be /en/products
  return Response.redirect(request.nextUrl);
}

export const config = {
  matcher: [
    // Skip all internal paths (_next)
    '/((?!_next).*)',
    // Optional: only run on root (/) URL
    // '/'
  ],
};

en.json

{
  "Index": {
    "title": "en title",
    "description": "en description"
    }
}

fr.json

{
  "Index": {
    "title": "french title",
    "description": "french description"
    }
}

messages.js

import en from './en.json';
import fr from './fr.json';

export const getSelectedLanguage = (lang) => {
  switch (lang) {
    case 'en':
      return en;
    case 'fr':
      return fr;
    default:
      return null;
  }
};

layout.js

'use client';

import '@/app/globals.css';
import { SessionProvider } from 'next-auth/react';
import { NextIntlClientProvider } from 'next-intl';
import { getSelectedLanguage } from '@/internationalization/messages/messages';

export default function RootLayout({ children, params: { lang } }) {
  const messages = getSelectedLanguage(lang);

  return (
    <html lang={lang}>
      <body>
        <NextIntlClientProvider locale={lang} messages={messages}>
          <SessionProvider>{children}</SessionProvider>
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

page.jsx

'use client';

import Link from 'next/link';
import { useLocale, useTranslations } from 'next-intl';

const HomePage = () => {
  const t = useTranslations('Index');
  const locale = useLocale();

  return (
    <>
      <main>
          <div className="container">
            <h1>{t('title')}</h1>
          </div>
      </main>
   </>
  );
}

Answer №3

Thanks to the assistance of @mateen-kiani and phind.com, I was able to successfully combine two middlewares for my project:

import { NextResponse } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import nextAuthMiddleware from "next-auth/middleware"

let locales = ['en', 'de']
let defaultLocale = 'en'

function getLocale(request) {
    let headers = { 'accept-language': 'en' }
    let languages = new Negotiator({ headers }).languages()
    return match(languages, locales, defaultLocale) // -> 'en'
}

export function middleware(request) {
    // Handling exceptions
    const pathname = request.nextUrl.pathname
    const isException = ['/img', '/preview', '/icons', '/logo.svg', '/api', '/manifest.json', '/sw.js'].some((allowedPath) =>
        pathname.startsWith(`${allowedPath}`),
    );
    if (isException) return;

    // Check for supported locale in the pathname
    const pathnameIsMissingLocale = locales.every(
        (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
    )

    // Redirect if no locale is found
    if (pathnameIsMissingLocale) {
        const locale = getLocale(request)
        return NextResponse.redirect(
            new URL(`/${locale}/${pathname}`, request.url)
        )
    }

    // Checking if authentication is required
    if (pathname.includes("/user")) {
        // Check & handle login status
        const nextAuthResponse = nextAuthMiddleware(request)
        if (nextAuthResponse) {
        return nextAuthResponse
        }
    }

    // Proceed if there is no NextAuth middleware response
    return NextResponse.next()
}

export const config = {
    matcher: [
        '/((?!_next).*)',
    ],
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it possible to postpone sending a message on Discord until a certain event has been successfully completed?

After the User leaves the Discord, I attempted to create a RichEmbed Message that would include a random GIF from Giphy. The GIF was meant to be generated using the getGiphyPic() function with the help of this node module: https://github.com/risan/giphy-ra ...

Sharing asynchronous data between AngularJS controllers

Among the multitude of discussions on sharing data between controllers, I have yet to come across a satisfactory solution for my particular scenario. In my setup, one controller fetches data asynchronously using promises. This controller then creates a co ...

error": "Unable to access undefined properties (reading 'SecretString')

Encountering the error message Cannot read properties of undefined (reading 'SecretString') when utilizing @aws-sdk/client-secrets-manager? It's a sign that you should consider updating your code to accommodate the latest version. ...

Utilizing the "apply" method with JavaScript OOP callbacks for dynamic context management

Encountering an issue while creating a callback in my custom EACH function utilizing Object-Oriented Programming principles. To achieve this, I have developed a JavaScript library that emulates the familiar habits of JQUERY. Experience it in action by ch ...

Modifying the DOM within a getJSON callback

My current challenge involves fetching data from the YouTube API and displaying it on my website. However, I am facing an issue where the DOM changes made inside the getJSON's callback function are not reflecting on the webpage. Even though I can see ...

The error message "NoSuchSessionError: invalid session id" pops up in Selenium, despite the fact that the application is running smoothly

Scenario and Background: I have recently developed a script to access an external website and extract specific data. The script's purpose is to retrieve grades of students and convert them into usable data for plotting. In order to streamline the dat ...

retrieve only the following occurrence throughout the entire file

I am looking to target only the first occurrence of an element in the document after a clicked element. Here is my current approach: $('.class').click(function(){ var x = $(this).nextAll('.anotherclass').first(); //do something ...

Count duplicated values in an array of objects using JavaScript ES6

I am working on creating a filter for my list of products to count all producers and display them as follows: Apple (3) I have managed to eliminate duplicates from the array: ["Apple", "Apple", "Apple"] using this helpful link: Get all non-unique values ...

Multiple occurrences of the cookie found in the document.cookie

My client is using IE9 and the server is asp.net (specifically a SharePoint application page). Within the Page_Load method of a page, I implemented the following code: Response.Cookies["XXXXX"].Value = tabtitles.IndexOf(Request.Params["tab"]).ToString(); ...

Managing authentication tokens in next.js

I'm currently working on a next.js app that requires authorization for certain functionalities. In the past with CRA, I would store the token in a config.js file and easily import, use, and update it throughout my application. Here is an example of ho ...

When you duplicate the React State object and make changes to the copied object, it directly affects

When attempting to duplicate a state object, I noticed that the state object is being modified directly in the code snippet below: @boundMethod private _onClickDeleteAttachment(attachmentName: string): void { console.log("_onClickDeleteAttachment | th ...

Setting up Webpack for Node applications

My current challenge involves configuring Webpack for a node app that already exists. I am encountering various issues and struggling to find solutions or even know where to begin. Situation The project's folder structure is as follows: +---app +-- ...

Display search results in Rails without needing to refresh the entire tab

I have a search functionality incorporated within a Bootstrap tab, and I aim to display the search results dynamically without reloading the entire page, specifically within the tab itself. Despite adding 'remote: true' to the form_tag and incor ...

Obtaining data from a callback function within a NodeJS application

There is a function in my code that performs a backend call to retrieve an array of names. The function looks something like this: module.exports.getTxnList = function(index, callback) { ....some operations ..... .... callback(null, respon ...

Tips for enabling mouse wheel zoom on a three.js scene

I have a straightforward three.js graphic that I wanted to make zoomable by using the mouse wheel. I attempted to implement solutions from this and this question in order to achieve this feature. Ideally, I want users to be able to zoom in or out of the gr ...

What is the best way to transfer scope to a callback function in the context of node-mysql?

When running the code below, I encounter an error that says client is not defined: var mysql = require('mysql'); var conf = { 'database':'database', 'user':'user', 'password':'password ...

Implementing JavaScript to showcase a list extracted from an API dataset

I'm currently undertaking a project where I am integrating an API from a specific website onto my own webpage. { "data": [{ "truckplanNo":"TCTTV___0D010013", "truckplanType":"COLLECTION", " ...

Encountering an error message stating, "Unable to assign value to 'onclick' property"

Currently, I am at a beginner level in Javascript. While working on some exercises, I encountered an issue with the 'onclick' function as mentioned above. Despite going through other queries in this forum, I have not found a solution that works ...

Adjust the scroll bar to move in the reverse direction

I'm trying to modify the behavior of an overlay div that moves when scrolling. Currently, it works as expected, but I want the scroll bar to move in the opposite direction. For example, when I scroll the content to the right, I want the scroll bar to ...

What is the process for assigning specific tags to specific items within an array?

As I navigate through a list of students, I am encountering an issue with my tag functionality. Currently, when a student adds a tag to their container, it is being added to every student's tags instead of just the intended student. How can I modify t ...