B2B-88: add starter kit structure and elements
This commit is contained in:
255
middleware.ts
255
middleware.ts
@@ -1,20 +1,243 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { updateSession } from "@/utils/supabase/middleware";
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse, URLPattern } from 'next/server';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request);
|
||||
}
|
||||
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: [
|
||||
/*
|
||||
* Match all request paths except:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
|
||||
* Feel free to modify this pattern to include more paths.
|
||||
*/
|
||||
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
||||
],
|
||||
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/*?' }),
|
||||
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;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user