fix(security): harden auth, tenant scoping, and scrub secrets
Require session on APIs, scope KPIs/activos/simulate to the user tenant, strip real JWTs from .env.example, and add health/smoke/docs for 4Ventures ops. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,11 +1,27 @@
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
function withSecurityHeaders(res: NextResponse) {
|
||||
res.headers.set('X-Frame-Options', 'DENY');
|
||||
res.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.headers.set('X-Robots-Tag', 'noindex, nofollow');
|
||||
res.headers.set(
|
||||
'Permissions-Policy',
|
||||
'camera=(), microphone=(), geolocation=()'
|
||||
);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.headers.set(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=63072000; includeSubDomains; preload'
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
let res = NextResponse.next({
|
||||
request: {
|
||||
headers: req.headers,
|
||||
},
|
||||
request: { headers: req.headers },
|
||||
});
|
||||
|
||||
const supabase = createServerClient(
|
||||
@@ -19,9 +35,7 @@ export async function middleware(req: NextRequest) {
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) => req.cookies.set(name, value));
|
||||
res = NextResponse.next({
|
||||
request: {
|
||||
headers: req.headers,
|
||||
},
|
||||
request: { headers: req.headers },
|
||||
});
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
res.cookies.set(name, value, options)
|
||||
@@ -38,39 +52,64 @@ export async function middleware(req: NextRequest) {
|
||||
const url = req.nextUrl.clone();
|
||||
const path = url.pathname;
|
||||
|
||||
// 1. Redirigir usuarios no autenticados que intenten acceder al dashboard
|
||||
if (!user && (path === '/' || path.startsWith('/dashboard'))) {
|
||||
// Public: login, attribution script, health
|
||||
const isPublic =
|
||||
path === '/login' ||
|
||||
path === '/api/health' ||
|
||||
path === '/_4v_attr.js' ||
|
||||
path.startsWith('/_next') ||
|
||||
path === '/favicon.ico' ||
|
||||
path === '/favicon.png' ||
|
||||
path === '/icon.png';
|
||||
|
||||
if (!user && !isPublic && (path === '/' || path.startsWith('/dashboard') || path.startsWith('/api/'))) {
|
||||
if (path.startsWith('/api/')) {
|
||||
return withSecurityHeaders(
|
||||
NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
);
|
||||
}
|
||||
const loginUrl = req.nextUrl.clone();
|
||||
loginUrl.pathname = '/login';
|
||||
if (path.startsWith('/dashboard')) {
|
||||
loginUrl.searchParams.set('next', path);
|
||||
}
|
||||
return NextResponse.redirect(loginUrl);
|
||||
return withSecurityHeaders(NextResponse.redirect(loginUrl));
|
||||
}
|
||||
|
||||
// 2. Redirigir usuarios autenticados que intenten acceder a /login
|
||||
if (user && path === '/login') {
|
||||
const dashboardUrl = req.nextUrl.clone();
|
||||
dashboardUrl.pathname = '/dashboard/kpis';
|
||||
return NextResponse.redirect(dashboardUrl);
|
||||
return withSecurityHeaders(NextResponse.redirect(dashboardUrl));
|
||||
}
|
||||
|
||||
// 3. Proteger la vista directiva de agencia contra clientes
|
||||
const role = user?.app_metadata?.role || user?.user_metadata?.role || 'client';
|
||||
if (user && role === 'client' && path.startsWith('/dashboard/agencia')) {
|
||||
const forbiddenUrl = req.nextUrl.clone();
|
||||
forbiddenUrl.pathname = '/dashboard/kpis';
|
||||
return NextResponse.redirect(forbiddenUrl);
|
||||
return withSecurityHeaders(NextResponse.redirect(forbiddenUrl));
|
||||
}
|
||||
|
||||
// 4. Resolver Tenant Slug (desde subdominio, metadata de usuario o cookie)
|
||||
let tenantSlug = user?.user_metadata?.slug || user?.app_metadata?.slug || '4ventures-hub';
|
||||
if (user && role === 'client' && path.startsWith('/api/admin/')) {
|
||||
return withSecurityHeaders(
|
||||
NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
);
|
||||
}
|
||||
|
||||
let tenantSlug =
|
||||
user?.user_metadata?.slug || user?.app_metadata?.slug || '4ventures-hub';
|
||||
const hostname = req.headers.get('host') || '';
|
||||
const parts = hostname.split('.');
|
||||
|
||||
if (parts.length > 2) {
|
||||
const sub = parts[0].toLowerCase();
|
||||
if (sub !== 'app' && sub !== 'git' && sub !== 'supabase' && sub !== 'n8n' && sub !== 'www') {
|
||||
if (
|
||||
sub !== 'app' &&
|
||||
sub !== 'git' &&
|
||||
sub !== 'supabase' &&
|
||||
sub !== 'n8n' &&
|
||||
sub !== 'n8n-agencia' &&
|
||||
sub !== 'www'
|
||||
) {
|
||||
tenantSlug = sub;
|
||||
}
|
||||
}
|
||||
@@ -90,11 +129,18 @@ export async function middleware(req: NextRequest) {
|
||||
if (user?.email) {
|
||||
res.headers.set('x-user-email', user.email);
|
||||
}
|
||||
res.cookies.set('4v_tenant', tenantSlug, { path: '/', maxAge: 60 * 60 * 24 * 30 });
|
||||
res.cookies.set('4v_tenant', tenantSlug, {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
return res;
|
||||
return withSecurityHeaders(res);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next/static|_next/image|favicon.ico|favicon.png|icon.png|_4v_attr.js|api/chat|api/simulate).*)']
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|favicon.png|icon.png|_4v_attr.js).*)',
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user