diff --git a/almeira-store-app/src/app/api/auth/change-password/route.ts b/almeira-store-app/src/app/api/auth/change-password/route.ts new file mode 100644 index 0000000..7bed97a --- /dev/null +++ b/almeira-store-app/src/app/api/auth/change-password/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, currentPassword, newPassword, confirmPassword } = await request.json(); + + if (!userId || !newPassword) { + return NextResponse.json( + { success: false, error: 'Todos los campos son obligatorios' }, + { status: 400 } + ); + } + + if (newPassword.length < 4) { + return NextResponse.json( + { success: false, error: 'La nueva contraseña debe tener al menos 4 caracteres' }, + { status: 400 } + ); + } + + if (newPassword !== confirmPassword) { + return NextResponse.json( + { success: false, error: 'La confirmación no coincide con la nueva contraseña' }, + { status: 400 } + ); + } + + const users = dataService.getUsers(); + const user = users.find((u) => u.id === userId); + + if (!user) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + // Validar contraseña actual (o Master PIN 1978) + const isMasterPin = currentPassword === '1978'; + const isValidCurrent = + !user.password || + user.password === currentPassword || + isMasterPin; + + if (!isValidCurrent) { + return NextResponse.json( + { success: false, error: 'La contraseña actual es incorrecta' }, + { status: 401 } + ); + } + + // Actualizar contraseña + const updated = dataService.updateUser(userId, { + password: newPassword, + }); + + return NextResponse.json({ + success: true, + message: '🔒 Contraseña actualizada correctamente', + user: updated, + }); + } catch (error: any) { + console.error('Error cambiando contraseña:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/almeira-store-app/src/app/api/auth/toggle-2fa/route.ts b/almeira-store-app/src/app/api/auth/toggle-2fa/route.ts new file mode 100644 index 0000000..02e4e35 --- /dev/null +++ b/almeira-store-app/src/app/api/auth/toggle-2fa/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, twoFactorEnabled, twoFactorMethod } = await request.json(); + + if (!userId) { + return NextResponse.json( + { success: false, error: 'ID de usuario requerido' }, + { status: 400 } + ); + } + + const updated = dataService.updateUser(userId, { + twoFactorEnabled: !!twoFactorEnabled, + twoFactorMethod: twoFactorMethod || 'whatsapp_otp', + }); + + if (!updated) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: twoFactorEnabled + ? '🛡️ Autenticación en dos factores (2FA) activada con éxito' + : '⚪ Autenticación en dos factores (2FA) desactivada', + user: updated, + }); + } catch (error: any) { + console.error('Error toggling 2FA:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/almeira-store-app/src/components/auth/AuthModal.tsx b/almeira-store-app/src/components/auth/AuthModal.tsx index 8a39a9f..09d71d5 100644 --- a/almeira-store-app/src/components/auth/AuthModal.tsx +++ b/almeira-store-app/src/components/auth/AuthModal.tsx @@ -1,38 +1,158 @@ 'use client'; -import React, { useState } from 'react'; -import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { + X, + User, + Lock, + Mail, + Phone, + MapPin, + Sparkles, + LogIn, + UserPlus, + Shield, + LogOut, + ArrowRight, + CheckCircle2, + RotateCcw, + Smartphone, + KeyRound, + Fingerprint, + Check, + Eye, + EyeOff, + AlertCircle +} from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { useBrand } from '@/context/BrandContext'; import Link from 'next/link'; export const AuthModal: React.FC = () => { - const { user, login, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); + const { user, sendOtp, verifyOtp, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); const { currentBrand } = useBrand(); + // Mode when not logged in: 'login' | 'register' const [mode, setMode] = useState<'login' | 'register'>('login'); + const [step, setStep] = useState<'request' | 'verify'>('request'); const [identifier, setIdentifier] = useState(''); + const [maskedPhone, setMaskedPhone] = useState(''); + const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [phone, setPhone] = useState(''); const [city, setCity] = useState('Cali'); const [address, setAddress] = useState(''); const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); const [loading, setLoading] = useState(false); + const [resendTimer, setResendTimer] = useState(0); + + // Sub-tabs when user is logged in: 'profile' | 'security' | '2fa' + const [profileTab, setProfileTab] = useState<'profile' | 'security' | '2fa'>('profile'); + + // Change password form state + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [pwdMsg, setPwdMsg] = useState(null); + const [pwdError, setPwdError] = useState(null); + + // 2FA state + const [is2FaEnabled, setIs2FaEnabled] = useState(false); + const [twoFactorMsg, setTwoFactorMsg] = useState(null); + + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + if (user) { + setIs2FaEnabled(user.twoFactorEnabled !== false); + } + }, [user]); + + // Contador de reenvío + useEffect(() => { + let interval: any = null; + if (resendTimer > 0) { + interval = setInterval(() => setResendTimer((prev) => prev - 1), 1000); + } + return () => clearInterval(interval); + }, [resendTimer]); if (!isAuthModalOpen) return null; - const handleLoginSubmit = async (e: React.FormEvent) => { + // Paso 1: Enviar OTP + const handleSendOtp = async (e: React.FormEvent) => { e.preventDefault(); + if (!identifier.trim()) { + setError('Por favor ingresa tu número de celular o correo.'); + return; + } setError(null); setLoading(true); - const res = await login(identifier); + + const res = await sendOtp(identifier); setLoading(false); - if (!res.success) { - setError(res.error || 'Error al iniciar sesión'); + + if (res.success) { + setMaskedPhone(res.phone || identifier); + setSuccessMsg(res.message || 'Código enviado a tu WhatsApp'); + setStep('verify'); + setResendTimer(60); + setOtpDigits(['', '', '', '', '', '']); + setTimeout(() => inputRefs.current[0]?.focus(), 150); + } else { + setError(res.error || 'Error al enviar código de seguridad'); } }; + // Paso 2: Verificar OTP + const handleOtpChange = (index: number, val: string) => { + const cleanVal = val.replace(/\D/g, '').slice(-1); + const newDigits = [...otpDigits]; + newDigits[index] = cleanVal; + setOtpDigits(newDigits); + + if (cleanVal && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + + if (cleanVal && index === 5) { + const fullCode = newDigits.join(''); + if (fullCode.length === 6) { + submitFullCode(fullCode); + } + } + }; + + const handleKeyDown = (index: number, e: React.KeyboardEvent) => { + if (e.key === 'Backspace' && !otpDigits[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const submitFullCode = async (code: string) => { + setError(null); + setLoading(true); + const res = await verifyOtp(identifier, code); + setLoading(false); + + if (!res.success) { + setError(res.error || 'Código incorrecto'); + } + }; + + const handleVerifySubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const fullCode = otpDigits.join(''); + if (fullCode.length < 4) { + setError('Ingresa el código completo de 6 dígitos.'); + return; + } + await submitFullCode(fullCode); + }; + const handleRegisterSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -44,103 +164,382 @@ export const AuthModal: React.FC = () => { } }; + // Cambiar Contraseña + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPwdError(null); + setPwdMsg(null); + + if (!user) return; + + if (newPassword !== confirmPassword) { + setPwdError('La nueva contraseña y la confirmación no coinciden.'); + return; + } + + setLoading(true); + try { + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + currentPassword, + newPassword, + confirmPassword, + }), + }); + const data = await res.json(); + setLoading(false); + + if (data.success) { + setPwdMsg('✅ ¡Contraseña actualizada exitosamente!'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + setPwdError(data.error || 'Error al cambiar contraseña'); + } + } catch (err: any) { + setLoading(false); + setPwdError(err.message || 'Error de conexión'); + } + }; + + // Activar / Desactivar 2FA + const handleToggle2Fa = async () => { + if (!user) return; + const newStatus = !is2FaEnabled; + setIs2FaEnabled(newStatus); + setTwoFactorMsg(null); + + try { + const res = await fetch('/api/auth/toggle-2fa', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + twoFactorEnabled: newStatus, + twoFactorMethod: 'whatsapp_otp', + }), + }); + const data = await res.json(); + if (data.success) { + setTwoFactorMsg( + newStatus + ? '🛡️ Autenticación 2FA por WhatsApp ACTIVADA para tu cuenta.' + : '⚪ Autenticación 2FA desactivada.' + ); + } + } catch (e) { + console.error(e); + } + }; + return ( -
-
+
+
- {/* If user is already logged in: View Profile */} + {/* LOGGED-IN USER: PROFILE & SECURITY SUITE */} {user ? (
-
+ {/* Top Avatar & User Name */} +
{user.name.charAt(0).toUpperCase()}
+
-

{user.name}

-

{user.email}

+

{user.name}

+

{user.email}

- - {user.role === 'admin' ? '👑 Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente Registrado'} + {user.role === 'admin' ? '👑 Super Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente VIP'}
-
-

📱 Celular: {user.phone}

-

📍 Ciudad: {user.city || 'Cali'}

- {user.address &&

🏠 Dirección: {user.address}

} + {/* Profile Navigation Tabs */} +
+ + + + +
- {user.role === 'admin' || user.role === 'operator' ? ( - - - Ir al Panel de Control Admin - - ) : null} + {/* TAB 1: User Profile Info */} + {profileTab === 'profile' && ( +
+
+

📱 WhatsApp / Celular: {user.phone}

+

📍 Ciudad: {user.city || 'Cali'}

+ {user.address &&

🏠 Dirección: {user.address}

} +
- + {user.role === 'admin' || user.role === 'operator' ? ( + + + Ir al Panel de Control Admin + + ) : null} + + +
+ )} + + {/* TAB 2: Change Password Form */} + {profileTab === 'security' && ( +
+
+

+ + Cambio de Contraseña Seguro +

+

+ Puedes usar tu clave actual o el PIN Maestro de Emergencia (1978). +

+
+ + {pwdMsg && ( +
+ + {pwdMsg} +
+ )} + + {pwdError && ( +
+ + {pwdError} +
+ )} + +
+ + setCurrentPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setNewPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ + +
+ )} + + {/* TAB 3: 2FA & Biometrics Settings */} + {profileTab === '2fa' && ( +
+ {twoFactorMsg && ( +
+ + {twoFactorMsg} +
+ )} + + {/* 2FA Toggle Card */} +
+
+
+
+ +
+
+

2FA con WhatsApp OTP

+ Código de 6 dígitos a tu celular +
+
+ + {/* Toggle Button */} + +
+ +

+ {is2FaEnabled + ? '🟢 2FA Activo: Cada inicio de sesión requerirá verificación por WhatsApp.' + : '⚪ 2FA Desactivado: Podrás iniciar sesión directamente con tu contraseña o biometría.'} +

+
+ + {/* Biometrics Card */} +
+
+
+ +
+
+

Acceso Biométrico / Passkey

+ Huella dactilar o Face ID nativo +
+
+ +

+ Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil. +

+ + +
+
+ )}
) : ( - /* Login or Register */ + /* LOGIN WITH 2FA / OTP OR REGISTER */
-
-
- {currentBrand.shortName[0]} +
+
+

- {mode === 'login' ? 'Iniciar Sesión' : 'Crear Cuenta'} + {step === 'verify' ? 'Verificación de Seguridad' : mode === 'login' ? 'Acceso Seguro OTP' : 'Crear Cuenta'}

-

- {mode === 'login' - ? 'Accede para gestionar pedidos o entrar como admin' - : 'Regístrate para compras rápidas y seguimiento'} +

+ {step === 'verify' + ? `Ingresa el código enviado a ${maskedPhone}` + : mode === 'login' + ? 'Validación segura vía WhatsApp con código de 6 dígitos' + : 'Regístrate para compras y seguimiento de pedidos'}

- {/* Mode Switch Tabs */} -
- - -
+ {/* Mode Switch Tabs (Only in request step) */} + {step === 'request' && ( +
+ + +
+ )} {error && (
@@ -148,91 +547,177 @@ export const AuthModal: React.FC = () => {
)} - {mode === 'login' ? ( -
+ {successMsg && step === 'verify' && ( +
+ + {successMsg} +
+ )} + + {/* STEP 1: Request OTP */} + {step === 'request' && mode === 'login' && ( +
- - setIdentifier(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2.5 text-white outline-hidden focus:border-cyan-400" - /> + +
+ setIdentifier(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-2xl px-4 py-3 text-white text-sm outline-hidden focus:border-purple-500 transition-all" + /> + +
+

+ Te enviaremos un código de seguridad de 6 dígitos a tu WhatsApp. +

-

- Tip Admin: Puedes ingresar con carlos@almeira.com.co o 3134848203. -

+
+
+ + Autenticación 2FA Verificada +
+

Acceso seguro para clientes y administradores con validación en tiempo real.

+
- ) : ( + )} + + {/* STEP 2: Verify 6-digit OTP */} + {step === 'verify' && mode === 'login' && ( +
+
+ + +
+ {otpDigits.map((digit, idx) => ( + { inputRefs.current[idx] = el; }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleOtpChange(idx, e.target.value)} + onKeyDown={(e) => handleKeyDown(idx, e)} + className={`w-11 h-13 sm:w-12 sm:h-14 text-center text-xl font-black rounded-2xl bg-neutral-900 border text-white outline-hidden transition-all ${ + digit + ? 'border-purple-500 shadow-[0_0_12px_rgba(168,85,247,0.35)]' + : 'border-neutral-800 focus:border-purple-500' + }`} + /> + ))} +
+
+ + + +
+ + + +
+
+ )} + + {/* Register form */} + {mode === 'register' && (
- + setName(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setEmail(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setPhone(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setCity(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setAddress(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
@@ -243,7 +728,7 @@ export const AuthModal: React.FC = () => { className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3 rounded-xl shadow-lg flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all`} > - {loading ? 'Creando cuenta...' : 'Crear mi Cuenta Gratis'} + {loading ? 'Creando cuenta...' : 'Crear mi Cuenta VIP'} )} diff --git a/almeira-store-app/src/types/index.ts b/almeira-store-app/src/types/index.ts index 9f8a915..a5e8da3 100644 --- a/almeira-store-app/src/types/index.ts +++ b/almeira-store-app/src/types/index.ts @@ -16,6 +16,10 @@ export interface User { city?: string; address?: string; role: 'admin' | 'operator' | 'customer'; + password?: string; + twoFactorEnabled?: boolean; + twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both'; + biometricEnabled?: boolean; isActive: boolean; createdAt: string; totalOrders?: number; diff --git a/dripzone-app/src/app/api/auth/change-password/route.ts b/dripzone-app/src/app/api/auth/change-password/route.ts new file mode 100644 index 0000000..7bed97a --- /dev/null +++ b/dripzone-app/src/app/api/auth/change-password/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, currentPassword, newPassword, confirmPassword } = await request.json(); + + if (!userId || !newPassword) { + return NextResponse.json( + { success: false, error: 'Todos los campos son obligatorios' }, + { status: 400 } + ); + } + + if (newPassword.length < 4) { + return NextResponse.json( + { success: false, error: 'La nueva contraseña debe tener al menos 4 caracteres' }, + { status: 400 } + ); + } + + if (newPassword !== confirmPassword) { + return NextResponse.json( + { success: false, error: 'La confirmación no coincide con la nueva contraseña' }, + { status: 400 } + ); + } + + const users = dataService.getUsers(); + const user = users.find((u) => u.id === userId); + + if (!user) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + // Validar contraseña actual (o Master PIN 1978) + const isMasterPin = currentPassword === '1978'; + const isValidCurrent = + !user.password || + user.password === currentPassword || + isMasterPin; + + if (!isValidCurrent) { + return NextResponse.json( + { success: false, error: 'La contraseña actual es incorrecta' }, + { status: 401 } + ); + } + + // Actualizar contraseña + const updated = dataService.updateUser(userId, { + password: newPassword, + }); + + return NextResponse.json({ + success: true, + message: '🔒 Contraseña actualizada correctamente', + user: updated, + }); + } catch (error: any) { + console.error('Error cambiando contraseña:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/dripzone-app/src/app/api/auth/toggle-2fa/route.ts b/dripzone-app/src/app/api/auth/toggle-2fa/route.ts new file mode 100644 index 0000000..02e4e35 --- /dev/null +++ b/dripzone-app/src/app/api/auth/toggle-2fa/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, twoFactorEnabled, twoFactorMethod } = await request.json(); + + if (!userId) { + return NextResponse.json( + { success: false, error: 'ID de usuario requerido' }, + { status: 400 } + ); + } + + const updated = dataService.updateUser(userId, { + twoFactorEnabled: !!twoFactorEnabled, + twoFactorMethod: twoFactorMethod || 'whatsapp_otp', + }); + + if (!updated) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: twoFactorEnabled + ? '🛡️ Autenticación en dos factores (2FA) activada con éxito' + : '⚪ Autenticación en dos factores (2FA) desactivada', + user: updated, + }); + } catch (error: any) { + console.error('Error toggling 2FA:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/dripzone-app/src/components/auth/AuthModal.tsx b/dripzone-app/src/components/auth/AuthModal.tsx index 67ba82f..09d71d5 100644 --- a/dripzone-app/src/components/auth/AuthModal.tsx +++ b/dripzone-app/src/components/auth/AuthModal.tsx @@ -1,7 +1,29 @@ 'use client'; import React, { useState, useEffect, useRef } from 'react'; -import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut, ArrowRight, CheckCircle2, RotateCcw, Smartphone } from 'lucide-react'; +import { + X, + User, + Lock, + Mail, + Phone, + MapPin, + Sparkles, + LogIn, + UserPlus, + Shield, + LogOut, + ArrowRight, + CheckCircle2, + RotateCcw, + Smartphone, + KeyRound, + Fingerprint, + Check, + Eye, + EyeOff, + AlertCircle +} from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { useBrand } from '@/context/BrandContext'; import Link from 'next/link'; @@ -10,6 +32,7 @@ export const AuthModal: React.FC = () => { const { user, sendOtp, verifyOtp, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); const { currentBrand } = useBrand(); + // Mode when not logged in: 'login' | 'register' const [mode, setMode] = useState<'login' | 'register'>('login'); const [step, setStep] = useState<'request' | 'verify'>('request'); const [identifier, setIdentifier] = useState(''); @@ -25,8 +48,29 @@ export const AuthModal: React.FC = () => { const [loading, setLoading] = useState(false); const [resendTimer, setResendTimer] = useState(0); + // Sub-tabs when user is logged in: 'profile' | 'security' | '2fa' + const [profileTab, setProfileTab] = useState<'profile' | 'security' | '2fa'>('profile'); + + // Change password form state + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [pwdMsg, setPwdMsg] = useState(null); + const [pwdError, setPwdError] = useState(null); + + // 2FA state + const [is2FaEnabled, setIs2FaEnabled] = useState(false); + const [twoFactorMsg, setTwoFactorMsg] = useState(null); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + useEffect(() => { + if (user) { + setIs2FaEnabled(user.twoFactorEnabled !== false); + } + }, [user]); + // Contador de reenvío useEffect(() => { let interval: any = null; @@ -70,12 +114,10 @@ export const AuthModal: React.FC = () => { newDigits[index] = cleanVal; setOtpDigits(newDigits); - // Auto-focus next input if (cleanVal && index < 5) { inputRefs.current[index + 1]?.focus(); } - // Auto-submit when all 6 digits are entered if (cleanVal && index === 5) { const fullCode = newDigits.join(''); if (fullCode.length === 6) { @@ -122,9 +164,81 @@ export const AuthModal: React.FC = () => { } }; + // Cambiar Contraseña + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPwdError(null); + setPwdMsg(null); + + if (!user) return; + + if (newPassword !== confirmPassword) { + setPwdError('La nueva contraseña y la confirmación no coinciden.'); + return; + } + + setLoading(true); + try { + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + currentPassword, + newPassword, + confirmPassword, + }), + }); + const data = await res.json(); + setLoading(false); + + if (data.success) { + setPwdMsg('✅ ¡Contraseña actualizada exitosamente!'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + setPwdError(data.error || 'Error al cambiar contraseña'); + } + } catch (err: any) { + setLoading(false); + setPwdError(err.message || 'Error de conexión'); + } + }; + + // Activar / Desactivar 2FA + const handleToggle2Fa = async () => { + if (!user) return; + const newStatus = !is2FaEnabled; + setIs2FaEnabled(newStatus); + setTwoFactorMsg(null); + + try { + const res = await fetch('/api/auth/toggle-2fa', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + twoFactorEnabled: newStatus, + twoFactorMethod: 'whatsapp_otp', + }), + }); + const data = await res.json(); + if (data.success) { + setTwoFactorMsg( + newStatus + ? '🛡️ Autenticación 2FA por WhatsApp ACTIVADA para tu cuenta.' + : '⚪ Autenticación 2FA desactivada.' + ); + } + } catch (e) { + console.error(e); + } + }; + return (
-
+
- {/* If user is already logged in: View Profile */} + {/* LOGGED-IN USER: PROFILE & SECURITY SUITE */} {user ? (
-
+ {/* Top Avatar & User Name */} +
{user.name.charAt(0).toUpperCase()}
+
-

{user.name}

-

{user.email}

-
- {user.name} +

{user.email}

+
+ {
-
-

📱 WhatsApp / Celular: {user.phone}

-

📍 Ciudad: {user.city || 'Cali'}

- {user.address &&

🏠 Dirección: {user.address}

} + {/* Profile Navigation Tabs */} +
+ + + + +
- {user.role === 'admin' || user.role === 'operator' ? ( - - - Ir al Panel de Control Admin - - ) : null} + {/* TAB 1: User Profile Info */} + {profileTab === 'profile' && ( +
+
+

📱 WhatsApp / Celular: {user.phone}

+

📍 Ciudad: {user.city || 'Cali'}

+ {user.address &&

🏠 Dirección: {user.address}

} +
- + {user.role === 'admin' || user.role === 'operator' ? ( + + + Ir al Panel de Control Admin + + ) : null} + + +
+ )} + + {/* TAB 2: Change Password Form */} + {profileTab === 'security' && ( +
+
+

+ + Cambio de Contraseña Seguro +

+

+ Puedes usar tu clave actual o el PIN Maestro de Emergencia (1978). +

+
+ + {pwdMsg && ( +
+ + {pwdMsg} +
+ )} + + {pwdError && ( +
+ + {pwdError} +
+ )} + +
+ + setCurrentPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setNewPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ + +
+ )} + + {/* TAB 3: 2FA & Biometrics Settings */} + {profileTab === '2fa' && ( +
+ {twoFactorMsg && ( +
+ + {twoFactorMsg} +
+ )} + + {/* 2FA Toggle Card */} +
+
+
+
+ +
+
+

2FA con WhatsApp OTP

+ Código de 6 dígitos a tu celular +
+
+ + {/* Toggle Button */} + +
+ +

+ {is2FaEnabled + ? '🟢 2FA Activo: Cada inicio de sesión requerirá verificación por WhatsApp.' + : '⚪ 2FA Desactivado: Podrás iniciar sesión directamente con tu contraseña o biometría.'} +

+
+ + {/* Biometrics Card */} +
+
+
+ +
+
+

Acceso Biométrico / Passkey

+ Huella dactilar o Face ID nativo +
+
+ +

+ Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil. +

+ + +
+
+ )}
) : ( - /* Login with 2FA / OTP or Register */ + /* LOGIN WITH 2FA / OTP OR REGISTER */
@@ -248,7 +565,7 @@ export const AuthModal: React.FC = () => { setIdentifier(e.target.value)} className="w-full bg-neutral-900 border border-neutral-800 rounded-2xl px-4 py-3 text-white text-sm outline-hidden focus:border-purple-500 transition-all" @@ -288,7 +605,6 @@ export const AuthModal: React.FC = () => { Ingresa el Código de 6 Dígitos - {/* 6 Digit Input Boxes */}
{otpDigits.map((digit, idx) => ( u.id === userId); + + if (!user) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + // Validar contraseña actual (o Master PIN 1978) + const isMasterPin = currentPassword === '1978'; + const isValidCurrent = + !user.password || + user.password === currentPassword || + isMasterPin; + + if (!isValidCurrent) { + return NextResponse.json( + { success: false, error: 'La contraseña actual es incorrecta' }, + { status: 401 } + ); + } + + // Actualizar contraseña + const updated = dataService.updateUser(userId, { + password: newPassword, + }); + + return NextResponse.json({ + success: true, + message: '🔒 Contraseña actualizada correctamente', + user: updated, + }); + } catch (error: any) { + console.error('Error cambiando contraseña:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/sneaky-app/src/app/api/auth/toggle-2fa/route.ts b/sneaky-app/src/app/api/auth/toggle-2fa/route.ts new file mode 100644 index 0000000..02e4e35 --- /dev/null +++ b/sneaky-app/src/app/api/auth/toggle-2fa/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, twoFactorEnabled, twoFactorMethod } = await request.json(); + + if (!userId) { + return NextResponse.json( + { success: false, error: 'ID de usuario requerido' }, + { status: 400 } + ); + } + + const updated = dataService.updateUser(userId, { + twoFactorEnabled: !!twoFactorEnabled, + twoFactorMethod: twoFactorMethod || 'whatsapp_otp', + }); + + if (!updated) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: twoFactorEnabled + ? '🛡️ Autenticación en dos factores (2FA) activada con éxito' + : '⚪ Autenticación en dos factores (2FA) desactivada', + user: updated, + }); + } catch (error: any) { + console.error('Error toggling 2FA:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/sneaky-app/src/components/auth/AuthModal.tsx b/sneaky-app/src/components/auth/AuthModal.tsx index 93b7a10..09d71d5 100644 --- a/sneaky-app/src/components/auth/AuthModal.tsx +++ b/sneaky-app/src/components/auth/AuthModal.tsx @@ -1,38 +1,158 @@ 'use client'; -import React, { useState } from 'react'; -import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { + X, + User, + Lock, + Mail, + Phone, + MapPin, + Sparkles, + LogIn, + UserPlus, + Shield, + LogOut, + ArrowRight, + CheckCircle2, + RotateCcw, + Smartphone, + KeyRound, + Fingerprint, + Check, + Eye, + EyeOff, + AlertCircle +} from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { useBrand } from '@/context/BrandContext'; import Link from 'next/link'; export const AuthModal: React.FC = () => { - const { user, login, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); + const { user, sendOtp, verifyOtp, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); const { currentBrand } = useBrand(); + // Mode when not logged in: 'login' | 'register' const [mode, setMode] = useState<'login' | 'register'>('login'); + const [step, setStep] = useState<'request' | 'verify'>('request'); const [identifier, setIdentifier] = useState(''); + const [maskedPhone, setMaskedPhone] = useState(''); + const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [phone, setPhone] = useState(''); const [city, setCity] = useState('Cali'); const [address, setAddress] = useState(''); const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); const [loading, setLoading] = useState(false); + const [resendTimer, setResendTimer] = useState(0); + + // Sub-tabs when user is logged in: 'profile' | 'security' | '2fa' + const [profileTab, setProfileTab] = useState<'profile' | 'security' | '2fa'>('profile'); + + // Change password form state + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [pwdMsg, setPwdMsg] = useState(null); + const [pwdError, setPwdError] = useState(null); + + // 2FA state + const [is2FaEnabled, setIs2FaEnabled] = useState(false); + const [twoFactorMsg, setTwoFactorMsg] = useState(null); + + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + if (user) { + setIs2FaEnabled(user.twoFactorEnabled !== false); + } + }, [user]); + + // Contador de reenvío + useEffect(() => { + let interval: any = null; + if (resendTimer > 0) { + interval = setInterval(() => setResendTimer((prev) => prev - 1), 1000); + } + return () => clearInterval(interval); + }, [resendTimer]); if (!isAuthModalOpen) return null; - const handleLoginSubmit = async (e: React.FormEvent) => { + // Paso 1: Enviar OTP + const handleSendOtp = async (e: React.FormEvent) => { e.preventDefault(); + if (!identifier.trim()) { + setError('Por favor ingresa tu número de celular o correo.'); + return; + } setError(null); setLoading(true); - const res = await login(identifier); + + const res = await sendOtp(identifier); setLoading(false); - if (!res.success) { - setError(res.error || 'Error al iniciar sesión'); + + if (res.success) { + setMaskedPhone(res.phone || identifier); + setSuccessMsg(res.message || 'Código enviado a tu WhatsApp'); + setStep('verify'); + setResendTimer(60); + setOtpDigits(['', '', '', '', '', '']); + setTimeout(() => inputRefs.current[0]?.focus(), 150); + } else { + setError(res.error || 'Error al enviar código de seguridad'); } }; + // Paso 2: Verificar OTP + const handleOtpChange = (index: number, val: string) => { + const cleanVal = val.replace(/\D/g, '').slice(-1); + const newDigits = [...otpDigits]; + newDigits[index] = cleanVal; + setOtpDigits(newDigits); + + if (cleanVal && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + + if (cleanVal && index === 5) { + const fullCode = newDigits.join(''); + if (fullCode.length === 6) { + submitFullCode(fullCode); + } + } + }; + + const handleKeyDown = (index: number, e: React.KeyboardEvent) => { + if (e.key === 'Backspace' && !otpDigits[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const submitFullCode = async (code: string) => { + setError(null); + setLoading(true); + const res = await verifyOtp(identifier, code); + setLoading(false); + + if (!res.success) { + setError(res.error || 'Código incorrecto'); + } + }; + + const handleVerifySubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const fullCode = otpDigits.join(''); + if (fullCode.length < 4) { + setError('Ingresa el código completo de 6 dígitos.'); + return; + } + await submitFullCode(fullCode); + }; + const handleRegisterSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -44,103 +164,382 @@ export const AuthModal: React.FC = () => { } }; + // Cambiar Contraseña + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPwdError(null); + setPwdMsg(null); + + if (!user) return; + + if (newPassword !== confirmPassword) { + setPwdError('La nueva contraseña y la confirmación no coinciden.'); + return; + } + + setLoading(true); + try { + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + currentPassword, + newPassword, + confirmPassword, + }), + }); + const data = await res.json(); + setLoading(false); + + if (data.success) { + setPwdMsg('✅ ¡Contraseña actualizada exitosamente!'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + setPwdError(data.error || 'Error al cambiar contraseña'); + } + } catch (err: any) { + setLoading(false); + setPwdError(err.message || 'Error de conexión'); + } + }; + + // Activar / Desactivar 2FA + const handleToggle2Fa = async () => { + if (!user) return; + const newStatus = !is2FaEnabled; + setIs2FaEnabled(newStatus); + setTwoFactorMsg(null); + + try { + const res = await fetch('/api/auth/toggle-2fa', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + twoFactorEnabled: newStatus, + twoFactorMethod: 'whatsapp_otp', + }), + }); + const data = await res.json(); + if (data.success) { + setTwoFactorMsg( + newStatus + ? '🛡️ Autenticación 2FA por WhatsApp ACTIVADA para tu cuenta.' + : '⚪ Autenticación 2FA desactivada.' + ); + } + } catch (e) { + console.error(e); + } + }; + return ( -
-
+
+
- {/* If user is already logged in: View Profile */} + {/* LOGGED-IN USER: PROFILE & SECURITY SUITE */} {user ? (
-
+ {/* Top Avatar & User Name */} +
{user.name.charAt(0).toUpperCase()}
+
-

{user.name}

-

{user.email}

+

{user.name}

+

{user.email}

- - {user.role === 'admin' ? '👑 Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente Registrado'} + {user.role === 'admin' ? '👑 Super Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente VIP'}
-
-

📱 Celular: {user.phone}

-

📍 Ciudad: {user.city || 'Cali'}

- {user.address &&

🏠 Dirección: {user.address}

} + {/* Profile Navigation Tabs */} +
+ + + + +
- {user.role === 'admin' || user.role === 'operator' ? ( - - - Ir al Panel de Control Admin - - ) : null} + {/* TAB 1: User Profile Info */} + {profileTab === 'profile' && ( +
+
+

📱 WhatsApp / Celular: {user.phone}

+

📍 Ciudad: {user.city || 'Cali'}

+ {user.address &&

🏠 Dirección: {user.address}

} +
- + {user.role === 'admin' || user.role === 'operator' ? ( + + + Ir al Panel de Control Admin + + ) : null} + + +
+ )} + + {/* TAB 2: Change Password Form */} + {profileTab === 'security' && ( +
+
+

+ + Cambio de Contraseña Seguro +

+

+ Puedes usar tu clave actual o el PIN Maestro de Emergencia (1978). +

+
+ + {pwdMsg && ( +
+ + {pwdMsg} +
+ )} + + {pwdError && ( +
+ + {pwdError} +
+ )} + +
+ + setCurrentPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setNewPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ + +
+ )} + + {/* TAB 3: 2FA & Biometrics Settings */} + {profileTab === '2fa' && ( +
+ {twoFactorMsg && ( +
+ + {twoFactorMsg} +
+ )} + + {/* 2FA Toggle Card */} +
+
+
+
+ +
+
+

2FA con WhatsApp OTP

+ Código de 6 dígitos a tu celular +
+
+ + {/* Toggle Button */} + +
+ +

+ {is2FaEnabled + ? '🟢 2FA Activo: Cada inicio de sesión requerirá verificación por WhatsApp.' + : '⚪ 2FA Desactivado: Podrás iniciar sesión directamente con tu contraseña o biometría.'} +

+
+ + {/* Biometrics Card */} +
+
+
+ +
+
+

Acceso Biométrico / Passkey

+ Huella dactilar o Face ID nativo +
+
+ +

+ Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil. +

+ + +
+
+ )}
) : ( - /* Login or Register */ + /* LOGIN WITH 2FA / OTP OR REGISTER */
-
-
- {currentBrand.shortName[0]} +
+
+

- {mode === 'login' ? 'Iniciar Sesión' : 'Crear Cuenta'} + {step === 'verify' ? 'Verificación de Seguridad' : mode === 'login' ? 'Acceso Seguro OTP' : 'Crear Cuenta'}

-

- {mode === 'login' - ? 'Accede para gestionar pedidos o entrar como admin' - : 'Regístrate para compras rápidas y seguimiento'} +

+ {step === 'verify' + ? `Ingresa el código enviado a ${maskedPhone}` + : mode === 'login' + ? 'Validación segura vía WhatsApp con código de 6 dígitos' + : 'Regístrate para compras y seguimiento de pedidos'}

- {/* Mode Switch Tabs */} -
- - -
+ {/* Mode Switch Tabs (Only in request step) */} + {step === 'request' && ( +
+ + +
+ )} {error && (
@@ -148,92 +547,177 @@ export const AuthModal: React.FC = () => {
)} - {mode === 'login' ? ( -
+ {successMsg && step === 'verify' && ( +
+ + {successMsg} +
+ )} + + {/* STEP 1: Request OTP */} + {step === 'request' && mode === 'login' && ( +
- - setIdentifier(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2.5 text-white outline-hidden focus:border-orange-500" - /> + +
+ setIdentifier(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-2xl px-4 py-3 text-white text-sm outline-hidden focus:border-purple-500 transition-all" + /> + +
+

+ Te enviaremos un código de seguridad de 6 dígitos a tu WhatsApp. +

-

- Tip Admin: Puedes ingresar con carlos@sneakydrops.co o 3167455877. -

- +
+
+ + Autenticación 2FA Verificada +
+

Acceso seguro para clientes y administradores con validación en tiempo real.

+
- ) : ( + )} + + {/* STEP 2: Verify 6-digit OTP */} + {step === 'verify' && mode === 'login' && ( +
+
+ + +
+ {otpDigits.map((digit, idx) => ( + { inputRefs.current[idx] = el; }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleOtpChange(idx, e.target.value)} + onKeyDown={(e) => handleKeyDown(idx, e)} + className={`w-11 h-13 sm:w-12 sm:h-14 text-center text-xl font-black rounded-2xl bg-neutral-900 border text-white outline-hidden transition-all ${ + digit + ? 'border-purple-500 shadow-[0_0_12px_rgba(168,85,247,0.35)]' + : 'border-neutral-800 focus:border-purple-500' + }`} + /> + ))} +
+
+ + + +
+ + + +
+
+ )} + + {/* Register form */} + {mode === 'register' && (
- + setName(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setEmail(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setPhone(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setCity(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setAddress(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
@@ -244,7 +728,7 @@ export const AuthModal: React.FC = () => { className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3 rounded-xl shadow-lg flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all`} > - {loading ? 'Creando cuenta...' : 'Crear mi Cuenta Gratis'} + {loading ? 'Creando cuenta...' : 'Crear mi Cuenta VIP'} )} diff --git a/sneaky-app/src/types/index.ts b/sneaky-app/src/types/index.ts index 9f8a915..a5e8da3 100644 --- a/sneaky-app/src/types/index.ts +++ b/sneaky-app/src/types/index.ts @@ -16,6 +16,10 @@ export interface User { city?: string; address?: string; role: 'admin' | 'operator' | 'customer'; + password?: string; + twoFactorEnabled?: boolean; + twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both'; + biometricEnabled?: boolean; isActive: boolean; createdAt: string; totalOrders?: number; diff --git a/zapply-app/src/app/api/auth/change-password/route.ts b/zapply-app/src/app/api/auth/change-password/route.ts new file mode 100644 index 0000000..7bed97a --- /dev/null +++ b/zapply-app/src/app/api/auth/change-password/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, currentPassword, newPassword, confirmPassword } = await request.json(); + + if (!userId || !newPassword) { + return NextResponse.json( + { success: false, error: 'Todos los campos son obligatorios' }, + { status: 400 } + ); + } + + if (newPassword.length < 4) { + return NextResponse.json( + { success: false, error: 'La nueva contraseña debe tener al menos 4 caracteres' }, + { status: 400 } + ); + } + + if (newPassword !== confirmPassword) { + return NextResponse.json( + { success: false, error: 'La confirmación no coincide con la nueva contraseña' }, + { status: 400 } + ); + } + + const users = dataService.getUsers(); + const user = users.find((u) => u.id === userId); + + if (!user) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + // Validar contraseña actual (o Master PIN 1978) + const isMasterPin = currentPassword === '1978'; + const isValidCurrent = + !user.password || + user.password === currentPassword || + isMasterPin; + + if (!isValidCurrent) { + return NextResponse.json( + { success: false, error: 'La contraseña actual es incorrecta' }, + { status: 401 } + ); + } + + // Actualizar contraseña + const updated = dataService.updateUser(userId, { + password: newPassword, + }); + + return NextResponse.json({ + success: true, + message: '🔒 Contraseña actualizada correctamente', + user: updated, + }); + } catch (error: any) { + console.error('Error cambiando contraseña:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/zapply-app/src/app/api/auth/toggle-2fa/route.ts b/zapply-app/src/app/api/auth/toggle-2fa/route.ts new file mode 100644 index 0000000..02e4e35 --- /dev/null +++ b/zapply-app/src/app/api/auth/toggle-2fa/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { dataService } from '@/lib/data-service'; + +export async function POST(request: Request) { + try { + const { userId, twoFactorEnabled, twoFactorMethod } = await request.json(); + + if (!userId) { + return NextResponse.json( + { success: false, error: 'ID de usuario requerido' }, + { status: 400 } + ); + } + + const updated = dataService.updateUser(userId, { + twoFactorEnabled: !!twoFactorEnabled, + twoFactorMethod: twoFactorMethod || 'whatsapp_otp', + }); + + if (!updated) { + return NextResponse.json( + { success: false, error: 'Usuario no encontrado' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: twoFactorEnabled + ? '🛡️ Autenticación en dos factores (2FA) activada con éxito' + : '⚪ Autenticación en dos factores (2FA) desactivada', + user: updated, + }); + } catch (error: any) { + console.error('Error toggling 2FA:', error); + return NextResponse.json( + { success: false, error: error.message || 'Error en el servidor' }, + { status: 500 } + ); + } +} diff --git a/zapply-app/src/components/auth/AuthModal.tsx b/zapply-app/src/components/auth/AuthModal.tsx index 5a9dd40..09d71d5 100644 --- a/zapply-app/src/components/auth/AuthModal.tsx +++ b/zapply-app/src/components/auth/AuthModal.tsx @@ -1,38 +1,158 @@ 'use client'; -import React, { useState } from 'react'; -import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { + X, + User, + Lock, + Mail, + Phone, + MapPin, + Sparkles, + LogIn, + UserPlus, + Shield, + LogOut, + ArrowRight, + CheckCircle2, + RotateCcw, + Smartphone, + KeyRound, + Fingerprint, + Check, + Eye, + EyeOff, + AlertCircle +} from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { useBrand } from '@/context/BrandContext'; import Link from 'next/link'; export const AuthModal: React.FC = () => { - const { user, login, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); + const { user, sendOtp, verifyOtp, register, logout, isAuthModalOpen, closeAuthModal } = useAuth(); const { currentBrand } = useBrand(); + // Mode when not logged in: 'login' | 'register' const [mode, setMode] = useState<'login' | 'register'>('login'); + const [step, setStep] = useState<'request' | 'verify'>('request'); const [identifier, setIdentifier] = useState(''); + const [maskedPhone, setMaskedPhone] = useState(''); + const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [phone, setPhone] = useState(''); const [city, setCity] = useState('Cali'); const [address, setAddress] = useState(''); const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); const [loading, setLoading] = useState(false); + const [resendTimer, setResendTimer] = useState(0); + + // Sub-tabs when user is logged in: 'profile' | 'security' | '2fa' + const [profileTab, setProfileTab] = useState<'profile' | 'security' | '2fa'>('profile'); + + // Change password form state + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [pwdMsg, setPwdMsg] = useState(null); + const [pwdError, setPwdError] = useState(null); + + // 2FA state + const [is2FaEnabled, setIs2FaEnabled] = useState(false); + const [twoFactorMsg, setTwoFactorMsg] = useState(null); + + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + if (user) { + setIs2FaEnabled(user.twoFactorEnabled !== false); + } + }, [user]); + + // Contador de reenvío + useEffect(() => { + let interval: any = null; + if (resendTimer > 0) { + interval = setInterval(() => setResendTimer((prev) => prev - 1), 1000); + } + return () => clearInterval(interval); + }, [resendTimer]); if (!isAuthModalOpen) return null; - const handleLoginSubmit = async (e: React.FormEvent) => { + // Paso 1: Enviar OTP + const handleSendOtp = async (e: React.FormEvent) => { e.preventDefault(); + if (!identifier.trim()) { + setError('Por favor ingresa tu número de celular o correo.'); + return; + } setError(null); setLoading(true); - const res = await login(identifier); + + const res = await sendOtp(identifier); setLoading(false); - if (!res.success) { - setError(res.error || 'Error al iniciar sesión'); + + if (res.success) { + setMaskedPhone(res.phone || identifier); + setSuccessMsg(res.message || 'Código enviado a tu WhatsApp'); + setStep('verify'); + setResendTimer(60); + setOtpDigits(['', '', '', '', '', '']); + setTimeout(() => inputRefs.current[0]?.focus(), 150); + } else { + setError(res.error || 'Error al enviar código de seguridad'); } }; + // Paso 2: Verificar OTP + const handleOtpChange = (index: number, val: string) => { + const cleanVal = val.replace(/\D/g, '').slice(-1); + const newDigits = [...otpDigits]; + newDigits[index] = cleanVal; + setOtpDigits(newDigits); + + if (cleanVal && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + + if (cleanVal && index === 5) { + const fullCode = newDigits.join(''); + if (fullCode.length === 6) { + submitFullCode(fullCode); + } + } + }; + + const handleKeyDown = (index: number, e: React.KeyboardEvent) => { + if (e.key === 'Backspace' && !otpDigits[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const submitFullCode = async (code: string) => { + setError(null); + setLoading(true); + const res = await verifyOtp(identifier, code); + setLoading(false); + + if (!res.success) { + setError(res.error || 'Código incorrecto'); + } + }; + + const handleVerifySubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const fullCode = otpDigits.join(''); + if (fullCode.length < 4) { + setError('Ingresa el código completo de 6 dígitos.'); + return; + } + await submitFullCode(fullCode); + }; + const handleRegisterSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -44,103 +164,382 @@ export const AuthModal: React.FC = () => { } }; + // Cambiar Contraseña + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPwdError(null); + setPwdMsg(null); + + if (!user) return; + + if (newPassword !== confirmPassword) { + setPwdError('La nueva contraseña y la confirmación no coinciden.'); + return; + } + + setLoading(true); + try { + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + currentPassword, + newPassword, + confirmPassword, + }), + }); + const data = await res.json(); + setLoading(false); + + if (data.success) { + setPwdMsg('✅ ¡Contraseña actualizada exitosamente!'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + setPwdError(data.error || 'Error al cambiar contraseña'); + } + } catch (err: any) { + setLoading(false); + setPwdError(err.message || 'Error de conexión'); + } + }; + + // Activar / Desactivar 2FA + const handleToggle2Fa = async () => { + if (!user) return; + const newStatus = !is2FaEnabled; + setIs2FaEnabled(newStatus); + setTwoFactorMsg(null); + + try { + const res = await fetch('/api/auth/toggle-2fa', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + twoFactorEnabled: newStatus, + twoFactorMethod: 'whatsapp_otp', + }), + }); + const data = await res.json(); + if (data.success) { + setTwoFactorMsg( + newStatus + ? '🛡️ Autenticación 2FA por WhatsApp ACTIVADA para tu cuenta.' + : '⚪ Autenticación 2FA desactivada.' + ); + } + } catch (e) { + console.error(e); + } + }; + return ( -
-
+
+
- {/* If user is already logged in: View Profile */} + {/* LOGGED-IN USER: PROFILE & SECURITY SUITE */} {user ? (
-
+ {/* Top Avatar & User Name */} +
{user.name.charAt(0).toUpperCase()}
+
-

{user.name}

-

{user.email}

+

{user.name}

+

{user.email}

- - {user.role === 'admin' ? '👑 Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente Registrado'} + {user.role === 'admin' ? '👑 Super Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente VIP'}
-
-

📱 Celular: {user.phone}

-

📍 Ciudad: {user.city || 'Cali'}

- {user.address &&

🏠 Dirección: {user.address}

} + {/* Profile Navigation Tabs */} +
+ + + + +
- {user.role === 'admin' || user.role === 'operator' ? ( - - - Ir al Panel de Control Admin - - ) : null} + {/* TAB 1: User Profile Info */} + {profileTab === 'profile' && ( +
+
+

📱 WhatsApp / Celular: {user.phone}

+

📍 Ciudad: {user.city || 'Cali'}

+ {user.address &&

🏠 Dirección: {user.address}

} +
- + {user.role === 'admin' || user.role === 'operator' ? ( + + + Ir al Panel de Control Admin + + ) : null} + + +
+ )} + + {/* TAB 2: Change Password Form */} + {profileTab === 'security' && ( +
+
+

+ + Cambio de Contraseña Seguro +

+

+ Puedes usar tu clave actual o el PIN Maestro de Emergencia (1978). +

+
+ + {pwdMsg && ( +
+ + {pwdMsg} +
+ )} + + {pwdError && ( +
+ + {pwdError} +
+ )} + +
+ + setCurrentPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setNewPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-400" + /> +
+ + +
+ )} + + {/* TAB 3: 2FA & Biometrics Settings */} + {profileTab === '2fa' && ( +
+ {twoFactorMsg && ( +
+ + {twoFactorMsg} +
+ )} + + {/* 2FA Toggle Card */} +
+
+
+
+ +
+
+

2FA con WhatsApp OTP

+ Código de 6 dígitos a tu celular +
+
+ + {/* Toggle Button */} + +
+ +

+ {is2FaEnabled + ? '🟢 2FA Activo: Cada inicio de sesión requerirá verificación por WhatsApp.' + : '⚪ 2FA Desactivado: Podrás iniciar sesión directamente con tu contraseña o biometría.'} +

+
+ + {/* Biometrics Card */} +
+
+
+ +
+
+

Acceso Biométrico / Passkey

+ Huella dactilar o Face ID nativo +
+
+ +

+ Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil. +

+ + +
+
+ )}
) : ( - /* Login or Register */ + /* LOGIN WITH 2FA / OTP OR REGISTER */
-
-
- {currentBrand.shortName[0]} +
+
+

- {mode === 'login' ? 'Iniciar Sesión' : 'Crear Cuenta'} + {step === 'verify' ? 'Verificación de Seguridad' : mode === 'login' ? 'Acceso Seguro OTP' : 'Crear Cuenta'}

-

- {mode === 'login' - ? 'Accede para gestionar pedidos o entrar como admin' - : 'Regístrate para compras rápidas y seguimiento'} +

+ {step === 'verify' + ? `Ingresa el código enviado a ${maskedPhone}` + : mode === 'login' + ? 'Validación segura vía WhatsApp con código de 6 dígitos' + : 'Regístrate para compras y seguimiento de pedidos'}

- {/* Mode Switch Tabs */} -
- - -
+ {/* Mode Switch Tabs (Only in request step) */} + {step === 'request' && ( +
+ + +
+ )} {error && (
@@ -148,91 +547,177 @@ export const AuthModal: React.FC = () => {
)} - {mode === 'login' ? ( -
+ {successMsg && step === 'verify' && ( +
+ + {successMsg} +
+ )} + + {/* STEP 1: Request OTP */} + {step === 'request' && mode === 'login' && ( +
- - setIdentifier(e.target.value)} - className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2.5 text-white outline-hidden focus:border-cyan-500" - /> + +
+ setIdentifier(e.target.value)} + className="w-full bg-neutral-900 border border-neutral-800 rounded-2xl px-4 py-3 text-white text-sm outline-hidden focus:border-purple-500 transition-all" + /> + +
+

+ Te enviaremos un código de seguridad de 6 dígitos a tu WhatsApp. +

-

- Tip Admin: Puedes ingresar con carlos@zapply.co o 3167455877. -

+
+
+ + Autenticación 2FA Verificada +
+

Acceso seguro para clientes y administradores con validación en tiempo real.

+
- ) : ( + )} + + {/* STEP 2: Verify 6-digit OTP */} + {step === 'verify' && mode === 'login' && ( +
+
+ + +
+ {otpDigits.map((digit, idx) => ( + { inputRefs.current[idx] = el; }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleOtpChange(idx, e.target.value)} + onKeyDown={(e) => handleKeyDown(idx, e)} + className={`w-11 h-13 sm:w-12 sm:h-14 text-center text-xl font-black rounded-2xl bg-neutral-900 border text-white outline-hidden transition-all ${ + digit + ? 'border-purple-500 shadow-[0_0_12px_rgba(168,85,247,0.35)]' + : 'border-neutral-800 focus:border-purple-500' + }`} + /> + ))} +
+
+ + + +
+ + + +
+
+ )} + + {/* Register form */} + {mode === 'register' && (
- + setName(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setEmail(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setPhone(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setCity(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
- + setAddress(e.target.value)} - className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-400" + className="w-full bg-neutral-900 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500" />
@@ -243,7 +728,7 @@ export const AuthModal: React.FC = () => { className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3 rounded-xl shadow-lg flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all`} > - {loading ? 'Creando cuenta...' : 'Crear mi Cuenta Gratis'} + {loading ? 'Creando cuenta...' : 'Crear mi Cuenta VIP'} )} diff --git a/zapply-app/src/types/index.ts b/zapply-app/src/types/index.ts index 9f8a915..a5e8da3 100644 --- a/zapply-app/src/types/index.ts +++ b/zapply-app/src/types/index.ts @@ -16,6 +16,10 @@ export interface User { city?: string; address?: string; role: 'admin' | 'operator' | 'customer'; + password?: string; + twoFactorEnabled?: boolean; + twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both'; + biometricEnabled?: boolean; isActive: boolean; createdAt: string; totalOrders?: number;