proxy.ts
Rama main · réplica de lectura del repositorio original.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { generalRateLimiter, getClientIP, getRateLimitHeaders } from '@/utils/rateLimit';
import { SECURITY_CONFIG } from '@/config/security';
import {
localeCookieMaxAge,
localeCookieName,
resolveLocale,
} from '@/i18n/config';
const PUBLIC_FILE = /^(?!\/\.(?!well-known\/))[^.]*\./;
const PUBLIC_PATHS = [
'/login',
'/login/reset',
'/register',
'/register/init',
'/register/terms',
];
function isPublicPath(pathname: string): boolean {
if (PUBLIC_PATHS.includes(pathname)) {
return true;
}
if (
pathname.startsWith('/login/otp/') ||
pathname.startsWith('/login/reset/') ||
pathname.startsWith('/share/') ||
pathname.startsWith('/api/v1/share/')
) {
return true;
}
return false;
}
export async function proxy(request: NextRequest): Promise<NextResponse> {
const { pathname } = request.nextUrl;
// Use internal URL for server-side fetches to avoid SSL errors behind a reverse proxy
const internalUrl = process.env.INTERNAL_APP_URL ?? request.nextUrl.origin;
// Apply rate limiting to all requests
const ip = getClientIP(request);
const rateLimitResult = generalRateLimiter.isAllowed(`${ip}:${pathname}`);
const response = NextResponse.next();
const locale = resolveLocale(
request.cookies.get(localeCookieName)?.value,
request.headers.get('accept-language'),
);
if (request.cookies.get(localeCookieName)?.value !== locale) {
response.cookies.set(localeCookieName, locale, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAge,
});
}
// Add security headers from config
Object.entries(SECURITY_CONFIG.HEADERS).forEach(([key, value]) => {
if (value) {
response.headers.set(key, value);
}
});
// Add rate limiting headers
if (!rateLimitResult.allowed) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
...getRateLimitHeaders(rateLimitResult.resetTime, rateLimitResult.remaining),
'Content-Type': 'text/plain',
},
});
}
Object.entries(getRateLimitHeaders(rateLimitResult.resetTime, rateLimitResult.remaining, SECURITY_CONFIG.RATE_LIMIT.GENERAL.MAX_REQUESTS)).forEach(([key, value]) => {
response.headers.set(key, value);
});
// CORS validation for non-GET requests
if (request.method !== 'GET') {
const originHeader = request.headers.get('Origin');
const hostHeader = request.headers.get('Host');
if (originHeader === null || hostHeader === null) {
return new NextResponse(null, {
status: 403,
headers: response.headers,
});
}
let origin: URL;
try {
origin = new URL(originHeader);
} catch {
return new NextResponse(null, {
status: 403,
headers: response.headers,
});
}
if (origin.host !== hostHeader && !SECURITY_CONFIG.CORS.ALLOWED_ORIGINS.includes(originHeader)) {
return new NextResponse(null, {
status: 403,
headers: response.headers,
});
}
// Add CORS headers for valid origins
if (SECURITY_CONFIG.CORS.ALLOWED_ORIGINS.includes(originHeader)) {
response.headers.set('Access-Control-Allow-Origin', originHeader);
response.headers.set('Access-Control-Allow-Methods', SECURITY_CONFIG.CORS.ALLOWED_METHODS.join(','));
response.headers.set('Access-Control-Allow-Headers', SECURITY_CONFIG.CORS.ALLOWED_HEADERS.join(', '));
response.headers.set('Access-Control-Allow-Credentials', SECURITY_CONFIG.CORS.ALLOW_CREDENTIALS.toString());
}
}
// Allow static files, Next.js internals and the auth API through
if (
pathname.startsWith('/api/v1/auth') ||
pathname.startsWith('/api/v1/locale') ||
pathname.startsWith('/_next') ||
pathname.startsWith('/static') ||
pathname === '/favicon.ico' ||
PUBLIC_FILE.test(pathname)
) {
return response;
}
// Check authentication for protected routes
if (!isPublicPath(pathname)) {
const token = request.cookies.get('session')?.value;
const sessionResponse = await fetch(
`${internalUrl}/api/v1/auth/session?token=${token || ''}`,
{
cache: 'no-store',
}
);
let isLoggedIn = false;
if (sessionResponse.ok) {
const sessionData = await sessionResponse.json();
isLoggedIn = sessionData.user !== null;
if (isLoggedIn && sessionData.sessionExpiresAt && token) {
response.cookies.set('session', token, {
httpOnly: SECURITY_CONFIG.SESSION.COOKIE_HTTP_ONLY,
sameSite: SECURITY_CONFIG.SESSION.COOKIE_SAME_SITE,
secure: SECURITY_CONFIG.SESSION.COOKIE_SECURE,
expires: new Date(sessionData.sessionExpiresAt),
path: '/',
});
}
}
if (!isLoggedIn) {
return NextResponse.redirect(
new URL(`/login?callbackUrl=${encodeURIComponent(pathname)}`, request.url)
);
}
}
// Validate if admin user exists (for initial registration flow)
// Allow legal terms during the initial registration flow.
if (pathname !== '/register/init' && pathname !== '/register/terms') {
const adminResponse = await fetch(
`${internalUrl}/api/v1/auth/admin`,
{
cache: 'force-cache',
}
);
if (!adminResponse.ok) {
return response;
}
const adminData = await adminResponse.json();
const adminExists = adminData.initialized;
if (!adminExists) {
return NextResponse.redirect(new URL('/register/init', request.url));
}
} else {
const adminResponse = await fetch(
`${internalUrl}/api/v1/auth/admin`,
{
cache: 'force-cache',
}
);
if (!adminResponse.ok) {
return response;
}
const adminData = await adminResponse.json();
const adminExists = adminData.initialized;
if (adminExists) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Redirect logged-in users away from login page
if (pathname === '/login') {
const token = request.cookies.get('session')?.value;
if (!token || !token.trim() || token === 'undefined') {
return response;
}
const sessionResponse = await fetch(
`${internalUrl}/api/v1/auth/session?token=${token}`,
{
cache: 'no-store',
}
);
if (!sessionResponse.ok) {
return response;
}
const sessionData = await sessionResponse.json();
const isLoggedIn = sessionData.user !== null;
if (isLoggedIn) {
const callbackUrl = request.nextUrl.searchParams.get('callbackUrl');
const redirectUrl = callbackUrl && callbackUrl.startsWith('/') ? callbackUrl : '/';
return NextResponse.redirect(new URL(redirectUrl, request.url));
}
}
// Handle registration page - check if registration is allowed
if (pathname === '/register') {
const allowResponse = await fetch(
`${internalUrl}/api/v1/auth/register`
);
if (!allowResponse.ok) {
return response;
}
const allowData = await allowResponse.json();
const allowRegistration = allowData.registration;
if (!allowRegistration) {
return NextResponse.redirect(new URL(`/login?callbackUrl=${encodeURIComponent(pathname)}`, request.url));
}
return response;
}
// Session exists — allow the request
return response;
}