Open stefanprobst opened 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,
});
};
@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 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
@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);
}
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`,
},
}));
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;
},
},
});
@balazsorban44 save us please 🥺
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 likenext-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