feat(auth): add /login page, session middleware, role-based navigation and logout

This commit is contained in:
Marcos Fargas
2026-09-21 02:20:09 +02:00
parent f728c33533
commit 3bcdb6379f
5 changed files with 323 additions and 42 deletions

View File

@@ -1,11 +1,71 @@
import { createServerClient } from '@supabase/ssr';
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const url = req.nextUrl.clone();
const hostname = req.headers.get('host') || '';
export async function middleware(req: NextRequest) {
let res = NextResponse.next({
request: {
headers: req.headers,
},
});
// 1. Extraer slug desde subdominio (ej: acme.4ventures.es)
let tenantSlug = '4ventures-hub';
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return req.cookies.getAll();
},
setAll(cookiesToSet) {
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;
// 1. Redirigir usuarios no autenticados que intenten acceder al dashboard
if (!user && (path === '/' || path.startsWith('/dashboard'))) {
const loginUrl = req.nextUrl.clone();
loginUrl.pathname = '/login';
if (path.startsWith('/dashboard')) {
loginUrl.searchParams.set('next', path);
}
return 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);
}
// 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);
}
// 4. Resolver Tenant Slug (desde subdominio, metadata de usuario o cookie)
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) {
@@ -15,29 +75,26 @@ export function middleware(req: NextRequest) {
}
}
// 2. Override opcional vía query param ?tenant=slug
const queryTenant = url.searchParams.get('tenant');
if (queryTenant) {
tenantSlug = queryTenant.toLowerCase();
} else {
const cookieTenant = req.cookies.get('4v_tenant')?.value;
if (cookieTenant) {
if (cookieTenant && !user?.user_metadata?.slug) {
tenantSlug = cookieTenant;
}
}
const response = NextResponse.next({
request: {
headers: new Headers(req.headers)
}
});
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 });
response.headers.set('x-tenant-slug', tenantSlug);
response.cookies.set('4v_tenant', tenantSlug, { path: '/', maxAge: 60 * 60 * 24 * 30 });
return response;
return res;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|_4v_attr.js).*)']
matcher: ['/((?!_next/static|_next/image|favicon.ico|_4v_attr.js|api/chat|api/simulate).*)']
};