Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
228 lines
6.6 KiB
TypeScript
228 lines
6.6 KiB
TypeScript
import type { NextRequest } from 'next/server';
|
|
import { NextResponse } from 'next/server';
|
|
|
|
import createNextIntlMiddleware from 'next-intl/middleware';
|
|
|
|
import { isSuperAdmin } from '@kit/admin';
|
|
import { routing } from '@kit/i18n/routing';
|
|
import { getSafeRedirectPath } from '@kit/shared/utils';
|
|
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
|
import { createMiddlewareClient } from '@kit/supabase/middleware-client';
|
|
|
|
import pathsConfig from '~/config/paths.config';
|
|
|
|
const NEXT_ACTION_HEADER = 'next-action';
|
|
|
|
export const config = {
|
|
matcher: ['/((?!_next/static|_next/image|images|locales|assets|api/*).*)'],
|
|
};
|
|
|
|
// create i18n middleware once at module scope
|
|
const handleI18nRouting = createNextIntlMiddleware(routing);
|
|
|
|
const getUser = (request: NextRequest, response: NextResponse) => {
|
|
const supabase = createMiddlewareClient(request, response);
|
|
|
|
return supabase.auth.getClaims();
|
|
};
|
|
|
|
export default async function proxy(request: NextRequest) {
|
|
// run next-intl middleware first to get the i18n-aware response
|
|
const response = handleI18nRouting(request);
|
|
|
|
// apply secure headers on top of the i18n response
|
|
const secureHeadersResponse = await createResponseWithSecureHeaders(response);
|
|
|
|
// set a unique request ID for each request
|
|
// this helps us log and trace requests
|
|
setRequestId(request);
|
|
|
|
// handle patterns for specific routes
|
|
const handlePattern = await matchUrlPattern(request.url);
|
|
|
|
// if a pattern handler exists, call it
|
|
if (handlePattern) {
|
|
const patternHandlerResponse = await handlePattern(
|
|
request,
|
|
secureHeadersResponse,
|
|
);
|
|
|
|
// 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)) {
|
|
secureHeadersResponse.headers.set(
|
|
'x-action-path',
|
|
request.nextUrl.pathname,
|
|
);
|
|
}
|
|
|
|
// if no pattern handler returned a response,
|
|
// return the session response
|
|
return secureHeadersResponse;
|
|
}
|
|
|
|
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, 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 (!data?.claims || 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.
|
|
*/
|
|
async function getPatterns() {
|
|
let URLPattern = globalThis.URLPattern;
|
|
|
|
if (!URLPattern) {
|
|
const { URLPattern: polyfill } = await import('urlpattern-polyfill');
|
|
URLPattern = polyfill as typeof URLPattern;
|
|
}
|
|
|
|
return [
|
|
{
|
|
pattern: new URLPattern({ pathname: '/admin/*?' }),
|
|
handler: adminMiddleware,
|
|
},
|
|
{
|
|
pattern: new URLPattern({ pathname: '/auth/*?' }),
|
|
handler: async (req: NextRequest, res: NextResponse) => {
|
|
const { data } = await getUser(req, res);
|
|
|
|
// the user is logged out, so we don't need to do anything
|
|
if (!data?.claims) {
|
|
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 = getSafeRedirectPath(
|
|
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 } = await getUser(req, res);
|
|
const { origin, pathname: next } = req.nextUrl;
|
|
|
|
// If user is not logged in, redirect to sign in page.
|
|
if (!data?.claims) {
|
|
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
|
|
*/
|
|
async function matchUrlPattern(url: string) {
|
|
const patterns = await 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(response: NextResponse) {
|
|
const enableStrictCsp = process.env.ENABLE_STRICT_CSP ?? 'false';
|
|
|
|
// we disable ENABLE_STRICT_CSP by default
|
|
if (enableStrictCsp === 'false') {
|
|
return response;
|
|
}
|
|
|
|
const { createCspResponse } = await import('./lib/create-csp-response');
|
|
const cspResponse = await createCspResponse();
|
|
|
|
// set the CSP headers on the i18n response
|
|
if (cspResponse) {
|
|
for (const [key, value] of cspResponse.headers.entries()) {
|
|
response.headers.set(key, value);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|