Files
medreport_mrb2b/middleware.ts
Danel Kungla c5ddccc15d fix build
2025-07-09 14:01:43 +03:00

250 lines
7.0 KiB
TypeScript

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 { 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|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);
}
// 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;
}
/**
* Define URL patterns and their corresponding handlers.
*/
function getPatterns() {
return [
{
pattern: new URLPattern({ pathname: '/admin/*?' }),
handler: adminMiddleware,
},
{
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();
}