feat: implement middleware for route protection and localization

- Enhanced middleware functionality to manage access for protected routes based on user authentication status.
- Added logic to redirect users to the login page if they attempt to access protected routes while not logged in.
- Implemented a utility function to strip locale from the pathname for better route handling.
- Updated route definitions to categorize routes that are not accessible to logged-in users and those that require authentication.
- Improved comments and logging for better traceability and understanding of middleware operations.
This commit is contained in:
javayhu 2025-04-11 12:33:31 +08:00
parent 313625577c
commit 72326403a0
4 changed files with 76 additions and 78 deletions

View File

@ -51,9 +51,8 @@ export function CheckoutButton({
metadata,
});
// Redirect to checkout
// Redirect to checkout page
if (result && result.data?.success && result.data.data?.url) {
// redirect to checkout page
window.location.href = result.data.data?.url;
} else {
console.error('Create checkout session error, result:', result);

View File

@ -33,19 +33,4 @@ export const routing = defineRouting({
// The prefix to use for the locale in the URL
// https://next-intl.dev/docs/routing#locale-prefix
localePrefix: 'as-needed',
// The pathnames for each locale
// https://next-intl.dev/docs/routing#pathnames
//
// https://next-intl.dev/docs/routing/navigation#link
// if we set pathnames, we need to use pathname in LocaleLink
// pathnames: {
// // used in sietmap.ts
// "/": "/",
// // used in blog pages
// "/blog/[...slug]": "/blog/[...slug]",
// "/blog/category/[slug]": "/blog/category/[slug]",
// },
});
// export type Pathnames = keyof typeof routing.pathnames;
// export type Locale = (typeof routing.locales)[number];

View File

@ -1,30 +1,72 @@
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
import { NextRequest, NextResponse } from 'next/server';
import { LOCALES, routing } from './i18n/routing';
import { getSession } from './lib/server';
import { DEFAULT_LOGIN_REDIRECT, protectedRoutes, routesNotAllowedByLoggedInUsers } from './routes';
export default createMiddleware(routing);
const intlMiddleware = createMiddleware(routing);
// TODO: add middleware rules for protected routes
export default async function middleware(req: NextRequest) {
const { nextUrl } = req;
console.log('>> middleware start, pathname', nextUrl.pathname);
const session = await getSession();
const isLoggedIn = !!session;
// console.log('middleware, isLoggedIn', isLoggedIn);
// Get the pathname of the request (e.g. /zh/dashboard to /dashboard)
const pathnameWithoutLocale = getPathnameWithoutLocale(nextUrl.pathname, LOCALES);
// If the route can not be accessed by logged in users, redirect if the user is logged in
if (isLoggedIn) {
const isNotAllowedRoute = routesNotAllowedByLoggedInUsers.some(route => new RegExp(`^${route}$`).test(pathnameWithoutLocale));
if (isNotAllowedRoute) {
console.log('<< middleware end, not allowed route, already logged in, redirecting to dashboard');
return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
}
}
const isProtectedRoute = protectedRoutes.some(route => new RegExp(`^${route}$`).test(pathnameWithoutLocale));
// console.log('middleware, isProtectedRoute', isProtectedRoute);
// If the route is a protected route, redirect to login if user is not logged in
if (!isLoggedIn && isProtectedRoute) {
let callbackUrl = nextUrl.pathname;
if (nextUrl.search) {
callbackUrl += nextUrl.search;
}
const encodedCallbackUrl = encodeURIComponent(callbackUrl);
console.log('<< middleware end, not logged in, redirecting to login, callbackUrl', callbackUrl);
return NextResponse.redirect(
new URL(`/auth/login?callbackUrl=${encodedCallbackUrl}`, nextUrl),
);
}
// Apply intlMiddleware for all routes
console.log('<< middleware end, applying intlMiddleware');
return intlMiddleware(req);
}
/**
* Get the pathname of the request (e.g. /zh/dashboard to /dashboard)
*/
function getPathnameWithoutLocale(pathname: string, locales: string[]): string {
const localePattern = new RegExp(`^/(${locales.join('|')})/`);
return pathname.replace(localePattern, '/');
}
/**
* Next.js internationalized routing
* specify the routes the middleware applies to
*
* https://next-intl.dev/docs/routing#base-path
*/
export const config = {
// The `matcher` is relative to the `basePath`
matcher: [
// This entry handles the root of the base
// path and should always be included
'/',
// Set a cookie to remember the previous locale for
// all requests that have a locale prefix
'/(zh|en)/:path*',
// Enable redirects that add missing locales
// (e.g. `/pathnames` -> `/zh/pathnames`)
// Exclude API routes and other Next.js internal routes
// if not exclude api routes, auth routes will not work
matcher: [
// Match all pathnames except for
// - … if they start with `/api`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
'/((?!api|_next|_vercel|.*\\..*).*)',
],
};

View File

@ -6,10 +6,10 @@
export enum Routes {
Root = '/',
// pages
FAQ = '/#faq',
Features = '/#features',
Pricing = '/pricing',
Blog = '/blog',
Docs = '/docs',
About = '/about',
@ -17,7 +17,6 @@ export enum Routes {
Waitlist = '/waitlist',
Changelog = '/changelog',
Roadmap = 'https://mksaas.featurebase.app',
CookiePolicy = '/cookie',
PrivacyPolicy = '/privacy',
TermsOfService = '/terms',
@ -29,11 +28,6 @@ export enum Routes {
ForgotPassword = '/auth/forgot-password',
ResetPassword = '/auth/reset-password',
AIText = '/ai/text',
AIImage = '/ai/image',
AIVideo = '/ai/video',
AIAudio = '/ai/audio',
// dashboard routes
Dashboard = '/dashboard',
SettingsProfile = '/settings/profile',
@ -41,6 +35,12 @@ export enum Routes {
SettingsSecurity = '/settings/security',
SettingsNotifications = '/settings/notifications',
// AI routes
AIText = '/ai/text',
AIImage = '/ai/image',
AIVideo = '/ai/video',
AIAudio = '/ai/audio',
// Block routes
HeroBlocks = '/blocks/hero-section',
LogoBlocks = '/blocks/logo-cloud',
@ -60,51 +60,23 @@ export enum Routes {
}
/**
* An array of routes that are accessible to the public
* These routes do not require authentication
* @type {string[]}
* The routes that can not be accessed by logged in users
*/
export const publicRoutes = [
'/',
// pages
'/blog(/.*)?',
'/blocks(/.*)?',
'/terms-of-service(/.*)?',
'/privacy-policy(/.*)?',
'/cookie-policy(/.*)?',
'/about(/.*)?',
'/contact(/.*)?',
'/waitlist(/.*)?',
'/changelog(/.*)?',
// unsubscribe newsletter
'/unsubscribe(/.*)?',
// stripe webhook
'/api/webhook',
// og images
'/api/og',
];
/**
* The routes for the authentication pages
*/
export const authRoutes = [
Routes.AuthError,
export const routesNotAllowedByLoggedInUsers = [
Routes.Login,
Routes.Register,
Routes.ForgotPassword,
Routes.ResetPassword,
];
/**
* The prefix for API authentication routes
* Routes that start with this prefix are used for API authentication purposes
* @type {string}
* The routes that are protected and require authentication
*/
export const apiAuthPrefix = '/api/auth';
export const protectedRoutes = [
Routes.Dashboard,
Routes.SettingsProfile,
Routes.SettingsBilling,
Routes.SettingsSecurity,
Routes.SettingsNotifications,
];
/**
* The default redirect path after logging in