feat(security): implementar cambio de clave, 2FA con WhatsApp y script de envio automatico de APK por WhatsApp
This commit is contained in:
parent
63cbeb3868
commit
4e1476e1a9
70
almeira-store-app/src/app/api/auth/change-password/route.ts
Normal file
70
almeira-store-app/src/app/api/auth/change-password/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
almeira-store-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
41
almeira-store-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +1,158 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut } 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 { useAuth } from '@/context/AuthContext';
|
||||||
import { useBrand } from '@/context/BrandContext';
|
import { useBrand } from '@/context/BrandContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const AuthModal: React.FC = () => {
|
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();
|
const { currentBrand } = useBrand();
|
||||||
|
|
||||||
|
// Mode when not logged in: 'login' | 'register'
|
||||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||||
|
const [step, setStep] = useState<'request' | 'verify'>('request');
|
||||||
const [identifier, setIdentifier] = useState('');
|
const [identifier, setIdentifier] = useState('');
|
||||||
|
const [maskedPhone, setMaskedPhone] = useState('');
|
||||||
|
const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [city, setCity] = useState('Cali');
|
const [city, setCity] = useState('Cali');
|
||||||
const [address, setAddress] = useState('');
|
const [address, setAddress] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
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<string | null>(null);
|
||||||
|
const [pwdError, setPwdError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 2FA state
|
||||||
|
const [is2FaEnabled, setIs2FaEnabled] = useState(false);
|
||||||
|
const [twoFactorMsg, setTwoFactorMsg] = useState<string | null>(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;
|
if (!isAuthModalOpen) return null;
|
||||||
|
|
||||||
const handleLoginSubmit = async (e: React.FormEvent) => {
|
// Paso 1: Enviar OTP
|
||||||
|
const handleSendOtp = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!identifier.trim()) {
|
||||||
|
setError('Por favor ingresa tu número de celular o correo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await login(identifier);
|
|
||||||
|
const res = await sendOtp(identifier);
|
||||||
setLoading(false);
|
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<HTMLInputElement>) => {
|
||||||
|
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) => {
|
const handleRegisterSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
<div className="fixed inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
||||||
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-md w-full p-6 space-y-4 shadow-2xl relative">
|
<div className="bg-neutral-950 border border-purple-500/30 rounded-3xl max-w-md w-full p-6 sm:p-7 space-y-4 shadow-[0_20px_50px_rgba(168,85,247,0.25)] relative max-h-[90vh] overflow-y-auto">
|
||||||
<button
|
<button
|
||||||
onClick={closeAuthModal}
|
onClick={closeAuthModal}
|
||||||
className="absolute top-4 right-4 text-slate-400 hover:text-white p-1 rounded-full cursor-pointer"
|
className="absolute top-4 right-4 text-neutral-400 hover:text-white p-1 rounded-full cursor-pointer transition-all"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* If user is already logged in: View Profile */}
|
{/* LOGGED-IN USER: PROFILE & SECURITY SUITE */}
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className={`w-16 h-16 rounded-3xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-2xl mx-auto shadow-lg`}>
|
{/* Top Avatar & User Name */}
|
||||||
|
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-xl mx-auto shadow-lg shadow-purple-600/30`}>
|
||||||
{user.name.charAt(0).toUpperCase()}
|
{user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-lg">{user.name}</h3>
|
<h3 className="font-black text-white text-base leading-tight">{user.name}</h3>
|
||||||
<p className="text-xs text-slate-400">{user.email}</p>
|
<p className="text-[11px] text-neutral-400">{user.email}</p>
|
||||||
<div className="inline-block mt-1">
|
<div className="inline-block mt-1">
|
||||||
<span className={`text-[10px] font-bold px-2.5 py-0.5 rounded-full border ${
|
<span className={`text-[10px] font-bold px-3 py-0.5 rounded-full border ${
|
||||||
user.role === 'admin'
|
user.role === 'admin'
|
||||||
? 'bg-purple-500/20 text-purple-300 border-purple-500/40'
|
? 'bg-purple-500/20 text-purple-300 border-purple-500/40 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
|
||||||
: user.role === 'operator'
|
: user.role === 'operator'
|
||||||
? 'bg-cyan-500/20 text-cyan-300 border-cyan-500/40'
|
? 'bg-fuchsia-500/20 text-fuchsia-300 border-fuchsia-500/40'
|
||||||
: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
||||||
}`}>
|
}`}>
|
||||||
{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'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-950/80 border border-slate-800 rounded-2xl p-3 text-left text-xs space-y-1.5 text-slate-300">
|
{/* Profile Navigation Tabs */}
|
||||||
<p><strong>📱 Celular:</strong> {user.phone}</p>
|
<div className="grid grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1 text-[11px] font-bold">
|
||||||
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
<button
|
||||||
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
onClick={() => setProfileTab('profile')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'profile'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
👤 Perfil
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('security')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'security'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🔒 Contraseña
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('2fa')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === '2fa'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🛡️ 2FA & Huella
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.role === 'admin' || user.role === 'operator' ? (
|
{/* TAB 1: User Profile Info */}
|
||||||
<Link
|
{profileTab === 'profile' && (
|
||||||
href="/admin"
|
<div className="space-y-3 animate-in fade-in">
|
||||||
onClick={closeAuthModal}
|
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-3.5 text-left text-xs space-y-1.5 text-neutral-300">
|
||||||
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md`}
|
<p><strong>📱 WhatsApp / Celular:</strong> {user.phone}</p>
|
||||||
>
|
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
||||||
<Shield className="w-4 h-4" />
|
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
||||||
<span>Ir al Panel de Control Admin</span>
|
</div>
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<button
|
{user.role === 'admin' || user.role === 'operator' ? (
|
||||||
onClick={logout}
|
<Link
|
||||||
className="w-full bg-slate-800 hover:bg-red-950/40 hover:border-red-500/50 border border-transparent text-slate-300 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
href="/admin"
|
||||||
>
|
onClick={closeAuthModal}
|
||||||
<LogOut className="w-4 h-4" />
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3.5 rounded-2xl flex items-center justify-center gap-2 shadow-lg shadow-purple-600/30 transition-all hover:opacity-95`}
|
||||||
<span>Cerrar Sesión</span>
|
>
|
||||||
</button>
|
<Shield className="w-4 h-4" />
|
||||||
|
<span>Ir al Panel de Control Admin</span>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="w-full bg-neutral-900 hover:bg-red-950/40 hover:border-red-500/50 border border-neutral-800 text-neutral-400 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>Cerrar Sesión</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 2: Change Password Form */}
|
||||||
|
{profileTab === 'security' && (
|
||||||
|
<form onSubmit={handleChangePassword} className="space-y-3 text-left text-xs animate-in fade-in">
|
||||||
|
<div className="bg-purple-950/30 border border-purple-500/30 p-3 rounded-2xl text-[11px] text-purple-300">
|
||||||
|
<p className="font-bold flex items-center gap-1">
|
||||||
|
<Lock className="w-3.5 h-3.5 text-purple-400" />
|
||||||
|
<span>Cambio de Contraseña Seguro</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-neutral-400 mt-0.5">
|
||||||
|
Puedes usar tu clave actual o el PIN Maestro de Emergencia (<strong>1978</strong>).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pwdMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{pwdMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pwdError && (
|
||||||
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||||
|
<span>{pwdError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Contraseña Actual *</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1 flex items-center justify-between">
|
||||||
|
<span>Nueva Contraseña *</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="text-purple-400 hover:text-purple-300 text-[10px] flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
|
||||||
|
<span>{showPassword ? 'Ocultar' : 'Mostrar'}</span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Mínimo 4 caracteres"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Confirmar Nueva Contraseña *</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Repite la nueva contraseña"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-3 rounded-xl shadow-md flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all mt-2"
|
||||||
|
>
|
||||||
|
<KeyRound className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Actualizando...' : 'Guardar Nueva Contraseña'}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 3: 2FA & Biometrics Settings */}
|
||||||
|
{profileTab === '2fa' && (
|
||||||
|
<div className="space-y-4 text-left text-xs animate-in fade-in">
|
||||||
|
{twoFactorMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{twoFactorMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2FA Toggle Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-purple-500/20 border border-purple-500/30 flex items-center justify-center text-purple-400">
|
||||||
|
<Smartphone className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">2FA con WhatsApp OTP</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Código de 6 dígitos a tu celular</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleToggle2Fa}
|
||||||
|
className={`w-12 h-6 rounded-full transition-colors relative cursor-pointer ${
|
||||||
|
is2FaEnabled ? 'bg-purple-600' : 'bg-neutral-800 border border-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full bg-white transition-transform absolute top-1 left-1 ${
|
||||||
|
is2FaEnabled ? 'translate-x-6' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400 leading-relaxed">
|
||||||
|
{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.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Biometrics Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center text-emerald-400">
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">Acceso Biométrico / Passkey</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Huella dactilar o Face ID nativo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400">
|
||||||
|
Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setTwoFactorMsg('👆 Tu dispositivo es 100% compatible con Passkeys y Biometría WebAuthn.');
|
||||||
|
}}
|
||||||
|
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-500/40 text-emerald-300 font-bold py-2 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all text-xs"
|
||||||
|
>
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
<span>Verificar Sensor Biométrico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Login or Register */
|
/* LOGIN WITH 2FA / OTP OR REGISTER */
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3">
|
||||||
<div className={`w-9 h-9 rounded-xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md`}>
|
<div className={`w-10 h-10 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md shadow-purple-600/30`}>
|
||||||
{currentBrand.shortName[0]}
|
<Shield className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-base leading-tight">
|
<h3 className="font-black text-white text-base leading-tight">
|
||||||
{mode === 'login' ? 'Iniciar Sesión' : 'Crear Cuenta'}
|
{step === 'verify' ? 'Verificación de Seguridad' : mode === 'login' ? 'Acceso Seguro OTP' : 'Crear Cuenta'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-neutral-400">
|
||||||
{mode === 'login'
|
{step === 'verify'
|
||||||
? 'Accede para gestionar pedidos o entrar como admin'
|
? `Ingresa el código enviado a ${maskedPhone}`
|
||||||
: 'Regístrate para compras rápidas y seguimiento'}
|
: mode === 'login'
|
||||||
|
? 'Validación segura vía WhatsApp con código de 6 dígitos'
|
||||||
|
: 'Regístrate para compras y seguimiento de pedidos'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode Switch Tabs */}
|
{/* Mode Switch Tabs (Only in request step) */}
|
||||||
<div className="grid grid-cols-2 p-1 bg-slate-950 border border-slate-800 rounded-2xl">
|
{step === 'request' && (
|
||||||
<button
|
<div className="grid grid-cols-2 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setMode('login'); setError(null); }}
|
type="button"
|
||||||
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
onClick={() => { setMode('login'); setError(null); }}
|
||||||
mode === 'login' ? 'bg-white text-slate-950 shadow' : 'text-slate-400 hover:text-white'
|
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
}`}
|
mode === 'login' ? 'bg-purple-600 text-white shadow-md' : 'text-neutral-400 hover:text-white'
|
||||||
>
|
}`}
|
||||||
Ingresar
|
>
|
||||||
</button>
|
Acceso con OTP
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setMode('register'); setError(null); }}
|
type="button"
|
||||||
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
onClick={() => { setMode('register'); setError(null); }}
|
||||||
mode === 'register' ? 'bg-white text-slate-950 shadow' : 'text-slate-400 hover:text-white'
|
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
}`}
|
mode === 'register' ? 'bg-purple-600 text-white shadow-md' : 'text-neutral-400 hover:text-white'
|
||||||
>
|
}`}
|
||||||
Registrarme
|
>
|
||||||
</button>
|
Registrarme
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3.5 py-2.5 rounded-xl animate-in fade-in">
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3.5 py-2.5 rounded-xl animate-in fade-in">
|
||||||
@ -148,91 +547,177 @@ export const AuthModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === 'login' ? (
|
{successMsg && step === 'verify' && (
|
||||||
<form onSubmit={handleLoginSubmit} className="space-y-3 text-xs">
|
<div className="bg-emerald-950/80 border border-emerald-600 text-emerald-300 text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-2 animate-in fade-in">
|
||||||
|
<CheckCircle2 className="w-4 h-4 shrink-0 text-emerald-400" />
|
||||||
|
<span>{successMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 1: Request OTP */}
|
||||||
|
{step === 'request' && mode === 'login' && (
|
||||||
|
<form onSubmit={handleSendOtp} className="space-y-3.5 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Correo Electrónico o Celular *</label>
|
<label className="block text-neutral-300 font-bold mb-1.5 flex items-center justify-between">
|
||||||
<input
|
<span>Número de Celular (WhatsApp) o Correo *</span>
|
||||||
type="text"
|
</label>
|
||||||
required
|
<div className="relative">
|
||||||
placeholder="ej: carlos@almeira.com.co o 3134848203"
|
<input
|
||||||
value={identifier}
|
type="text"
|
||||||
onChange={(e) => setIdentifier(e.target.value)}
|
required
|
||||||
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"
|
placeholder="Ej: 3167455877 o carlos.andres.lasso@gmail.com"
|
||||||
/>
|
value={identifier}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<Smartphone className="w-4 h-4 text-neutral-500 absolute right-3.5 top-3.5" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-neutral-500 mt-1">
|
||||||
|
Te enviaremos un código de seguridad de 6 dígitos a tu WhatsApp.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
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`}
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3.5 rounded-2xl shadow-lg shadow-purple-600/30 flex items-center justify-center gap-2 cursor-pointer active:scale-95 transition-all text-xs`}
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<Lock className="w-4 h-4" />
|
||||||
<span>{loading ? 'Accediendo...' : 'Iniciar Sesión'}</span>
|
<span>{loading ? 'Generando código...' : 'Enviar Código por WhatsApp'}</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p className="text-[11px] text-center text-slate-500">
|
<div className="bg-neutral-900/60 border border-neutral-800/80 rounded-2xl p-3 text-[11px] text-neutral-400 space-y-1">
|
||||||
Tip Admin: Puedes ingresar con <strong>carlos@almeira.com.co</strong> o <strong>3134848203</strong>.
|
<div className="flex items-center gap-1.5 text-purple-400 font-bold">
|
||||||
</p>
|
<Shield className="w-3.5 h-3.5" />
|
||||||
|
<span>Autenticación 2FA Verificada</span>
|
||||||
|
</div>
|
||||||
|
<p>Acceso seguro para clientes y administradores con validación en tiempo real.</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{/* STEP 2: Verify 6-digit OTP */}
|
||||||
|
{step === 'verify' && mode === 'login' && (
|
||||||
|
<form onSubmit={handleVerifySubmit} className="space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-300 font-bold mb-2 text-center">
|
||||||
|
Ingresa el Código de 6 Dígitos
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-2 sm:gap-2.5">
|
||||||
|
{otpDigits.map((digit, idx) => (
|
||||||
|
<input
|
||||||
|
key={idx}
|
||||||
|
ref={(el) => { 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'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3.5 rounded-2xl shadow-lg shadow-purple-600/30 flex items-center justify-center gap-2 cursor-pointer active:scale-95 transition-all text-xs`}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Verificando...' : 'Verificar y Acceder'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setStep('request'); setError(null); setSuccessMsg(null); }}
|
||||||
|
className="text-neutral-400 hover:text-white cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
← Cambiar número
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={resendTimer > 0 || loading}
|
||||||
|
onClick={handleSendOtp}
|
||||||
|
className={`flex items-center gap-1 font-bold cursor-pointer transition-all ${
|
||||||
|
resendTimer > 0 ? 'text-neutral-500 cursor-not-allowed' : 'text-purple-400 hover:text-purple-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
<span>{resendTimer > 0 ? `Reenviar en ${resendTimer}s` : 'Reenviar código'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Register form */}
|
||||||
|
{mode === 'register' && (
|
||||||
<form onSubmit={handleRegisterSubmit} className="space-y-3 text-xs">
|
<form onSubmit={handleRegisterSubmit} className="space-y-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Nombre Completo *</label>
|
<label className="block text-neutral-400 font-bold mb-1">Nombre Completo *</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="ej: Andrés Morales"
|
placeholder="ej: Carlos Lasso"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Correo Electrónico *</label>
|
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="andres@gmail.com"
|
placeholder="carlos@gmail.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Celular *</label>
|
<label className="block text-neutral-400 font-bold mb-1">WhatsApp / Celular *</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
required
|
required
|
||||||
placeholder="3112345678"
|
placeholder="3167455877"
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Ciudad</label>
|
<label className="block text-neutral-400 font-bold mb-1">Ciudad</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={city}
|
value={city}
|
||||||
onChange={(e) => setCity(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Dirección Entrega</label>
|
<label className="block text-neutral-400 font-bold mb-1">Dirección de Entrega</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Calle 5 # 34-12"
|
placeholder="Calle 5 # 34-12"
|
||||||
value={address}
|
value={address}
|
||||||
onChange={(e) => setAddress(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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`}
|
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`}
|
||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
<span>{loading ? 'Creando cuenta...' : 'Crear mi Cuenta Gratis'}</span>
|
<span>{loading ? 'Creando cuenta...' : 'Crear mi Cuenta VIP'}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ export interface User {
|
|||||||
city?: string;
|
city?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
role: 'admin' | 'operator' | 'customer';
|
role: 'admin' | 'operator' | 'customer';
|
||||||
|
password?: string;
|
||||||
|
twoFactorEnabled?: boolean;
|
||||||
|
twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both';
|
||||||
|
biometricEnabled?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
totalOrders?: number;
|
totalOrders?: number;
|
||||||
|
|||||||
70
dripzone-app/src/app/api/auth/change-password/route.ts
Normal file
70
dripzone-app/src/app/api/auth/change-password/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
dripzone-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
41
dripzone-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,29 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
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 { useAuth } from '@/context/AuthContext';
|
||||||
import { useBrand } from '@/context/BrandContext';
|
import { useBrand } from '@/context/BrandContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -10,6 +32,7 @@ export const AuthModal: React.FC = () => {
|
|||||||
const { user, sendOtp, verifyOtp, register, logout, isAuthModalOpen, closeAuthModal } = useAuth();
|
const { user, sendOtp, verifyOtp, register, logout, isAuthModalOpen, closeAuthModal } = useAuth();
|
||||||
const { currentBrand } = useBrand();
|
const { currentBrand } = useBrand();
|
||||||
|
|
||||||
|
// Mode when not logged in: 'login' | 'register'
|
||||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||||
const [step, setStep] = useState<'request' | 'verify'>('request');
|
const [step, setStep] = useState<'request' | 'verify'>('request');
|
||||||
const [identifier, setIdentifier] = useState('');
|
const [identifier, setIdentifier] = useState('');
|
||||||
@ -25,8 +48,29 @@ export const AuthModal: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [resendTimer, setResendTimer] = useState(0);
|
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<string | null>(null);
|
||||||
|
const [pwdError, setPwdError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 2FA state
|
||||||
|
const [is2FaEnabled, setIs2FaEnabled] = useState(false);
|
||||||
|
const [twoFactorMsg, setTwoFactorMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setIs2FaEnabled(user.twoFactorEnabled !== false);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
// Contador de reenvío
|
// Contador de reenvío
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: any = null;
|
let interval: any = null;
|
||||||
@ -70,12 +114,10 @@ export const AuthModal: React.FC = () => {
|
|||||||
newDigits[index] = cleanVal;
|
newDigits[index] = cleanVal;
|
||||||
setOtpDigits(newDigits);
|
setOtpDigits(newDigits);
|
||||||
|
|
||||||
// Auto-focus next input
|
|
||||||
if (cleanVal && index < 5) {
|
if (cleanVal && index < 5) {
|
||||||
inputRefs.current[index + 1]?.focus();
|
inputRefs.current[index + 1]?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-submit when all 6 digits are entered
|
|
||||||
if (cleanVal && index === 5) {
|
if (cleanVal && index === 5) {
|
||||||
const fullCode = newDigits.join('');
|
const fullCode = newDigits.join('');
|
||||||
if (fullCode.length === 6) {
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
<div className="fixed inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
||||||
<div className="bg-neutral-950 border border-purple-500/30 rounded-3xl max-w-md w-full p-6 sm:p-7 space-y-4 shadow-[0_20px_50px_rgba(168,85,247,0.25)] relative">
|
<div className="bg-neutral-950 border border-purple-500/30 rounded-3xl max-w-md w-full p-6 sm:p-7 space-y-4 shadow-[0_20px_50px_rgba(168,85,247,0.25)] relative max-h-[90vh] overflow-y-auto">
|
||||||
<button
|
<button
|
||||||
onClick={closeAuthModal}
|
onClick={closeAuthModal}
|
||||||
className="absolute top-4 right-4 text-neutral-400 hover:text-white p-1 rounded-full cursor-pointer transition-all"
|
className="absolute top-4 right-4 text-neutral-400 hover:text-white p-1 rounded-full cursor-pointer transition-all"
|
||||||
@ -132,17 +246,19 @@ export const AuthModal: React.FC = () => {
|
|||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* If user is already logged in: View Profile */}
|
{/* LOGGED-IN USER: PROFILE & SECURITY SUITE */}
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className={`w-16 h-16 rounded-3xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-2xl mx-auto shadow-lg shadow-purple-600/30`}>
|
{/* Top Avatar & User Name */}
|
||||||
|
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-xl mx-auto shadow-lg shadow-purple-600/30`}>
|
||||||
{user.name.charAt(0).toUpperCase()}
|
{user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-lg">{user.name}</h3>
|
<h3 className="font-black text-white text-base leading-tight">{user.name}</h3>
|
||||||
<p className="text-xs text-neutral-400">{user.email}</p>
|
<p className="text-[11px] text-neutral-400">{user.email}</p>
|
||||||
<div className="inline-block mt-1.5">
|
<div className="inline-block mt-1">
|
||||||
<span className={`text-[10px] font-bold px-3 py-1 rounded-full border ${
|
<span className={`text-[10px] font-bold px-3 py-0.5 rounded-full border ${
|
||||||
user.role === 'admin'
|
user.role === 'admin'
|
||||||
? 'bg-purple-500/20 text-purple-300 border-purple-500/40 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
|
? 'bg-purple-500/20 text-purple-300 border-purple-500/40 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
|
||||||
: user.role === 'operator'
|
: user.role === 'operator'
|
||||||
@ -154,33 +270,234 @@ export const AuthModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-3.5 text-left text-xs space-y-1.5 text-neutral-300">
|
{/* Profile Navigation Tabs */}
|
||||||
<p><strong>📱 WhatsApp / Celular:</strong> {user.phone}</p>
|
<div className="grid grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1 text-[11px] font-bold">
|
||||||
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
<button
|
||||||
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
onClick={() => setProfileTab('profile')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'profile'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
👤 Perfil
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('security')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'security'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🔒 Contraseña
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('2fa')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === '2fa'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🛡️ 2FA & Huella
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.role === 'admin' || user.role === 'operator' ? (
|
{/* TAB 1: User Profile Info */}
|
||||||
<Link
|
{profileTab === 'profile' && (
|
||||||
href="/admin"
|
<div className="space-y-3 animate-in fade-in">
|
||||||
onClick={closeAuthModal}
|
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-3.5 text-left text-xs space-y-1.5 text-neutral-300">
|
||||||
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3.5 rounded-2xl flex items-center justify-center gap-2 shadow-lg shadow-purple-600/30 transition-all hover:opacity-95`}
|
<p><strong>📱 WhatsApp / Celular:</strong> {user.phone}</p>
|
||||||
>
|
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
||||||
<Shield className="w-4 h-4" />
|
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
||||||
<span>Ir al Panel de Control Admin</span>
|
</div>
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<button
|
{user.role === 'admin' || user.role === 'operator' ? (
|
||||||
onClick={logout}
|
<Link
|
||||||
className="w-full bg-neutral-900 hover:bg-red-950/40 hover:border-red-500/50 border border-neutral-800 text-neutral-400 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
href="/admin"
|
||||||
>
|
onClick={closeAuthModal}
|
||||||
<LogOut className="w-4 h-4" />
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3.5 rounded-2xl flex items-center justify-center gap-2 shadow-lg shadow-purple-600/30 transition-all hover:opacity-95`}
|
||||||
<span>Cerrar Sesión</span>
|
>
|
||||||
</button>
|
<Shield className="w-4 h-4" />
|
||||||
|
<span>Ir al Panel de Control Admin</span>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="w-full bg-neutral-900 hover:bg-red-950/40 hover:border-red-500/50 border border-neutral-800 text-neutral-400 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>Cerrar Sesión</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 2: Change Password Form */}
|
||||||
|
{profileTab === 'security' && (
|
||||||
|
<form onSubmit={handleChangePassword} className="space-y-3 text-left text-xs animate-in fade-in">
|
||||||
|
<div className="bg-purple-950/30 border border-purple-500/30 p-3 rounded-2xl text-[11px] text-purple-300">
|
||||||
|
<p className="font-bold flex items-center gap-1">
|
||||||
|
<Lock className="w-3.5 h-3.5 text-purple-400" />
|
||||||
|
<span>Cambio de Contraseña Seguro</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-neutral-400 mt-0.5">
|
||||||
|
Puedes usar tu clave actual o el PIN Maestro de Emergencia (<strong>1978</strong>).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pwdMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{pwdMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pwdError && (
|
||||||
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||||
|
<span>{pwdError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Contraseña Actual *</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1 flex items-center justify-between">
|
||||||
|
<span>Nueva Contraseña *</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="text-purple-400 hover:text-purple-300 text-[10px] flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
|
||||||
|
<span>{showPassword ? 'Ocultar' : 'Mostrar'}</span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Mínimo 4 caracteres"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Confirmar Nueva Contraseña *</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Repite la nueva contraseña"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-3 rounded-xl shadow-md flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all mt-2"
|
||||||
|
>
|
||||||
|
<KeyRound className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Actualizando...' : 'Guardar Nueva Contraseña'}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 3: 2FA & Biometrics Settings */}
|
||||||
|
{profileTab === '2fa' && (
|
||||||
|
<div className="space-y-4 text-left text-xs animate-in fade-in">
|
||||||
|
{twoFactorMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{twoFactorMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2FA Toggle Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-purple-500/20 border border-purple-500/30 flex items-center justify-center text-purple-400">
|
||||||
|
<Smartphone className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">2FA con WhatsApp OTP</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Código de 6 dígitos a tu celular</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleToggle2Fa}
|
||||||
|
className={`w-12 h-6 rounded-full transition-colors relative cursor-pointer ${
|
||||||
|
is2FaEnabled ? 'bg-purple-600' : 'bg-neutral-800 border border-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full bg-white transition-transform absolute top-1 left-1 ${
|
||||||
|
is2FaEnabled ? 'translate-x-6' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400 leading-relaxed">
|
||||||
|
{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.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Biometrics Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center text-emerald-400">
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">Acceso Biométrico / Passkey</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Huella dactilar o Face ID nativo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400">
|
||||||
|
Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setTwoFactorMsg('👆 Tu dispositivo es 100% compatible con Passkeys y Biometría WebAuthn.');
|
||||||
|
}}
|
||||||
|
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-500/40 text-emerald-300 font-bold py-2 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all text-xs"
|
||||||
|
>
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
<span>Verificar Sensor Biométrico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Login with 2FA / OTP or Register */
|
/* LOGIN WITH 2FA / OTP OR REGISTER */
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className={`w-10 h-10 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md shadow-purple-600/30`}>
|
<div className={`w-10 h-10 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md shadow-purple-600/30`}>
|
||||||
@ -248,7 +565,7 @@ export const AuthModal: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="Ej: 3167455877 o carlos@dripzone.co"
|
placeholder="Ej: 3167455877 o carlos.andres.lasso@gmail.com"
|
||||||
value={identifier}
|
value={identifier}
|
||||||
onChange={(e) => setIdentifier(e.target.value)}
|
onChange={(e) => 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"
|
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
|
Ingresa el Código de 6 Dígitos
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* 6 Digit Input Boxes */}
|
|
||||||
<div className="flex justify-center gap-2 sm:gap-2.5">
|
<div className="flex justify-center gap-2 sm:gap-2.5">
|
||||||
{otpDigits.map((digit, idx) => (
|
{otpDigits.map((digit, idx) => (
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export interface User {
|
|||||||
address?: string;
|
address?: string;
|
||||||
role: 'admin' | 'operator' | 'customer';
|
role: 'admin' | 'operator' | 'customer';
|
||||||
password?: string;
|
password?: string;
|
||||||
|
twoFactorEnabled?: boolean;
|
||||||
|
twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both';
|
||||||
biometricEnabled?: boolean;
|
biometricEnabled?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
57
scripts/send_apk_to_whatsapp.js
Normal file
57
scripts/send_apk_to_whatsapp.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
|
||||||
|
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
|
||||||
|
const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
|
||||||
|
const TARGET_PHONE = '573167455877';
|
||||||
|
|
||||||
|
const apkPath = path.resolve(__dirname, '../dripzone-app/android/app/build/outputs/apk/debug/app-debug.apk');
|
||||||
|
|
||||||
|
async function sendApkToCarlos() {
|
||||||
|
if (!fs.existsSync(apkPath)) {
|
||||||
|
console.error('❌ El archivo APK no existe en:', apkPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(apkPath);
|
||||||
|
const sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
||||||
|
console.log(`📦 Leyendo APK oficial (${sizeMB} MB)...`);
|
||||||
|
|
||||||
|
const apkBuffer = fs.readFileSync(apkPath);
|
||||||
|
const base64Data = apkBuffer.toString('base64');
|
||||||
|
|
||||||
|
const caption = `🚀 *DripZone Sneakers - APK Actualizado*\n\n📅 *Generado:* ${new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' })}\n📦 *Tamaño:* ${sizeMB} MB\n\n🔐 *Novedades de la versión:*\n✅ Cambio de contraseña en perfil\n✅ Autenticación de Dos Factores (2FA) por WhatsApp OTP\n✅ Acceso con Huella / Passkey biométrico\n✅ Despacho logístico con integración a Waze\n✅ SplashScreen vertical limpio en alta definición\n\n📲 *Instalación:* Toca este archivo para actualizar tu app.`;
|
||||||
|
|
||||||
|
console.log(`📤 Enviando APK vía Evolution API a WhatsApp (+${TARGET_PHONE})...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${EVOLUTION_API_URL}/message/sendMedia/${EVOLUTION_INSTANCE_NAME}`,
|
||||||
|
{
|
||||||
|
number: TARGET_PHONE,
|
||||||
|
mediatype: 'document',
|
||||||
|
mimetype: 'application/vnd.android.package-archive',
|
||||||
|
fileName: 'DripZone_Actualizado.apk',
|
||||||
|
caption,
|
||||||
|
media: base64Data,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
apikey: EVOLUTION_API_KEY,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
maxContentLength: Infinity,
|
||||||
|
maxBodyLength: Infinity,
|
||||||
|
timeout: 180000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('🎉 ¡APK enviado con éxito a tu WhatsApp!', response.data?.key?.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error enviando APK:', error?.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendApkToCarlos();
|
||||||
70
sneaky-app/src/app/api/auth/change-password/route.ts
Normal file
70
sneaky-app/src/app/api/auth/change-password/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
sneaky-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
41
sneaky-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +1,158 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut } 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 { useAuth } from '@/context/AuthContext';
|
||||||
import { useBrand } from '@/context/BrandContext';
|
import { useBrand } from '@/context/BrandContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const AuthModal: React.FC = () => {
|
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();
|
const { currentBrand } = useBrand();
|
||||||
|
|
||||||
|
// Mode when not logged in: 'login' | 'register'
|
||||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||||
|
const [step, setStep] = useState<'request' | 'verify'>('request');
|
||||||
const [identifier, setIdentifier] = useState('');
|
const [identifier, setIdentifier] = useState('');
|
||||||
|
const [maskedPhone, setMaskedPhone] = useState('');
|
||||||
|
const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [city, setCity] = useState('Cali');
|
const [city, setCity] = useState('Cali');
|
||||||
const [address, setAddress] = useState('');
|
const [address, setAddress] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
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<string | null>(null);
|
||||||
|
const [pwdError, setPwdError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 2FA state
|
||||||
|
const [is2FaEnabled, setIs2FaEnabled] = useState(false);
|
||||||
|
const [twoFactorMsg, setTwoFactorMsg] = useState<string | null>(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;
|
if (!isAuthModalOpen) return null;
|
||||||
|
|
||||||
const handleLoginSubmit = async (e: React.FormEvent) => {
|
// Paso 1: Enviar OTP
|
||||||
|
const handleSendOtp = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!identifier.trim()) {
|
||||||
|
setError('Por favor ingresa tu número de celular o correo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await login(identifier);
|
|
||||||
|
const res = await sendOtp(identifier);
|
||||||
setLoading(false);
|
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<HTMLInputElement>) => {
|
||||||
|
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) => {
|
const handleRegisterSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
<div className="fixed inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
||||||
<div className="bg-zinc-900 border border-zinc-800 rounded-3xl max-w-md w-full p-6 space-y-4 shadow-2xl relative">
|
<div className="bg-neutral-950 border border-purple-500/30 rounded-3xl max-w-md w-full p-6 sm:p-7 space-y-4 shadow-[0_20px_50px_rgba(168,85,247,0.25)] relative max-h-[90vh] overflow-y-auto">
|
||||||
<button
|
<button
|
||||||
onClick={closeAuthModal}
|
onClick={closeAuthModal}
|
||||||
className="absolute top-4 right-4 text-gray-400 hover:text-white p-1 rounded-full cursor-pointer"
|
className="absolute top-4 right-4 text-neutral-400 hover:text-white p-1 rounded-full cursor-pointer transition-all"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* If user is already logged in: View Profile */}
|
{/* LOGGED-IN USER: PROFILE & SECURITY SUITE */}
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className={`w-16 h-16 rounded-3xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-2xl mx-auto shadow-lg`}>
|
{/* Top Avatar & User Name */}
|
||||||
|
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-xl mx-auto shadow-lg shadow-purple-600/30`}>
|
||||||
{user.name.charAt(0).toUpperCase()}
|
{user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-lg">{user.name}</h3>
|
<h3 className="font-black text-white text-base leading-tight">{user.name}</h3>
|
||||||
<p className="text-xs text-gray-400">{user.email}</p>
|
<p className="text-[11px] text-neutral-400">{user.email}</p>
|
||||||
<div className="inline-block mt-1">
|
<div className="inline-block mt-1">
|
||||||
<span className={`text-[10px] font-bold px-2.5 py-0.5 rounded-full border ${
|
<span className={`text-[10px] font-bold px-3 py-0.5 rounded-full border ${
|
||||||
user.role === 'admin'
|
user.role === 'admin'
|
||||||
? 'bg-purple-500/20 text-purple-300 border-purple-500/40'
|
? 'bg-purple-500/20 text-purple-300 border-purple-500/40 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
|
||||||
: user.role === 'operator'
|
: user.role === 'operator'
|
||||||
? 'bg-orange-500/20 text-orange-300 border-orange-500/40'
|
? 'bg-fuchsia-500/20 text-fuchsia-300 border-fuchsia-500/40'
|
||||||
: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
||||||
}`}>
|
}`}>
|
||||||
{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'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-zinc-950/80 border border-zinc-800 rounded-2xl p-3 text-left text-xs space-y-1.5 text-gray-300">
|
{/* Profile Navigation Tabs */}
|
||||||
<p><strong>📱 Celular:</strong> {user.phone}</p>
|
<div className="grid grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1 text-[11px] font-bold">
|
||||||
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
<button
|
||||||
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
onClick={() => setProfileTab('profile')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'profile'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
👤 Perfil
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('security')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'security'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🔒 Contraseña
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('2fa')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === '2fa'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🛡️ 2FA & Huella
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.role === 'admin' || user.role === 'operator' ? (
|
{/* TAB 1: User Profile Info */}
|
||||||
<Link
|
{profileTab === 'profile' && (
|
||||||
href="/admin"
|
<div className="space-y-3 animate-in fade-in">
|
||||||
onClick={closeAuthModal}
|
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-3.5 text-left text-xs space-y-1.5 text-neutral-300">
|
||||||
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md`}
|
<p><strong>📱 WhatsApp / Celular:</strong> {user.phone}</p>
|
||||||
>
|
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
||||||
<Shield className="w-4 h-4" />
|
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
||||||
<span>Ir al Panel de Control Admin</span>
|
</div>
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<button
|
{user.role === 'admin' || user.role === 'operator' ? (
|
||||||
onClick={logout}
|
<Link
|
||||||
className="w-full bg-zinc-800 hover:bg-red-950/40 hover:border-red-500/50 border border-transparent text-gray-300 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
href="/admin"
|
||||||
>
|
onClick={closeAuthModal}
|
||||||
<LogOut className="w-4 h-4" />
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3.5 rounded-2xl flex items-center justify-center gap-2 shadow-lg shadow-purple-600/30 transition-all hover:opacity-95`}
|
||||||
<span>Cerrar Sesión</span>
|
>
|
||||||
</button>
|
<Shield className="w-4 h-4" />
|
||||||
|
<span>Ir al Panel de Control Admin</span>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="w-full bg-neutral-900 hover:bg-red-950/40 hover:border-red-500/50 border border-neutral-800 text-neutral-400 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>Cerrar Sesión</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 2: Change Password Form */}
|
||||||
|
{profileTab === 'security' && (
|
||||||
|
<form onSubmit={handleChangePassword} className="space-y-3 text-left text-xs animate-in fade-in">
|
||||||
|
<div className="bg-purple-950/30 border border-purple-500/30 p-3 rounded-2xl text-[11px] text-purple-300">
|
||||||
|
<p className="font-bold flex items-center gap-1">
|
||||||
|
<Lock className="w-3.5 h-3.5 text-purple-400" />
|
||||||
|
<span>Cambio de Contraseña Seguro</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-neutral-400 mt-0.5">
|
||||||
|
Puedes usar tu clave actual o el PIN Maestro de Emergencia (<strong>1978</strong>).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pwdMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{pwdMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pwdError && (
|
||||||
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||||
|
<span>{pwdError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Contraseña Actual *</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1 flex items-center justify-between">
|
||||||
|
<span>Nueva Contraseña *</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="text-purple-400 hover:text-purple-300 text-[10px] flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
|
||||||
|
<span>{showPassword ? 'Ocultar' : 'Mostrar'}</span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Mínimo 4 caracteres"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Confirmar Nueva Contraseña *</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Repite la nueva contraseña"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-3 rounded-xl shadow-md flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all mt-2"
|
||||||
|
>
|
||||||
|
<KeyRound className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Actualizando...' : 'Guardar Nueva Contraseña'}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 3: 2FA & Biometrics Settings */}
|
||||||
|
{profileTab === '2fa' && (
|
||||||
|
<div className="space-y-4 text-left text-xs animate-in fade-in">
|
||||||
|
{twoFactorMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{twoFactorMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2FA Toggle Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-purple-500/20 border border-purple-500/30 flex items-center justify-center text-purple-400">
|
||||||
|
<Smartphone className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">2FA con WhatsApp OTP</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Código de 6 dígitos a tu celular</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleToggle2Fa}
|
||||||
|
className={`w-12 h-6 rounded-full transition-colors relative cursor-pointer ${
|
||||||
|
is2FaEnabled ? 'bg-purple-600' : 'bg-neutral-800 border border-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full bg-white transition-transform absolute top-1 left-1 ${
|
||||||
|
is2FaEnabled ? 'translate-x-6' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400 leading-relaxed">
|
||||||
|
{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.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Biometrics Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center text-emerald-400">
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">Acceso Biométrico / Passkey</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Huella dactilar o Face ID nativo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400">
|
||||||
|
Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setTwoFactorMsg('👆 Tu dispositivo es 100% compatible con Passkeys y Biometría WebAuthn.');
|
||||||
|
}}
|
||||||
|
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-500/40 text-emerald-300 font-bold py-2 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all text-xs"
|
||||||
|
>
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
<span>Verificar Sensor Biométrico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Login or Register */
|
/* LOGIN WITH 2FA / OTP OR REGISTER */
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3">
|
||||||
<div className={`w-9 h-9 rounded-xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md`}>
|
<div className={`w-10 h-10 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md shadow-purple-600/30`}>
|
||||||
{currentBrand.shortName[0]}
|
<Shield className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-base leading-tight">
|
<h3 className="font-black text-white text-base leading-tight">
|
||||||
{mode === 'login' ? 'Iniciar Sesión' : 'Crear Cuenta'}
|
{step === 'verify' ? 'Verificación de Seguridad' : mode === 'login' ? 'Acceso Seguro OTP' : 'Crear Cuenta'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-neutral-400">
|
||||||
{mode === 'login'
|
{step === 'verify'
|
||||||
? 'Accede para gestionar pedidos o entrar como admin'
|
? `Ingresa el código enviado a ${maskedPhone}`
|
||||||
: 'Regístrate para compras rápidas y seguimiento'}
|
: mode === 'login'
|
||||||
|
? 'Validación segura vía WhatsApp con código de 6 dígitos'
|
||||||
|
: 'Regístrate para compras y seguimiento de pedidos'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode Switch Tabs */}
|
{/* Mode Switch Tabs (Only in request step) */}
|
||||||
<div className="grid grid-cols-2 p-1 bg-zinc-950 border border-zinc-800 rounded-2xl">
|
{step === 'request' && (
|
||||||
<button
|
<div className="grid grid-cols-2 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setMode('login'); setError(null); }}
|
type="button"
|
||||||
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
onClick={() => { setMode('login'); setError(null); }}
|
||||||
mode === 'login' ? 'bg-white text-zinc-950 shadow' : 'text-gray-400 hover:text-white'
|
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
}`}
|
mode === 'login' ? 'bg-purple-600 text-white shadow-md' : 'text-neutral-400 hover:text-white'
|
||||||
>
|
}`}
|
||||||
Ingresar
|
>
|
||||||
</button>
|
Acceso con OTP
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setMode('register'); setError(null); }}
|
type="button"
|
||||||
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
onClick={() => { setMode('register'); setError(null); }}
|
||||||
mode === 'register' ? 'bg-white text-zinc-950 shadow' : 'text-gray-400 hover:text-white'
|
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
}`}
|
mode === 'register' ? 'bg-purple-600 text-white shadow-md' : 'text-neutral-400 hover:text-white'
|
||||||
>
|
}`}
|
||||||
Registrarme
|
>
|
||||||
</button>
|
Registrarme
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3.5 py-2.5 rounded-xl animate-in fade-in">
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3.5 py-2.5 rounded-xl animate-in fade-in">
|
||||||
@ -148,92 +547,177 @@ export const AuthModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === 'login' ? (
|
{successMsg && step === 'verify' && (
|
||||||
<form onSubmit={handleLoginSubmit} className="space-y-3 text-xs">
|
<div className="bg-emerald-950/80 border border-emerald-600 text-emerald-300 text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-2 animate-in fade-in">
|
||||||
|
<CheckCircle2 className="w-4 h-4 shrink-0 text-emerald-400" />
|
||||||
|
<span>{successMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 1: Request OTP */}
|
||||||
|
{step === 'request' && mode === 'login' && (
|
||||||
|
<form onSubmit={handleSendOtp} className="space-y-3.5 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-400 font-bold mb-1">Correo Electrónico o Celular *</label>
|
<label className="block text-neutral-300 font-bold mb-1.5 flex items-center justify-between">
|
||||||
<input
|
<span>Número de Celular (WhatsApp) o Correo *</span>
|
||||||
type="text"
|
</label>
|
||||||
required
|
<div className="relative">
|
||||||
placeholder="ej: carlos@sneakydrops.co o 3167455877"
|
<input
|
||||||
value={identifier}
|
type="text"
|
||||||
onChange={(e) => setIdentifier(e.target.value)}
|
required
|
||||||
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"
|
placeholder="Ej: 3167455877 o carlos.andres.lasso@gmail.com"
|
||||||
/>
|
value={identifier}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<Smartphone className="w-4 h-4 text-neutral-500 absolute right-3.5 top-3.5" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-neutral-500 mt-1">
|
||||||
|
Te enviaremos un código de seguridad de 6 dígitos a tu WhatsApp.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
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`}
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3.5 rounded-2xl shadow-lg shadow-purple-600/30 flex items-center justify-center gap-2 cursor-pointer active:scale-95 transition-all text-xs`}
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<Lock className="w-4 h-4" />
|
||||||
<span>{loading ? 'Accediendo...' : 'Iniciar Sesión'}</span>
|
<span>{loading ? 'Generando código...' : 'Enviar Código por WhatsApp'}</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p className="text-[11px] text-center text-gray-500">
|
<div className="bg-neutral-900/60 border border-neutral-800/80 rounded-2xl p-3 text-[11px] text-neutral-400 space-y-1">
|
||||||
Tip Admin: Puedes ingresar con <strong>carlos@sneakydrops.co</strong> o <strong>3167455877</strong>.
|
<div className="flex items-center gap-1.5 text-purple-400 font-bold">
|
||||||
</p>
|
<Shield className="w-3.5 h-3.5" />
|
||||||
|
<span>Autenticación 2FA Verificada</span>
|
||||||
|
</div>
|
||||||
|
<p>Acceso seguro para clientes y administradores con validación en tiempo real.</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{/* STEP 2: Verify 6-digit OTP */}
|
||||||
|
{step === 'verify' && mode === 'login' && (
|
||||||
|
<form onSubmit={handleVerifySubmit} className="space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-300 font-bold mb-2 text-center">
|
||||||
|
Ingresa el Código de 6 Dígitos
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-2 sm:gap-2.5">
|
||||||
|
{otpDigits.map((digit, idx) => (
|
||||||
|
<input
|
||||||
|
key={idx}
|
||||||
|
ref={(el) => { 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'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3.5 rounded-2xl shadow-lg shadow-purple-600/30 flex items-center justify-center gap-2 cursor-pointer active:scale-95 transition-all text-xs`}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Verificando...' : 'Verificar y Acceder'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setStep('request'); setError(null); setSuccessMsg(null); }}
|
||||||
|
className="text-neutral-400 hover:text-white cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
← Cambiar número
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={resendTimer > 0 || loading}
|
||||||
|
onClick={handleSendOtp}
|
||||||
|
className={`flex items-center gap-1 font-bold cursor-pointer transition-all ${
|
||||||
|
resendTimer > 0 ? 'text-neutral-500 cursor-not-allowed' : 'text-purple-400 hover:text-purple-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
<span>{resendTimer > 0 ? `Reenviar en ${resendTimer}s` : 'Reenviar código'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Register form */}
|
||||||
|
{mode === 'register' && (
|
||||||
<form onSubmit={handleRegisterSubmit} className="space-y-3 text-xs">
|
<form onSubmit={handleRegisterSubmit} className="space-y-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-400 font-bold mb-1">Nombre Completo *</label>
|
<label className="block text-neutral-400 font-bold mb-1">Nombre Completo *</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="ej: Andrés Morales"
|
placeholder="ej: Carlos Lasso"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-400 font-bold mb-1">Correo Electrónico *</label>
|
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="andres@gmail.com"
|
placeholder="carlos@gmail.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-400 font-bold mb-1">Celular *</label>
|
<label className="block text-neutral-400 font-bold mb-1">WhatsApp / Celular *</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
required
|
required
|
||||||
placeholder="3112345678"
|
placeholder="3167455877"
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-400 font-bold mb-1">Ciudad</label>
|
<label className="block text-neutral-400 font-bold mb-1">Ciudad</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={city}
|
value={city}
|
||||||
onChange={(e) => setCity(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-400 font-bold mb-1">Dirección Entrega</label>
|
<label className="block text-neutral-400 font-bold mb-1">Dirección de Entrega</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Calle 5 # 34-12"
|
placeholder="Calle 5 # 34-12"
|
||||||
value={address}
|
value={address}
|
||||||
onChange={(e) => setAddress(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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`}
|
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`}
|
||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
<span>{loading ? 'Creando cuenta...' : 'Crear mi Cuenta Gratis'}</span>
|
<span>{loading ? 'Creando cuenta...' : 'Crear mi Cuenta VIP'}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ export interface User {
|
|||||||
city?: string;
|
city?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
role: 'admin' | 'operator' | 'customer';
|
role: 'admin' | 'operator' | 'customer';
|
||||||
|
password?: string;
|
||||||
|
twoFactorEnabled?: boolean;
|
||||||
|
twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both';
|
||||||
|
biometricEnabled?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
totalOrders?: number;
|
totalOrders?: number;
|
||||||
|
|||||||
70
zapply-app/src/app/api/auth/change-password/route.ts
Normal file
70
zapply-app/src/app/api/auth/change-password/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
zapply-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
41
zapply-app/src/app/api/auth/toggle-2fa/route.ts
Normal file
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +1,158 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { X, User, Lock, Mail, Phone, MapPin, Sparkles, LogIn, UserPlus, Shield, LogOut } 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 { useAuth } from '@/context/AuthContext';
|
||||||
import { useBrand } from '@/context/BrandContext';
|
import { useBrand } from '@/context/BrandContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const AuthModal: React.FC = () => {
|
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();
|
const { currentBrand } = useBrand();
|
||||||
|
|
||||||
|
// Mode when not logged in: 'login' | 'register'
|
||||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||||
|
const [step, setStep] = useState<'request' | 'verify'>('request');
|
||||||
const [identifier, setIdentifier] = useState('');
|
const [identifier, setIdentifier] = useState('');
|
||||||
|
const [maskedPhone, setMaskedPhone] = useState('');
|
||||||
|
const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [city, setCity] = useState('Cali');
|
const [city, setCity] = useState('Cali');
|
||||||
const [address, setAddress] = useState('');
|
const [address, setAddress] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
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<string | null>(null);
|
||||||
|
const [pwdError, setPwdError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 2FA state
|
||||||
|
const [is2FaEnabled, setIs2FaEnabled] = useState(false);
|
||||||
|
const [twoFactorMsg, setTwoFactorMsg] = useState<string | null>(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;
|
if (!isAuthModalOpen) return null;
|
||||||
|
|
||||||
const handleLoginSubmit = async (e: React.FormEvent) => {
|
// Paso 1: Enviar OTP
|
||||||
|
const handleSendOtp = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!identifier.trim()) {
|
||||||
|
setError('Por favor ingresa tu número de celular o correo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await login(identifier);
|
|
||||||
|
const res = await sendOtp(identifier);
|
||||||
setLoading(false);
|
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<HTMLInputElement>) => {
|
||||||
|
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) => {
|
const handleRegisterSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
<div className="fixed inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in">
|
||||||
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-md w-full p-6 space-y-4 shadow-2xl relative">
|
<div className="bg-neutral-950 border border-purple-500/30 rounded-3xl max-w-md w-full p-6 sm:p-7 space-y-4 shadow-[0_20px_50px_rgba(168,85,247,0.25)] relative max-h-[90vh] overflow-y-auto">
|
||||||
<button
|
<button
|
||||||
onClick={closeAuthModal}
|
onClick={closeAuthModal}
|
||||||
className="absolute top-4 right-4 text-slate-400 hover:text-white p-1 rounded-full cursor-pointer"
|
className="absolute top-4 right-4 text-neutral-400 hover:text-white p-1 rounded-full cursor-pointer transition-all"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* If user is already logged in: View Profile */}
|
{/* LOGGED-IN USER: PROFILE & SECURITY SUITE */}
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className={`w-16 h-16 rounded-3xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-2xl mx-auto shadow-lg`}>
|
{/* Top Avatar & User Name */}
|
||||||
|
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black text-xl mx-auto shadow-lg shadow-purple-600/30`}>
|
||||||
{user.name.charAt(0).toUpperCase()}
|
{user.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-lg">{user.name}</h3>
|
<h3 className="font-black text-white text-base leading-tight">{user.name}</h3>
|
||||||
<p className="text-xs text-slate-400">{user.email}</p>
|
<p className="text-[11px] text-neutral-400">{user.email}</p>
|
||||||
<div className="inline-block mt-1">
|
<div className="inline-block mt-1">
|
||||||
<span className={`text-[10px] font-bold px-2.5 py-0.5 rounded-full border ${
|
<span className={`text-[10px] font-bold px-3 py-0.5 rounded-full border ${
|
||||||
user.role === 'admin'
|
user.role === 'admin'
|
||||||
? 'bg-purple-500/20 text-purple-300 border-purple-500/40'
|
? 'bg-purple-500/20 text-purple-300 border-purple-500/40 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
|
||||||
: user.role === 'operator'
|
: user.role === 'operator'
|
||||||
? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
? 'bg-fuchsia-500/20 text-fuchsia-300 border-fuchsia-500/40'
|
||||||
: 'bg-teal-500/20 text-teal-300 border-teal-500/40'
|
: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
||||||
}`}>
|
}`}>
|
||||||
{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'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-950/80 border border-slate-800 rounded-2xl p-3 text-left text-xs space-y-1.5 text-slate-300">
|
{/* Profile Navigation Tabs */}
|
||||||
<p><strong>📱 Celular:</strong> {user.phone}</p>
|
<div className="grid grid-cols-3 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl gap-1 text-[11px] font-bold">
|
||||||
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
<button
|
||||||
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
onClick={() => setProfileTab('profile')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'profile'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
👤 Perfil
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('security')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === 'security'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🔒 Contraseña
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setProfileTab('2fa')}
|
||||||
|
className={`py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
|
profileTab === '2fa'
|
||||||
|
? 'bg-purple-600 text-white shadow'
|
||||||
|
: 'text-neutral-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🛡️ 2FA & Huella
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.role === 'admin' || user.role === 'operator' ? (
|
{/* TAB 1: User Profile Info */}
|
||||||
<Link
|
{profileTab === 'profile' && (
|
||||||
href="/admin"
|
<div className="space-y-3 animate-in fade-in">
|
||||||
onClick={closeAuthModal}
|
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-3.5 text-left text-xs space-y-1.5 text-neutral-300">
|
||||||
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md`}
|
<p><strong>📱 WhatsApp / Celular:</strong> {user.phone}</p>
|
||||||
>
|
<p><strong>📍 Ciudad:</strong> {user.city || 'Cali'}</p>
|
||||||
<Shield className="w-4 h-4" />
|
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
|
||||||
<span>Ir al Panel de Control Admin</span>
|
</div>
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<button
|
{user.role === 'admin' || user.role === 'operator' ? (
|
||||||
onClick={logout}
|
<Link
|
||||||
className="w-full bg-slate-800 hover:bg-red-950/40 hover:border-red-500/50 border border-transparent text-slate-300 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
href="/admin"
|
||||||
>
|
onClick={closeAuthModal}
|
||||||
<LogOut className="w-4 h-4" />
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-xs py-3.5 rounded-2xl flex items-center justify-center gap-2 shadow-lg shadow-purple-600/30 transition-all hover:opacity-95`}
|
||||||
<span>Cerrar Sesión</span>
|
>
|
||||||
</button>
|
<Shield className="w-4 h-4" />
|
||||||
|
<span>Ir al Panel de Control Admin</span>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="w-full bg-neutral-900 hover:bg-red-950/40 hover:border-red-500/50 border border-neutral-800 text-neutral-400 hover:text-red-300 font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>Cerrar Sesión</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 2: Change Password Form */}
|
||||||
|
{profileTab === 'security' && (
|
||||||
|
<form onSubmit={handleChangePassword} className="space-y-3 text-left text-xs animate-in fade-in">
|
||||||
|
<div className="bg-purple-950/30 border border-purple-500/30 p-3 rounded-2xl text-[11px] text-purple-300">
|
||||||
|
<p className="font-bold flex items-center gap-1">
|
||||||
|
<Lock className="w-3.5 h-3.5 text-purple-400" />
|
||||||
|
<span>Cambio de Contraseña Seguro</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-neutral-400 mt-0.5">
|
||||||
|
Puedes usar tu clave actual o el PIN Maestro de Emergencia (<strong>1978</strong>).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pwdMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{pwdMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pwdError && (
|
||||||
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||||
|
<span>{pwdError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Contraseña Actual *</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1 flex items-center justify-between">
|
||||||
|
<span>Nueva Contraseña *</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="text-purple-400 hover:text-purple-300 text-[10px] flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
|
||||||
|
<span>{showPassword ? 'Ocultar' : 'Mostrar'}</span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Mínimo 4 caracteres"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-400 font-bold mb-1">Confirmar Nueva Contraseña *</label>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
placeholder="Repite la nueva contraseña"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-bold py-3 rounded-xl shadow-md flex items-center justify-center gap-1.5 cursor-pointer active:scale-95 transition-all mt-2"
|
||||||
|
>
|
||||||
|
<KeyRound className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Actualizando...' : 'Guardar Nueva Contraseña'}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAB 3: 2FA & Biometrics Settings */}
|
||||||
|
{profileTab === '2fa' && (
|
||||||
|
<div className="space-y-4 text-left text-xs animate-in fade-in">
|
||||||
|
{twoFactorMsg && (
|
||||||
|
<div className="bg-emerald-950/80 border border-emerald-500 text-emerald-300 text-xs px-3 py-2 rounded-xl flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||||
|
<span>{twoFactorMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 2FA Toggle Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-purple-500/20 border border-purple-500/30 flex items-center justify-center text-purple-400">
|
||||||
|
<Smartphone className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">2FA con WhatsApp OTP</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Código de 6 dígitos a tu celular</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleToggle2Fa}
|
||||||
|
className={`w-12 h-6 rounded-full transition-colors relative cursor-pointer ${
|
||||||
|
is2FaEnabled ? 'bg-purple-600' : 'bg-neutral-800 border border-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full bg-white transition-transform absolute top-1 left-1 ${
|
||||||
|
is2FaEnabled ? 'translate-x-6' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400 leading-relaxed">
|
||||||
|
{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.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Biometrics Card */}
|
||||||
|
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 space-y-3">
|
||||||
|
<div className="flex items-center space-x-2.5">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center text-emerald-400">
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-white text-xs">Acceso Biométrico / Passkey</p>
|
||||||
|
<span className="text-[10px] text-neutral-400">Huella dactilar o Face ID nativo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-400">
|
||||||
|
Inicia sesión en 1 segundo usando el sensor de huella de tu teléfono o cámara de tu portátil.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setTwoFactorMsg('👆 Tu dispositivo es 100% compatible con Passkeys y Biometría WebAuthn.');
|
||||||
|
}}
|
||||||
|
className="w-full bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-500/40 text-emerald-300 font-bold py-2 rounded-xl flex items-center justify-center gap-1.5 cursor-pointer transition-all text-xs"
|
||||||
|
>
|
||||||
|
<Fingerprint className="w-4 h-4" />
|
||||||
|
<span>Verificar Sensor Biométrico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Login or Register */
|
/* LOGIN WITH 2FA / OTP OR REGISTER */
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3">
|
||||||
<div className={`w-9 h-9 rounded-xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md`}>
|
<div className={`w-10 h-10 rounded-2xl bg-gradient-to-br ${currentBrand.colors.primaryGradient} flex items-center justify-center text-white font-black shadow-md shadow-purple-600/30`}>
|
||||||
{currentBrand.shortName[0]}
|
<Shield className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-black text-white text-base leading-tight">
|
<h3 className="font-black text-white text-base leading-tight">
|
||||||
{mode === 'login' ? 'Iniciar Sesión' : 'Crear Cuenta'}
|
{step === 'verify' ? 'Verificación de Seguridad' : mode === 'login' ? 'Acceso Seguro OTP' : 'Crear Cuenta'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-neutral-400">
|
||||||
{mode === 'login'
|
{step === 'verify'
|
||||||
? 'Accede para gestionar pedidos o entrar como admin'
|
? `Ingresa el código enviado a ${maskedPhone}`
|
||||||
: 'Regístrate para compras rápidas y seguimiento'}
|
: mode === 'login'
|
||||||
|
? 'Validación segura vía WhatsApp con código de 6 dígitos'
|
||||||
|
: 'Regístrate para compras y seguimiento de pedidos'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode Switch Tabs */}
|
{/* Mode Switch Tabs (Only in request step) */}
|
||||||
<div className="grid grid-cols-2 p-1 bg-slate-950 border border-slate-800 rounded-2xl">
|
{step === 'request' && (
|
||||||
<button
|
<div className="grid grid-cols-2 p-1 bg-neutral-900 border border-neutral-800 rounded-2xl">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setMode('login'); setError(null); }}
|
type="button"
|
||||||
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
onClick={() => { setMode('login'); setError(null); }}
|
||||||
mode === 'login' ? 'bg-white text-slate-950 shadow' : 'text-slate-400 hover:text-white'
|
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
}`}
|
mode === 'login' ? 'bg-purple-600 text-white shadow-md' : 'text-neutral-400 hover:text-white'
|
||||||
>
|
}`}
|
||||||
Ingresar
|
>
|
||||||
</button>
|
Acceso con OTP
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setMode('register'); setError(null); }}
|
type="button"
|
||||||
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
onClick={() => { setMode('register'); setError(null); }}
|
||||||
mode === 'register' ? 'bg-white text-slate-950 shadow' : 'text-slate-400 hover:text-white'
|
className={`text-xs font-bold py-2 rounded-xl transition-all cursor-pointer ${
|
||||||
}`}
|
mode === 'register' ? 'bg-purple-600 text-white shadow-md' : 'text-neutral-400 hover:text-white'
|
||||||
>
|
}`}
|
||||||
Registrarme
|
>
|
||||||
</button>
|
Registrarme
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3.5 py-2.5 rounded-xl animate-in fade-in">
|
<div className="bg-red-950/80 border border-red-500 text-red-300 text-xs px-3.5 py-2.5 rounded-xl animate-in fade-in">
|
||||||
@ -148,91 +547,177 @@ export const AuthModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === 'login' ? (
|
{successMsg && step === 'verify' && (
|
||||||
<form onSubmit={handleLoginSubmit} className="space-y-3 text-xs">
|
<div className="bg-emerald-950/80 border border-emerald-600 text-emerald-300 text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-2 animate-in fade-in">
|
||||||
|
<CheckCircle2 className="w-4 h-4 shrink-0 text-emerald-400" />
|
||||||
|
<span>{successMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 1: Request OTP */}
|
||||||
|
{step === 'request' && mode === 'login' && (
|
||||||
|
<form onSubmit={handleSendOtp} className="space-y-3.5 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Correo Electrónico o Celular *</label>
|
<label className="block text-neutral-300 font-bold mb-1.5 flex items-center justify-between">
|
||||||
<input
|
<span>Número de Celular (WhatsApp) o Correo *</span>
|
||||||
type="text"
|
</label>
|
||||||
required
|
<div className="relative">
|
||||||
placeholder="ej: carlos@zapply.co o 3167455877"
|
<input
|
||||||
value={identifier}
|
type="text"
|
||||||
onChange={(e) => setIdentifier(e.target.value)}
|
required
|
||||||
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"
|
placeholder="Ej: 3167455877 o carlos.andres.lasso@gmail.com"
|
||||||
/>
|
value={identifier}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<Smartphone className="w-4 h-4 text-neutral-500 absolute right-3.5 top-3.5" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-neutral-500 mt-1">
|
||||||
|
Te enviaremos un código de seguridad de 6 dígitos a tu WhatsApp.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
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`}
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3.5 rounded-2xl shadow-lg shadow-purple-600/30 flex items-center justify-center gap-2 cursor-pointer active:scale-95 transition-all text-xs`}
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<Lock className="w-4 h-4" />
|
||||||
<span>{loading ? 'Accediendo...' : 'Iniciar Sesión'}</span>
|
<span>{loading ? 'Generando código...' : 'Enviar Código por WhatsApp'}</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p className="text-[11px] text-center text-gray-500">
|
<div className="bg-neutral-900/60 border border-neutral-800/80 rounded-2xl p-3 text-[11px] text-neutral-400 space-y-1">
|
||||||
Tip Admin: Puedes ingresar con <strong>carlos@zapply.co</strong> o <strong>3167455877</strong>.
|
<div className="flex items-center gap-1.5 text-purple-400 font-bold">
|
||||||
</p>
|
<Shield className="w-3.5 h-3.5" />
|
||||||
|
<span>Autenticación 2FA Verificada</span>
|
||||||
|
</div>
|
||||||
|
<p>Acceso seguro para clientes y administradores con validación en tiempo real.</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{/* STEP 2: Verify 6-digit OTP */}
|
||||||
|
{step === 'verify' && mode === 'login' && (
|
||||||
|
<form onSubmit={handleVerifySubmit} className="space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-neutral-300 font-bold mb-2 text-center">
|
||||||
|
Ingresa el Código de 6 Dígitos
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-2 sm:gap-2.5">
|
||||||
|
{otpDigits.map((digit, idx) => (
|
||||||
|
<input
|
||||||
|
key={idx}
|
||||||
|
ref={(el) => { 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'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black py-3.5 rounded-2xl shadow-lg shadow-purple-600/30 flex items-center justify-center gap-2 cursor-pointer active:scale-95 transition-all text-xs`}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
<span>{loading ? 'Verificando...' : 'Verificar y Acceder'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs pt-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setStep('request'); setError(null); setSuccessMsg(null); }}
|
||||||
|
className="text-neutral-400 hover:text-white cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
← Cambiar número
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={resendTimer > 0 || loading}
|
||||||
|
onClick={handleSendOtp}
|
||||||
|
className={`flex items-center gap-1 font-bold cursor-pointer transition-all ${
|
||||||
|
resendTimer > 0 ? 'text-neutral-500 cursor-not-allowed' : 'text-purple-400 hover:text-purple-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
<span>{resendTimer > 0 ? `Reenviar en ${resendTimer}s` : 'Reenviar código'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Register form */}
|
||||||
|
{mode === 'register' && (
|
||||||
<form onSubmit={handleRegisterSubmit} className="space-y-3 text-xs">
|
<form onSubmit={handleRegisterSubmit} className="space-y-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Nombre Completo *</label>
|
<label className="block text-neutral-400 font-bold mb-1">Nombre Completo *</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="ej: Andrés Morales"
|
placeholder="ej: Carlos Lasso"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Correo Electrónico *</label>
|
<label className="block text-neutral-400 font-bold mb-1">Correo Electrónico *</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
placeholder="andres@gmail.com"
|
placeholder="carlos@gmail.com"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Celular *</label>
|
<label className="block text-neutral-400 font-bold mb-1">WhatsApp / Celular *</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
required
|
required
|
||||||
placeholder="3112345678"
|
placeholder="3167455877"
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Ciudad</label>
|
<label className="block text-neutral-400 font-bold mb-1">Ciudad</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={city}
|
value={city}
|
||||||
onChange={(e) => setCity(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-slate-400 font-bold mb-1">Dirección Entrega</label>
|
<label className="block text-neutral-400 font-bold mb-1">Dirección de Entrega</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Calle 5 # 34-12"
|
placeholder="Calle 5 # 34-12"
|
||||||
value={address}
|
value={address}
|
||||||
onChange={(e) => setAddress(e.target.value)}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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`}
|
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`}
|
||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
<span>{loading ? 'Creando cuenta...' : 'Crear mi Cuenta Gratis'}</span>
|
<span>{loading ? 'Creando cuenta...' : 'Crear mi Cuenta VIP'}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ export interface User {
|
|||||||
city?: string;
|
city?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
role: 'admin' | 'operator' | 'customer';
|
role: 'admin' | 'operator' | 'customer';
|
||||||
|
password?: string;
|
||||||
|
twoFactorEnabled?: boolean;
|
||||||
|
twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both';
|
||||||
|
biometricEnabled?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
totalOrders?: number;
|
totalOrders?: number;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user