diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
new file mode 100644
index 0000000..8e1dd47
--- /dev/null
+++ b/src/app/login/page.tsx
@@ -0,0 +1,168 @@
+'use client';
+
+import React, { useState, Suspense } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { createClient } from '@/lib/supabase/client';
+import { Mail, Lock, Eye, EyeOff, ShieldCheck, ArrowRight, Loader2 } from 'lucide-react';
+
+function LoginForm() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const next = searchParams.get('next') || '/dashboard/kpis';
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [errorMsg, setErrorMsg] = useState
(null);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setErrorMsg(null);
+ setLoading(true);
+
+ try {
+ const supabase = createClient();
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email: email.trim(),
+ password: password,
+ });
+
+ if (error) {
+ setErrorMsg('Credenciales inválidas. Verifica tu correo y contraseña.');
+ setLoading(false);
+ return;
+ }
+
+ if (data.session) {
+ router.push(next);
+ router.refresh();
+ }
+ } catch (err: any) {
+ setErrorMsg(err?.message || 'Error de conexión con el servidor de autenticación.');
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
Iniciar Sesión
+
Introduce tus credenciales corporativas para acceder a tu portal.
+
+
+ {errorMsg && (
+
+
+ {errorMsg}
+
+ )}
+
+
+
+ );
+}
+
+export default function LoginPage() {
+ return (
+
+ {/* Background Glow */}
+
+
+
+ {/* Header Branding */}
+
+
+ 4V
+
+
4VENTURES
+
Centro de Mando del Cliente
+
+
+ {/* Login Box wrapped in Suspense for useSearchParams */}
+
+
+
+ }>
+
+
+
+ {/* Security Footer */}
+
+
+
+ Aislamiento RLS Multi-Tenant
+
+
•
+
Cifrado TLS v1.3
+
•
+
Dokploy VPS
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1f1ce9d..10923c2 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,5 +1,13 @@
import { redirect } from 'next/navigation';
+import { createClient } from '@/lib/supabase/server';
-export default function Home() {
- redirect('/dashboard/kpis');
+export default async function Home() {
+ const supabase = await createClient();
+ const { data: { user } } = await supabase.auth.getUser();
+
+ if (user) {
+ redirect('/dashboard/kpis');
+ } else {
+ redirect('/login');
+ }
}
diff --git a/src/middleware.ts b/src/middleware.ts
index 750bb02..c37ca92 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -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).*)']
};