175 lines
5.3 KiB
TypeScript
175 lines
5.3 KiB
TypeScript
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 },
|
|
});
|
|
|
|
const supabase = createServerClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
{
|
|
global: {
|
|
fetch: (url: string | URL | Request, options?: RequestInit) => {
|
|
const u = new URL(url.toString());
|
|
if (process.env.NODE_ENV === 'production' && u.hostname === 'supabase.4ventures.es') {
|
|
u.protocol = 'http:';
|
|
u.hostname = 'ventures-web-supabase-gm0nnn-supabase-kong';
|
|
u.port = '8000';
|
|
if (!options) options = {};
|
|
if (!options.headers) options.headers = new Headers();
|
|
const headers = new Headers(options.headers);
|
|
headers.set('Host', 'supabase.4ventures.es');
|
|
options.headers = headers;
|
|
}
|
|
return fetch(u.toString(), options);
|
|
}
|
|
},
|
|
cookies: {
|
|
getAll() {
|
|
return req.cookies.getAll();
|
|
},
|
|
setAll(cookiesToSet: { name: string; value: string; options?: Record<string, unknown> }[]) {
|
|
cookiesToSet.forEach(({ name, value }) => req.cookies.set(name, value));
|
|
res = NextResponse.next({
|
|
request: { headers: req.headers },
|
|
});
|
|
cookiesToSet.forEach(({ name, value, options }) =>
|
|
res.cookies.set(name, value, options)
|
|
);
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
|
|
const url = req.nextUrl.clone();
|
|
const path = url.pathname;
|
|
|
|
// Public: login, attribution script, health, inbound webhooks (CRM), cron (Bearer CRON_SECRET)
|
|
const isPublic =
|
|
path === '/login' ||
|
|
path === '/api/health' ||
|
|
path.startsWith('/api/webhooks/') ||
|
|
path.startsWith('/api/cron/') ||
|
|
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 withSecurityHeaders(NextResponse.redirect(loginUrl));
|
|
}
|
|
|
|
if (user && path === '/login') {
|
|
const role = user?.app_metadata?.role || user?.user_metadata?.role || 'client';
|
|
const dashboardUrl = req.nextUrl.clone();
|
|
dashboardUrl.pathname =
|
|
role === 'admin' || role === 'team'
|
|
? '/dashboard/agencia'
|
|
: '/dashboard/kpis';
|
|
return withSecurityHeaders(NextResponse.redirect(dashboardUrl));
|
|
}
|
|
|
|
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 withSecurityHeaders(NextResponse.redirect(forbiddenUrl));
|
|
}
|
|
|
|
if (user && role === 'client' && path.startsWith('/api/admin/')) {
|
|
return withSecurityHeaders(
|
|
NextResponse.json(
|
|
{
|
|
error:
|
|
'Forbidden: solo usuarios agencia (admin/team) pueden usar esta acción. Entra con marketing@4ventures.es.',
|
|
},
|
|
{ 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 !== 'n8n-agencia' &&
|
|
sub !== 'www'
|
|
) {
|
|
tenantSlug = sub;
|
|
}
|
|
}
|
|
|
|
const queryTenant = url.searchParams.get('tenant');
|
|
if (queryTenant) {
|
|
tenantSlug = queryTenant.toLowerCase();
|
|
} else {
|
|
const cookieTenant = req.cookies.get('4v_tenant')?.value;
|
|
if (cookieTenant && !user?.user_metadata?.slug) {
|
|
tenantSlug = cookieTenant;
|
|
}
|
|
}
|
|
|
|
res.headers.set('x-tenant-slug', tenantSlug);
|
|
res.headers.set('x-user-role', role);
|
|
if (user?.email) {
|
|
res.headers.set('x-user-email', user.email);
|
|
}
|
|
res.cookies.set('4v_tenant', tenantSlug, {
|
|
path: '/',
|
|
maxAge: 60 * 60 * 24 * 30,
|
|
sameSite: 'lax',
|
|
secure: process.env.NODE_ENV === 'production',
|
|
});
|
|
|
|
return withSecurityHeaders(res);
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
'/((?!_next/static|_next/image|favicon.ico|favicon.png|icon.png|_4v_attr.js).*)',
|
|
],
|
|
};
|