Next.js 16, React 19.2, Identities page, Invitations identities step, PNPM Catalogs (#381)
* Upgraded to Next.js 16 * Refactored code to comply with React 19.2 ESLint rules * Refactored some useEffect usages with the new useEffectEvent * Added Identities page and added second step to set up an identity after accepting an invitation * Updated all dependencies * Introduced PNPM catalogs for some frequently updated dependencies * Bugs fixing and improvements
This commit is contained in:
committed by
GitHub
parent
ea0c1dde80
commit
2c0d0bf7a1
234
apps/web/proxy.ts
Normal file
234
apps/web/proxy.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
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.getClaims();
|
||||
};
|
||||
|
||||
export async function proxy(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, 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.
|
||||
*/
|
||||
function getPatterns() {
|
||||
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 =
|
||||
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
|
||||
*/
|
||||
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