Files
aggios.app/front-end-agency/middleware.ts
Erik Silva 2f1cf2bb2a v1.4: Segurança multi-tenant, file serving via API e UX humanizada
-  Validação cross-tenant no login e rotas protegidas
-  File serving via /api/files/{bucket}/{path} (eliminação DNS)
-  Mensagens de erro humanizadas inline (sem pop-ups)
-  Middleware tenant detection via headers customizados
-  Upload de logos retorna URLs via API
-  README atualizado com changelog v1.4 completo
2025-12-13 15:05:51 -03:00

76 lines
2.8 KiB
TypeScript

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const url = request.nextUrl;
const apiBase = process.env.API_INTERNAL_URL || 'http://backend:8080';
// Extrair subdomínio
const subdomain = hostname.split('.')[0];
// Validar subdomínio de agência ({subdomain}.localhost)
if (hostname.includes('.')) {
try {
const res = await fetch(`${apiBase}/api/tenant/check?subdomain=${subdomain}`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
}
});
if (!res.ok) {
console.error(`Tenant check failed for ${subdomain}: ${res.status}`);
// Se for 404, realmente não existe. Se for 500, pode ser erro temporário.
// Por segurança, vamos redirecionar apenas se tivermos certeza que falhou a validação (ex: 404)
// ou se o backend estiver inalcançável de forma persistente.
// Para evitar loops durante desenvolvimento, vamos permitir passar se for erro de servidor (5xx)
// mas redirecionar se for 404.
if (res.status === 404) {
const baseHost = hostname.split('.').slice(1).join('.') || hostname;
const redirectUrl = new URL(url.toString());
redirectUrl.hostname = baseHost;
redirectUrl.pathname = '/';
return NextResponse.redirect(redirectUrl);
}
}
} catch (err) {
console.error('Middleware error:', err);
// Em caso de erro de rede (backend fora do ar), permitir carregar a página
// para não travar o frontend completamente (pode mostrar erro na tela depois)
// return NextResponse.next();
}
}
// Para requisições de API, adicionar headers com informações do tenant
if (url.pathname.startsWith('/api/')) {
// Cria um header customizado com o subdomain
const requestHeaders = new Headers(request.headers);
requestHeaders.set('X-Tenant-Subdomain', subdomain);
requestHeaders.set('X-Original-Host', hostname);
return NextResponse.rewrite(url, {
request: {
headers: requestHeaders,
},
});
}
// Permitir acesso normal
return NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};