import type { NextRequest } from 'next/server'; import { NextResponse, URLPattern } from 'next/server'; import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; import { isSuperAdmin } from '@kit/admin'; import { isDoctor } from '@kit/doctor'; import { appConfig, pathsConfig } from '@kit/shared/config'; import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa'; import { createMiddlewareClient } from '@kit/supabase/middleware-client'; import { middleware as medusaMiddleware } from '~/medusa/middleware'; const CSRF_SECRET_COOKIE = 'csrfSecret'; const NEXT_ACTION_HEADER = 'next-action'; export const config = { matcher: ['/((?!_next/static|_next/image|images|locales|assets|api/*).*)'], }; const getUser = (request: NextRequest, response: NextResponse) => { const supabase = createMiddlewareClient(request, response); return supabase.auth.getUser(); }; export async function middleware(request: NextRequest) { const secureHeaders = await createResponseWithSecureHeaders(); const response = NextResponse.next(secureHeaders); // 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); // if a pattern handler returns a response, return it if (patternHandlerResponse) { return patternHandlerResponse; } } // append the action path to the request headers // which is useful for knowing the action path in server actions if (isServerAction(request)) { csrfResponse.headers.set('x-action-path', request.nextUrl.pathname); } await medusaMiddleware(request as any); // if no pattern handler returned a response, // return the session response return csrfResponse; } async function withCsrfMiddleware( request: NextRequest, response: 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; } const { data: { user }, error, } = await getUser(request, response); // If user is not logged in, redirect to sign in page. // This should never happen, but just in case. if (!user || error) { return NextResponse.redirect( new URL(pathsConfig.auth.signIn, request.nextUrl.origin).href, ); } const client = createMiddlewareClient(request, response); const userIsSuperAdmin = await isSuperAdmin(client); // If user is not an admin, redirect to 404 page. if (!userIsSuperAdmin) { return NextResponse.redirect(new URL('/404', request.nextUrl.origin).href); } // in all other cases, return the response return response; } async function doctorMiddleware(request: NextRequest, response: NextResponse) { const isDoctorPath = request.nextUrl.pathname.startsWith('/doctor'); if (!isDoctorPath) { return; } const { data: { user }, error, } = await getUser(request, response); if (!user || error) { return NextResponse.redirect( new URL(pathsConfig.auth.signIn, request.nextUrl.origin).href, ); } const client = createMiddlewareClient(request, response); const userIsDoctor = await isDoctor(client); if (!userIsDoctor) { return NextResponse.redirect(new URL('/404', request.nextUrl.origin).href); } return response; } /** * Define URL patterns and their corresponding handlers. */ function getPatterns() { return [ { pattern: new URLPattern({ pathname: '/' }), handler: async (req: NextRequest, res: NextResponse) => { const { data: { user }, } = await getUser(req, res); if (user) { return NextResponse.redirect( new URL(pathsConfig.app.home, req.nextUrl.origin).href, ); } }, }, { pattern: new URLPattern({ pathname: '/admin/*?' }), handler: adminMiddleware, }, { pattern: new URLPattern({ pathname: '/doctor/*?' }), handler: doctorMiddleware, }, { pattern: new URLPattern({ pathname: '/auth/update-account' }), handler: async (req: NextRequest, res: NextResponse) => { const { data: { user }, } = await getUser(req, res); // the user is logged out, so we don't need to do anything if (!user) { return NextResponse.redirect(new URL('/', req.nextUrl.origin).href); } const client = createMiddlewareClient(req, res); const userIsSuperAdmin = await isSuperAdmin(client); if (userIsSuperAdmin) { // 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 (!isVerifyMfa) { const nextPath = req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home; return NextResponse.redirect( new URL(nextPath, req.nextUrl.origin).href, ); } } }, }, { pattern: new URLPattern({ pathname: '/home/*?' }), handler: async (req: NextRequest, res: NextResponse) => { const { data: { user }, } = await getUser(req, res); const origin = req.nextUrl.origin; const next = req.nextUrl.pathname; // If user is not logged in, redirect to sign in page. if (!user) { 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()); } /** * @name createResponseWithSecureHeaders * @description Create a middleware with enhanced headers applied (if applied). * This is disabled by default. To enable set ENABLE_STRICT_CSP=true */ async function createResponseWithSecureHeaders() { const enableStrictCsp = process.env.ENABLE_STRICT_CSP ?? 'false'; // we disable ENABLE_STRICT_CSP by default if (enableStrictCsp === 'false') { return {}; } const { createCspResponse } = await import('./lib/create-csp-response'); return createCspResponse(); }