Files
myeasycms-v2/apps/web/middleware.ts
giancarlo 51a90bde83 Improve user session handling in middleware
The changes add user session handling directly in the middleware. This ensures the user data is fetched at the start of a request and then passed on to route handlers, reducing repeated data fetching. Also, these improvements include adjustments for how sign-out and auth-change events are managed, particularly when the user session state changes. Additionally, it corrects the error response from useUser hook to return `undefined` instead of `null`.
2024-04-28 13:20:25 +07:00

221 lines
6.1 KiB
TypeScript

import type { NextRequest } from 'next/server';
import { NextResponse, URLPattern } from 'next/server';
import type { UserResponse } from '@supabase/supabase-js';
import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
import { createMiddlewareClient } from '@kit/supabase/middleware-client';
import appConfig from '~/config/app.config';
import pathsConfig from '~/config/paths.config';
const CSRF_SECRET_COOKIE = 'csrfSecret';
const NEXT_ACTION_HEADER = 'next-action';
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|locales|assets|api/*).*)',
],
};
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const supabase = createMiddlewareClient(request, response);
// get the user from the session (no matter if it's logged in or not)
const userResponse = await supabase.auth.getUser();
// set a unique request ID for each request
// this helps us log and trace requests
setRequestId(request);
// apply CSRF protection for mutating requests
const csrfResponse = await withCsrfMiddleware(request, response);
// handle patterns for specific routes
const handlePattern = matchUrlPattern(request.url);
// if a pattern handler exists, call it
if (handlePattern) {
const patternHandlerResponse = await handlePattern(
request,
csrfResponse,
userResponse,
);
// if a pattern handler returns a response, return it
if (patternHandlerResponse) {
return patternHandlerResponse;
}
}
// if no pattern handler returned a response,
// return the session response
return csrfResponse;
}
async function withCsrfMiddleware(
request: NextRequest,
response = new NextResponse(),
) {
// set up CSRF protection
const csrfProtect = createCsrfProtect({
cookie: {
secure: appConfig.production,
name: CSRF_SECRET_COOKIE,
},
// ignore CSRF errors for server actions since protection is built-in
ignoreMethods: isServerAction(request)
? ['POST']
: // always ignore GET, HEAD, and OPTIONS requests
['GET', 'HEAD', 'OPTIONS'],
});
try {
await csrfProtect(request, response);
return response;
} catch (error) {
// if there is a CSRF error, return a 403 response
if (error instanceof CsrfError) {
return NextResponse.json('Invalid CSRF token', {
status: 401,
});
}
throw error;
}
}
function isServerAction(request: NextRequest) {
const headers = new Headers(request.headers);
return headers.has(NEXT_ACTION_HEADER);
}
async function adminMiddleware(request: NextRequest, response: NextResponse) {
const isAdminPath = request.nextUrl.pathname.startsWith('/admin');
if (!isAdminPath) {
return response;
}
const supabase = createMiddlewareClient(request, response);
const { data, error } = await supabase.auth.getUser();
// If user is not logged in, redirect to sign in page.
// This should never happen, but just in case.
if (!data.user || error) {
return NextResponse.redirect(
new URL(pathsConfig.auth.signIn, request.nextUrl.origin).href,
);
}
const role = data.user?.app_metadata.role;
// If user is not an admin, redirect to 404 page.
if (!role || role !== 'super-admin') {
return NextResponse.redirect(new URL('/404', request.nextUrl.origin).href);
}
// in all other cases, return the response
return response;
}
/**
* Define URL patterns and their corresponding handlers.
*/
function getPatterns() {
return [
{
pattern: new URLPattern({ pathname: '/admin*' }),
handler: adminMiddleware,
},
{
pattern: new URLPattern({ pathname: '/auth*' }),
handler: async (req: NextRequest, res: NextResponse) => {
const supabase = createMiddlewareClient(req, res);
const { data: user, error } = await supabase.auth.getUser();
// the user is logged out, so we don't need to do anything
if (error) {
await supabase.auth.signOut();
return;
}
// check if we need to verify MFA (user is authenticated but needs to verify MFA)
const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
// If user is logged in and does not need to verify MFA,
// redirect to home page.
if (user && !isVerifyMfa) {
return NextResponse.redirect(
new URL(pathsConfig.app.home, req.nextUrl.origin).href,
);
}
},
},
{
pattern: new URLPattern({ pathname: '/home*' }),
handler: async (
req: NextRequest,
res: NextResponse,
userResponse: UserResponse,
) => {
const { data: user, error } = userResponse;
const origin = req.nextUrl.origin;
const next = req.nextUrl.pathname;
// If user is not logged in, redirect to sign in page.
if (!user || error) {
const signIn = pathsConfig.auth.signIn;
const redirectPath = `${signIn}?next=${next}`;
return NextResponse.redirect(new URL(redirectPath, origin).href);
}
const supabase = createMiddlewareClient(req, res);
const requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(supabase);
// If user requires multi-factor authentication, redirect to MFA page.
if (requiresMultiFactorAuthentication) {
return NextResponse.redirect(
new URL(pathsConfig.auth.verifyMfa, origin).href,
);
}
},
},
];
}
/**
* Match URL patterns to specific handlers.
* @param url
*/
function matchUrlPattern(url: string) {
const patterns = getPatterns();
const input = url.split('?')[0];
for (const pattern of patterns) {
const patternResult = pattern.pattern.exec(input);
if (patternResult !== null && 'pathname' in patternResult) {
return pattern.handler;
}
}
}
/**
* Set a unique request ID for each request.
* @param request
*/
function setRequestId(request: Request) {
request.headers.set('x-correlation-id', crypto.randomUUID());
}