Files
myeasycms-v2/apps/web/proxy.ts
Zaid Marzguioui b2c9503749
Some checks failed
Workflow / ⚫️ Test (push) Has been cancelled
Workflow / ʦ TypeScript (push) Has been cancelled
fix(proxy): graceful error handling when Supabase is unreachable
Wrap getUser() calls in proxy.ts with try/catch so the proxy doesn't
crash when the Supabase client can't connect. Without this, the proxy
fails silently and Next.js returns 404 for all locale-dependent routes
(/auth/sign-in, /join, etc.) because the locale rewrite never happens.
2026-04-01 11:18:44 +02:00

247 lines
7.1 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).catch(() => ({
data: null as any,
error: new Error('Supabase unreachable'),
}));
// 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) => {
let data;
try {
({ data } = await getUser(req, res));
} catch {
// Supabase unreachable — treat as logged out, let the page render
return;
}
// 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) => {
let data;
try {
({ data } = await getUser(req, res));
} catch {
// Supabase unreachable — redirect to sign in
const signIn = pathsConfig.auth.signIn;
return NextResponse.redirect(new URL(signIn, req.nextUrl.origin).href);
}
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;
}