nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
23.24k stars 3.18k forks source link

provide an example with i18n #9536

Open stefanprobst opened 6 months ago

stefanprobst commented 6 months ago

What is the improvement or update you wish to see?

it would be super helpful to have an official example repo which shows how to combine next-auth v5 with an i18n library like next-intl.

from the current docs, it is not clear to me

thanks :pray:

Is there any context that might help us understand?

Does the docs page already exist? Please link to it.

No response

jero237 commented 6 months ago

Hi! After many hours I've managed to create my own auth middleware to work properly alongside next-intl. Here is the code:

Middleware

import { auth } from "@/auth";
import pages from "./lib/pages";
import { NextRequest, NextResponse } from "next/server";
import createIntlMiddleware from "next-intl/middleware";

const locales = ["en", "es"];
const protectedPages = ["/dashboard/*"];
const authPages = ["/auth/signin", "/auth/signup"];

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale: "es",
  localePrefix: "as-needed",
});

const testPagesRegex = (pages: string[], pathname: string) => {
  const regex = `^(/(${locales.join("|")}))?(${pages
    .map((p) => p.replace("/*", ".*"))
    .join("|")})/?$`;
  return new RegExp(regex, "i").test(pathname);
};

const handleAuth = async (
  req: NextRequest,
  isAuthPage: boolean,
  isProtectedPage: boolean,
) => {
  const session = await auth();
  const isAuth = !!session?.user;

  if (!isAuth && isProtectedPage) {
    let from = req.nextUrl.pathname;
    if (req.nextUrl.search) {
      from += req.nextUrl.search;
    }

    return NextResponse.redirect(
      new URL(
        `${pages.auth.signin()}?from=${encodeURIComponent(from)}`,
        req.url,
      ),
    );
  }

  if (isAuth && isAuthPage) {
    return NextResponse.redirect(new URL(pages.dashboard.root, req.nextUrl));
  }

  return intlMiddleware(req);
};

export default async function middleware(req: NextRequest) {
  const isAuthPage = testPagesRegex(authPages, req.nextUrl.pathname);
  const isProtectedPage = testPagesRegex(protectedPages, req.nextUrl.pathname);

  return await handleAuth(req, isAuthPage, isProtectedPage);
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

use of signIn()

  const searchParams = useSearchParams();

  const handleGoogle = async () => {
    setLoading(true);
    return await signIn("google", {
      callbackUrl: searchParams?.get("from") || pages.dashboard.root,
    });
  };
Adherentman commented 6 months ago

@jero237 How to deal with callbackUrl in signin?

signin("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: true,
      callbackUrl: searchParams?.get("callbackUrl") || "/",
    });

this code alwalys redirect pages.dashboard.root

jero237 commented 6 months ago

@jero237 How to deal with callbackUrl in signin?

signin("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirect: true,
      callbackUrl: searchParams?.get("callbackUrl") || "/",
    });

this code alwalys redirect pages.dashboard.root

great question, I've just updated the comment above

Adherentman commented 5 months ago

@jero237 I change some code with signin after redirect by callbackUrl

  if (isAuth && isAuthPage) {
    const url = req.nextUrl.clone();
    const fromValue = url.searchParams.get("from");
    return NextResponse.redirect(new URL(fromValue ?? "/", req.nextUrl));
  }

This is my all code


const handleAuth = async (req: NextRequest, isAuthPage: boolean, isProtectedPage: boolean) => {
  const session = await auth();
  const isAuth = !!session?.user;

  if (!isAuth && isProtectedPage) {
    let from = req.nextUrl.pathname;
    if (req.nextUrl.search) {
      from += req.nextUrl.search;
    }
    return NextResponse.redirect(new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url));
  }

  if (isAuth && isAuthPage) {
    const url = req.nextUrl.clone();
    const fromValue = url.searchParams.get("from");
    return NextResponse.redirect(new URL(fromValue ?? "/", req.nextUrl));
  }

  return intlMiddleware(req);
};

export default async function middleware(req: NextRequest) {
  const isAuthPage = testPagesRegex(authPages, req.nextUrl.pathname);
  const isProtectedPage = testPagesRegex(protectedPages, req.nextUrl.pathname);

  return await handleAuth(req, isAuthPage, isProtectedPage);
}
soapproject commented 1 month ago

Same requirement from i18next

import { withAuth } from 'next-auth/middleware';
import acceptLanguage from 'accept-language';

const cookieName = 'i18next';
const fallbackLng = 'en';
const languages = ['en', 'zh'];

// Language from cookie/headers/fallback
function getLng(req: NextRequest) {
  if (req.cookies.has(cookieName))
    return acceptLanguage.get(req.cookies.get(cookieName)?.value);

  return acceptLanguage.get(req.headers.get('Accept-Language')) ?? fallbackLng;
};

// Append i18n language to path like: app/[lng]/social-login/page.tsx
// example: /social-login -> /en/social-login
function handleI18n(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/api')) return;

  if (
    !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    const lng = getLng(req);
    return NextResponse.redirect(
      new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
    );
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer')!);
    const lngInReferer = languages.find(l =>
      refererUrl.pathname.startsWith(`/${l}`)
    );
    const response = NextResponse.next();
    if (lngInReferer)
      response.cookies.set(cookieName, lngInReferer, { sameSite: 'lax' });
    return response;
  }
}

function middleware(req: NextRequest) {
  console.log('Running middleware!'); // <--------- This will not run if not authed
  return handleI18n(req) ?? NextResponse.next();
}

export default withAuth(middleware, {
  pages: {
    signIn: '/social-login', // <--------- This will not redirect to /en/social-login
    signOut: '/social-login',
  },
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|assets|favicon.ico|sw.js|fonts).*)'],
};

will be nice if we can access req in NextAuthMiddlewareOptions

export default withAuth(middleware, (req) => ({
  pages: {
    signIn: `${getLng(req)}/social-login`,
    signOut: `${getLng(req)}/social-login`,
  },
}));
soapproject commented 1 month ago

update: I just notic v5 comming soon and it will be much easyer. But so far this works with v4.

  import acceptLanguage from 'accept-language';
import { withAuth, type NextRequestWithAuth } from 'next-auth/middleware';
import { NextResponse, type NextRequest } from 'next/server';
import { cookieName, fallbackLng, languages } from './i18n';

acceptLanguage.languages([...languages]);

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|assets|favicon.ico|sw.js|manifest.json|fonts).*)',
  ],
};

const getLng = (req: NextRequest) => {
  if (req.cookies.has(cookieName))
    return acceptLanguage.get(req.cookies.get(cookieName)?.value);

  return acceptLanguage.get(req.headers.get('Accept-Language')) ?? fallbackLng;
};

const handleI18n = (req: NextRequest) => {
  if (req.nextUrl.pathname.startsWith('/api')) return;

  const pathnameStartsWithLanguage = languages.some(loc =>
    req.nextUrl.pathname.startsWith(`/${loc}`)
  );
  if (
    !pathnameStartsWithLanguage &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    const lng = getLng(req);
    return NextResponse.redirect(
      new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
    );
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer')!);
    const lngInReferer = languages.find(l =>
      refererUrl.pathname.startsWith(`/${l}`)
    );
    const response = NextResponse.next();
    if (lngInReferer) {
      response.cookies.set(cookieName, lngInReferer, { sameSite: 'lax' });
    }
    return response;
  }

  return;
};

const handleAuth = (req: NextRequestWithAuth) => {
  if (req.nextUrl.pathname.includes('/dmz')) return NextResponse.next();

  const token = req.nextauth.token;
  if (!token)
    return NextResponse.redirect(new URL(`/dmz/social-login`, req.url));

  return;
};

const middleware = (req: NextRequestWithAuth) => {
  const i18nRes = handleI18n(req);
  if (i18nRes) return i18nRes;

  const authRes = handleAuth(req);
  if (authRes) return authRes;

  return NextResponse.next();
};

export default withAuth(middleware, {
  callbacks: {
    authorized() {
      /**
       * Trick, tell next-auth we are always authorized, so middleware callback will not skip.
       * then we handle token check and redirect to login page our self.
       */
      return true;
    },
  },
});
ScreamZ commented 2 weeks ago

@balazsorban44 save us please 🥺