fix(auth): restaurar boton admin permanente en web y asegurar rol de Super Administrador para Carlos (3167455877)

This commit is contained in:
AI Bot 2026-09-07 11:53:40 -05:00
parent daa24a25fc
commit a2524211f2
9 changed files with 294 additions and 54 deletions

View File

@ -37,7 +37,19 @@ def deploy():
print(f"[2/4] Conectando a VPS {HOST} via SSH / SFTP...") print(f"[2/4] Conectando a VPS {HOST} via SSH / SFTP...")
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30) connected = False
for attempt in range(1, 4):
try:
print(f" Intento {attempt} de conexion a {HOST}...")
client.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=60, banner_timeout=60, auth_timeout=60)
connected = True
break
except Exception as conn_err:
print(f" Aviso: fallo en intento {attempt}: {conn_err}")
import time
time.sleep(2)
if not connected:
raise Exception(f"No se pudo conectar a {HOST} tras 3 intentos")
sftp = client.open_sftp() sftp = client.open_sftp()
print("[3/4] Subiendo archivo comprimido y APK al servidor...") print("[3/4] Subiendo archivo comprimido y APK al servidor...")
@ -115,20 +127,21 @@ def send_apk_to_whatsapp():
import urllib.request import urllib.request
import json import json
print("[5/5] Enviando APK y reporte de actualizacion al WhatsApp de Carlos (+57 316 745 5877)...") print("[5/5] Enviando APK y reporte de actualizacion al WhatsApp de Carlos (+57 316 745 5877)...")
url = "https://evolution.almeira.com.co/message/sendMedia/DripZone" url = "https://evolution.almeira.com.co/message/sendMedia/AutomataAlmeira"
headers = { headers = {
"apikey": "83B5BD230796-4324-AE73-3508F69E279D", "apikey": "EE49029EEB85-4568-8F81-C9FDFC6E2943",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
caption = ( caption = (
"📲 *¡DRIPZONE ACTUALIZADA EN PRODUCCIÓN!* 🚀👟\n" "👑 *¡ACCESO DE ADMINISTRADOR RESTAURADO & APK COMPILADO!* 🚀👟\n"
"━━━━━━━━━━━━━━━━━━━━━━\n" "━━━━━━━━━━━━━━━━━━━━━━\n"
"✨ *Novedades de esta versión:*\n" "✨ *Ajustes implementados y desplegados en Producción:*\n\n"
"• 📦 *Nueva Vista Dedicada 'Mis Pedidos'*: Interfaz completa y no modal en `/mis-pedidos` con pedidos agrupados por estado (Pendiente de anticipo, Anticipo aprobado, Despachados, Entregados, Cancelados), mostrando información clave (ID, fecha, total, anticipo Bold, contraentrega) y detalle desplegable con 1-clic.\n" "1️⃣ 🛡️ *Botón de Admin Permanente en la Web*: Restaurado de forma visible y accesible en la barra superior de navegación de https://drip-zone.online. Ahora está siempre disponible para ingresar y destacado con gradiente morado/fucsia cuando inicias sesión.\n"
"• 🛒 *Selección Limpia de Producto*: Eliminado botón y banners de pedir por WhatsApp en la vitrina de producto, canalizando la compra de forma directa al carrito y checkout seguro.\n" "2️⃣ 👑 *Rol de Super Administrador para Carlos*: Tu celular personal *3167455877* (+57 316 745 5877) quedó configurado como Administrador Maestro incondicional en el servicio de datos (`dataService`), sesión web (`AuthContext`), API de usuarios y panel de control (`/admin`), con badge '👑 Super Administrador'.\n"
"• 📱 *Navegación Móvil Mejorada*: Reemplazado botón duplicado de Asesor por acceso directo a 'Mis Pedidos' en el dock inferior y en el menú superior.\n" "3️⃣ 🚀 *Despliegue en Servidor VPS*: Contenedor `dripzone-production` reconstruido e iniciado con éxito en el servidor de producción.\n"
"• 💬 *Atención Comercial en WhatsApp & Pagos en Web/App*: Consultas por chat y procesamiento seguro de órdenes y pasarela Bold ($10k anticipo) en la app.\n\n" "4️⃣ 📲 *APK Android Actualizado*: Compilado y disponible para descarga directa.\n\n"
"🌐 *Descarga directa del APK:*\n👉 https://drip-zone.online/DripZone.apk" "👉 *Panel de Administración:* https://drip-zone.online/admin\n"
"📥 *Descarga directa del APK:* https://drip-zone.online/DripZone.apk"
) )
payload = { payload = {
"number": "573167455877", "number": "573167455877",
@ -144,11 +157,10 @@ def send_apk_to_whatsapp():
print(f"[OK] APK despachada con exito al WhatsApp de Carlos (HTTP {resp.status})") print(f"[OK] APK despachada con exito al WhatsApp de Carlos (HTTP {resp.status})")
except Exception as e: except Exception as e:
print(f"[Aviso] No se pudo enviar archivo multimedia por Evolution: {e}. Enviando mensaje de texto...") print(f"[Aviso] No se pudo enviar archivo multimedia por Evolution: {e}. Enviando mensaje de texto...")
text_url = "https://evolution.almeira.com.co/message/sendText/DripZone" text_url = "https://evolution.almeira.com.co/message/sendText/AutomataAlmeira"
text_payload = { text_payload = {
"number": "573167455877", "number": "573167455877",
"text": ( "text": (
"📲 *¡DRIPZONE APK ACTUALIZADA EN PRODUCCIÓN!* 🚀👟\n\n"
f"{caption}\n\n" f"{caption}\n\n"
"📥 *Descarga e instala el APK actualizado aquí:*\n" "📥 *Descarga e instala el APK actualizado aquí:*\n"
"👉 https://drip-zone.online/DripZone.apk" "👉 https://drip-zone.online/DripZone.apk"

View File

@ -27,8 +27,16 @@ import { AuthModal } from '@/components/auth/AuthModal';
export default function AdminLayout({ children }: { children: React.ReactNode }) { export default function AdminLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
const { user, openAuthModal } = useAuth(); const { user, isAdmin: authIsAdmin, openAuthModal } = useAuth();
const isAdmin = Boolean(user && (user.role === 'admin' || user.role === 'operator')); const isAdmin = Boolean(
authIsAdmin ||
(user && (
user.role === 'admin' ||
user.role === 'operator' ||
(user.phone && user.phone.replace(/\D/g, '').endsWith('3167455877')) ||
(user.email && (user.email.toLowerCase() === 'carlos.andres.lasso@gmail.com' || user.email.toLowerCase() === 'info@drip-zone.online'))
))
);
const NAV_ITEMS = [ const NAV_ITEMS = [
{ href: '/admin', label: 'Resumen & KPIs', icon: LayoutDashboard }, { href: '/admin', label: 'Resumen & KPIs', icon: LayoutDashboard },

View File

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service'; import { dataService, isCarlosAdmin } from '@/lib/data-service';
import { otpStore } from '@/lib/otp-store'; import { otpStore } from '@/lib/otp-store';
export async function POST(request: Request) { export async function POST(request: Request) {
@ -16,25 +16,30 @@ export async function POST(request: Request) {
return NextResponse.json({ success: false, error: verification.reason || 'Código inválido o expirado' }, { status: 401 }); return NextResponse.json({ success: false, error: verification.reason || 'Código inválido o expirado' }, { status: 401 });
} }
const isCarlos = isCarlosAdmin(cleanInput);
// Código válido -> Obtener o crear usuario registrado en la plataforma // Código válido -> Obtener o crear usuario registrado en la plataforma
let user = dataService.authenticate(cleanInput); let user = dataService.authenticate(cleanInput);
if (!user) { if (!user) {
// Si es un nuevo cliente autenticado por primera vez con su celular // Si es un nuevo cliente o administrador autenticado por primera vez con su celular
const cleanPhone = cleanInput.replace(/\D/g, ''); const cleanPhone = cleanInput.replace(/\D/g, '');
const assignedName = name?.trim() || `Cliente Verificado (${cleanPhone.slice(-4)})`; const assignedName = isCarlos
? (name?.trim() || 'Carlos Lasso')
: (name?.trim() || `Cliente Verificado (${cleanPhone.slice(-4)})`);
user = dataService.addUser({ user = dataService.addUser({
id: `usr-${Date.now()}`, id: isCarlos ? 'usr-admin-carlos' : `usr-${Date.now()}`,
name: assignedName, name: assignedName,
email: `${cleanPhone}@cliente.dripzone.co`, email: isCarlos ? 'carlos.andres.lasso@gmail.com' : `${cleanPhone}@cliente.dripzone.co`,
phone: cleanPhone, phone: cleanPhone,
city: city?.trim() || 'Cali', city: city?.trim() || 'Cali',
address: address?.trim() || '', address: address?.trim() || (isCarlos ? 'Sede Principal / Cali' : ''),
role: 'customer', role: isCarlos ? 'admin' : 'customer',
isActive: true, isActive: true,
phoneVerified: true, phoneVerified: true,
requiresShippingDeposit: false, requiresShippingDeposit: false,
depositReason: 'Número verificado vía WhatsApp OTP', depositReason: isCarlos ? 'Super Administrador' : 'Número verificado vía WhatsApp OTP',
rejectedOrdersCount: 0, rejectedOrdersCount: 0,
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
totalOrders: 0, totalOrders: 0,
@ -44,12 +49,15 @@ export async function POST(request: Request) {
// Usuario existente: si verificó su teléfono, actualizar estado y datos si se suministraron // Usuario existente: si verificó su teléfono, actualizar estado y datos si se suministraron
const hadRejections = (user.rejectedOrdersCount || 0) > 0; const hadRejections = (user.rejectedOrdersCount || 0) > 0;
dataService.updateUser(user.id, { dataService.updateUser(user.id, {
name: name?.trim() || user.name, name: isCarlos ? (name?.trim() || user.name || 'Carlos Lasso') : (name?.trim() || user.name),
city: city?.trim() || user.city, city: city?.trim() || user.city,
address: address?.trim() || user.address, address: address?.trim() || user.address,
role: isCarlos ? 'admin' : user.role,
phoneVerified: true, phoneVerified: true,
requiresShippingDeposit: hadRejections ? true : false, requiresShippingDeposit: hadRejections && !isCarlos ? true : false,
depositReason: hadRejections depositReason: isCarlos
? 'Super Administrador'
: hadRejections
? user.depositReason ? user.depositReason
: 'Número de WhatsApp verificado con éxito (Pago 100% contraentrega habilitado)', : 'Número de WhatsApp verificado con éxito (Pago 100% contraentrega habilitado)',
lastLogin: 'Justo ahora', lastLogin: 'Justo ahora',

View File

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service'; import { dataService, isCarlosAdmin } from '@/lib/data-service';
import { User } from '@/types'; import { User } from '@/types';
export async function GET(request: Request) { export async function GET(request: Request) {
@ -23,21 +23,38 @@ export async function GET(request: Request) {
? 'Garantía de separación requerida para primer pedido' ? 'Garantía de separación requerida para primer pedido'
: 'Cliente distinguido exento de anticipo (100% Contraentrega)'; : 'Cliente distinguido exento de anticipo (100% Contraentrega)';
const isCarlos = isCarlosAdmin(cleanPhone);
const userRole = isCarlos ? 'admin' : (user?.role || 'customer');
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
isFirstPurchase: !hasCompletedPreviousOrder, isFirstPurchase: isCarlos ? false : !hasCompletedPreviousOrder,
requiresShippingDeposit, requiresShippingDeposit: isCarlos ? false : requiresShippingDeposit,
depositAmount, depositAmount: isCarlos ? 0 : depositAmount,
depositReason, depositReason: isCarlos ? 'Super Administrador DripZone' : depositReason,
phoneVerified: user ? user.phoneVerified === true : false, phoneVerified: isCarlos ? true : (user ? user.phoneVerified === true : false),
user: user user: user
? { ? {
id: user.id, ...user,
name: user.name, role: userRole,
phoneVerified: user.phoneVerified, phoneVerified: isCarlos ? true : user.phoneVerified,
totalOrders: user.totalOrders || existingOrders.length, requiresShippingDeposit: isCarlos ? false : requiresShippingDeposit,
requiresShippingDeposit, depositReason: isCarlos ? 'Super Administrador DripZone' : depositReason,
depositReason, }
: isCarlos
? {
id: 'usr-admin-carlos',
name: 'Carlos Lasso',
email: 'carlos.andres.lasso@gmail.com',
phone: cleanPhone,
city: 'Cali',
address: 'Sede Principal / Cali',
role: 'admin',
isActive: true,
phoneVerified: true,
requiresShippingDeposit: false,
depositReason: 'Super Administrador DripZone',
totalOrders: 0,
} }
: null, : null,
}); });

View File

@ -272,13 +272,17 @@ export const AuthModal: React.FC = () => {
<p className="text-[11px] text-neutral-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-3 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' || user.phone?.replace(/\D/g, '').endsWith('3167455877') || user.email?.toLowerCase() === 'carlos.andres.lasso@gmail.com'
? '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'
? 'bg-fuchsia-500/20 text-fuchsia-300 border-fuchsia-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' ? '👑 Super Administrador' : user.role === 'operator' ? '📦 Operador Logística' : '🛍️ Cliente VIP'} {user.role === 'admin' || user.phone?.replace(/\D/g, '').endsWith('3167455877') || user.email?.toLowerCase() === 'carlos.andres.lasso@gmail.com'
? '👑 Super Administrador'
: user.role === 'operator'
? '📦 Operador Logística'
: '🛍️ Cliente VIP'}
</span> </span>
</div> </div>
</div> </div>
@ -328,11 +332,11 @@ export const AuthModal: React.FC = () => {
{user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>} {user.address && <p><strong>🏠 Dirección:</strong> {user.address}</p>}
</div> </div>
{user.role === 'admin' || user.role === 'operator' ? ( {user.role === 'admin' || user.role === 'operator' || user.phone?.replace(/\D/g, '').endsWith('3167455877') || user.email?.toLowerCase() === 'carlos.andres.lasso@gmail.com' ? (
<Link <Link
href="/admin" href="/admin"
onClick={closeAuthModal} onClick={closeAuthModal}
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`} 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 cursor-pointer`}
> >
<Shield className="w-4 h-4" /> <Shield className="w-4 h-4" />
<span>Ir al Panel de Control Admin</span> <span>Ir al Panel de Control Admin</span>

View File

@ -21,7 +21,7 @@ export const MobileBottomDock: React.FC<MobileBottomDockProps> = ({
onOpenSearch, onOpenSearch,
}) => { }) => {
const { currentBrand } = useBrand(); const { currentBrand } = useBrand();
const { user, openAuthModal } = useAuth(); const { user, isAdmin: authIsAdmin, openAuthModal } = useAuth();
const pathname = usePathname(); const pathname = usePathname();
const [activeTab, setActiveTab] = useState('home'); const [activeTab, setActiveTab] = useState('home');
@ -35,8 +35,14 @@ export const MobileBottomDock: React.FC<MobileBottomDockProps> = ({
window.open(`https://wa.me/${currentBrand.whatsappNumber}?text=${text}`, '_blank'); window.open(`https://wa.me/${currentBrand.whatsappNumber}?text=${text}`, '_blank');
}; };
const isAdmin = Boolean(
const isAdmin = Boolean(user && (user.role === 'admin' || user.role === 'operator')); authIsAdmin ||
(user && (
user.role === 'admin' ||
user.role === 'operator' ||
(user.phone && user.phone.replace(/\D/g, '').endsWith('3167455877'))
))
);
const isAdminActive = pathname?.startsWith('/admin'); const isAdminActive = pathname?.startsWith('/admin');
const isHomeActive = pathname === '/' && activeTab === 'home'; const isHomeActive = pathname === '/' && activeTab === 'home';

View File

@ -28,7 +28,7 @@ export const Navbar: React.FC<NavbarProps> = ({
onOpenCustomRequest, onOpenCustomRequest,
}) => { }) => {
const { currentBrand } = useBrand(); const { currentBrand } = useBrand();
const { user, openAuthModal } = useAuth(); const { user, isAdmin, openAuthModal } = useAuth();
return ( return (
<header className={`sticky top-0 z-40 backdrop-blur-xl border-b ${currentBrand.colors.headerBg}`}> <header className={`sticky top-0 z-40 backdrop-blur-xl border-b ${currentBrand.colors.headerBg}`}>
@ -151,15 +151,24 @@ export const Navbar: React.FC<NavbarProps> = ({
<span className="hidden sm:inline">Mis Pedidos</span> <span className="hidden sm:inline">Mis Pedidos</span>
</Link> </Link>
{/* Admin Link - Solo para Administradores */} {/* Admin Link - Acceso Directo y Permanente */}
{user && (user.role === 'admin' || user.role === 'operator') && ( {isAdmin ? (
<Link <Link
href="/admin" href="/admin"
className="hidden sm:flex items-center space-x-1 text-xs font-semibold text-neutral-300 hover:text-purple-400 bg-neutral-900/80 hover:bg-neutral-800 px-3 py-2 rounded-full border border-neutral-700 transition-all" className="flex items-center space-x-1.5 text-xs font-black text-white bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:from-purple-500 hover:to-fuchsia-500 px-3.5 py-2 rounded-full border border-purple-400/80 shadow-md shadow-purple-600/30 transition-all cursor-pointer hover:scale-105 active:scale-95"
title="Panel de Administración" title="Panel de Control Administrador DripZone"
>
<Store className="w-4 h-4 text-purple-200" />
<span>Admin</span>
</Link>
) : (
<Link
href="/admin"
className="flex items-center space-x-1 text-xs font-semibold text-neutral-400 hover:text-purple-300 bg-neutral-900/80 hover:bg-neutral-800 px-2.5 sm:px-3 py-2 rounded-full border border-neutral-700 hover:border-purple-500/40 transition-all cursor-pointer"
title="Acceso al Panel de Administración"
> >
<Store className="w-4 h-4 text-purple-400" /> <Store className="w-4 h-4 text-purple-400" />
<span className="hidden lg:inline">Admin</span> <span className="hidden sm:inline">Admin</span>
</Link> </Link>
)} )}

View File

@ -5,6 +5,7 @@ import { User } from '@/types';
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
isAdmin: boolean;
sendOtp: (identifier: string) => Promise<{ success: boolean; error?: string; message?: string; phone?: string }>; sendOtp: (identifier: string) => Promise<{ success: boolean; error?: string; message?: string; phone?: string }>;
verifyOtp: (identifier: string, code: string) => Promise<{ success: boolean; error?: string }>; verifyOtp: (identifier: string, code: string) => Promise<{ success: boolean; error?: string }>;
login: (identifier: string) => Promise<{ success: boolean; error?: string }>; login: (identifier: string) => Promise<{ success: boolean; error?: string }>;
@ -15,6 +16,19 @@ interface AuthContextType {
closeAuthModal: () => void; closeAuthModal: () => void;
} }
export function isUserAdmin(user: User | null): boolean {
if (!user) return false;
if (user.role === 'admin' || user.role === 'operator') return true;
const cleanDigits = (user.phone || '').replace(/\D/g, '');
const email = (user.email || '').toLowerCase();
return (
cleanDigits.endsWith('3167455877') ||
cleanDigits.includes('3167455877') ||
email === 'carlos.andres.lasso@gmail.com' ||
email === 'info@drip-zone.online'
);
}
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@ -25,7 +39,21 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const saved = localStorage.getItem('dripzone_user'); const saved = localStorage.getItem('dripzone_user');
if (saved) { if (saved) {
try { try {
setUser(JSON.parse(saved)); const parsed = JSON.parse(saved);
const cleanDigits = (parsed.phone || '').replace(/\D/g, '');
const email = (parsed.email || '').toLowerCase();
if (
cleanDigits.endsWith('3167455877') ||
cleanDigits.includes('3167455877') ||
email === 'carlos.andres.lasso@gmail.com' ||
email === 'info@drip-zone.online'
) {
parsed.role = 'admin';
parsed.isActive = true;
parsed.phoneVerified = true;
localStorage.setItem('dripzone_user', JSON.stringify(parsed));
}
setUser(parsed);
} catch (e) {} } catch (e) {}
} }
}, []); }, []);
@ -56,6 +84,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}); });
const data = await res.json(); const data = await res.json();
if (data.success && data.user) { if (data.success && data.user) {
const cleanDigits = (data.user.phone || '').replace(/\D/g, '');
const email = (data.user.email || '').toLowerCase();
if (
cleanDigits.endsWith('3167455877') ||
cleanDigits.includes('3167455877') ||
email === 'carlos.andres.lasso@gmail.com' ||
email === 'info@drip-zone.online'
) {
data.user.role = 'admin';
data.user.isActive = true;
data.user.phoneVerified = true;
}
setUser(data.user); setUser(data.user);
localStorage.setItem('dripzone_user', JSON.stringify(data.user)); localStorage.setItem('dripzone_user', JSON.stringify(data.user));
setIsAuthModalOpen(false); setIsAuthModalOpen(false);
@ -105,6 +145,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
user, user,
isAdmin: isUserAdmin(user),
sendOtp, sendOtp,
verifyOtp, verifyOtp,
login, login,

View File

@ -170,17 +170,58 @@ export const INITIAL_SUPPLIER_POSTS: SupplierPost[] = [];
export const INITIAL_ORDERS: Order[] = []; export const INITIAL_ORDERS: Order[] = [];
export function isCarlosAdmin(identifier?: string | null): boolean {
if (!identifier) return false;
const clean = identifier.trim().toLowerCase();
const digits = clean.replace(/\D/g, '');
return (
digits.endsWith('3167455877') ||
digits.includes('3167455877') ||
clean === 'carlos.andres.lasso@gmail.com' ||
clean === 'info@drip-zone.online'
);
}
export const INITIAL_USERS: User[] = [ export const INITIAL_USERS: User[] = [
{
id: 'usr-admin-carlos',
name: 'Carlos Lasso',
email: 'carlos.andres.lasso@gmail.com',
phone: '3167455877',
city: 'Cali',
address: 'Sede Principal / Cali',
role: 'admin',
twoFactorEnabled: false,
isActive: true,
phoneVerified: true,
createdAt: '2026-01-01',
totalOrders: 0,
},
{
id: 'usr-admin-carlos-57',
name: 'Carlos Lasso (Admin)',
email: 'info@drip-zone.online',
phone: '573167455877',
city: 'Cali',
address: 'Sede Principal / Cali',
role: 'admin',
twoFactorEnabled: false,
isActive: true,
phoneVerified: true,
createdAt: '2026-01-01',
totalOrders: 0,
},
{ {
id: 'usr-admin-dripzone', id: 'usr-admin-dripzone',
name: 'Administrador DripZone', name: 'Administrador DripZone',
email: 'info@drip-zone.online', email: 'soporte@drip-zone.online',
phone: '573106390296', phone: '573106390296',
city: 'Cali', city: 'Cali',
address: 'Sede Principal / Cali', address: 'Sede Principal / Cali',
role: 'admin', role: 'admin',
twoFactorEnabled: false, twoFactorEnabled: false,
isActive: true, isActive: true,
phoneVerified: true,
createdAt: '2026-01-01', createdAt: '2026-01-01',
totalOrders: 0, totalOrders: 0,
}, },
@ -905,18 +946,69 @@ class DataService {
} }
// Control de Usuarios // Control de Usuarios
private users: User[] = [...INITIAL_USERS]; private initUsers(): User[] {
const loaded = loadPersistedData<User[]>('persisted_users.json', [...INITIAL_USERS]);
// Asegurar que las cuentas de administrador de Carlos siempre existan y tengan role: 'admin'
INITIAL_USERS.forEach((initialAdmin) => {
const existing = loaded.find(
(u) =>
u.id === initialAdmin.id ||
u.phone.replace(/\D/g, '') === initialAdmin.phone.replace(/\D/g, '') ||
(initialAdmin.email && u.email.toLowerCase() === initialAdmin.email.toLowerCase())
);
if (!existing) {
loaded.unshift({ ...initialAdmin });
} else {
existing.role = 'admin';
existing.isActive = true;
existing.phoneVerified = true;
}
});
// Sanitizar cualquier registro con el número de Carlos para que sea admin
loaded.forEach((u) => {
if (isCarlosAdmin(u.phone) || isCarlosAdmin(u.email)) {
u.role = 'admin';
u.isActive = true;
u.phoneVerified = true;
}
});
return loaded;
}
private users: User[] = this.initUsers();
getUsers(): User[] { getUsers(): User[] {
// Garantizar que Carlos siempre aparezca con role admin
this.users.forEach((u) => {
if (isCarlosAdmin(u.phone) || isCarlosAdmin(u.email)) {
u.role = 'admin';
u.isActive = true;
u.phoneVerified = true;
}
});
return this.users; return this.users;
} }
getUserById(id: string): User | undefined { getUserById(id: string): User | undefined {
return this.users.find((u) => u.id === id); const user = this.users.find((u) => u.id === id);
if (user && (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email))) {
user.role = 'admin';
user.isActive = true;
user.phoneVerified = true;
}
return user;
} }
addUser(user: User): User { addUser(user: User): User {
if (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email)) {
user.role = 'admin';
user.isActive = true;
user.phoneVerified = true;
}
this.users.unshift(user); this.users.unshift(user);
savePersistedData('persisted_users.json', this.users);
return user; return user;
} }
@ -924,6 +1016,12 @@ class DataService {
const idx = this.users.findIndex((u) => u.id === id); const idx = this.users.findIndex((u) => u.id === id);
if (idx === -1) return null; if (idx === -1) return null;
this.users[idx] = { ...this.users[idx], ...updates }; this.users[idx] = { ...this.users[idx], ...updates };
if (isCarlosAdmin(this.users[idx].phone) || isCarlosAdmin(this.users[idx].email)) {
this.users[idx].role = 'admin';
this.users[idx].isActive = true;
this.users[idx].phoneVerified = true;
}
savePersistedData('persisted_users.json', this.users);
return this.users[idx]; return this.users[idx];
} }
@ -931,12 +1029,42 @@ class DataService {
const idx = this.users.findIndex((u) => u.id === id); const idx = this.users.findIndex((u) => u.id === id);
if (idx === -1) return false; if (idx === -1) return false;
this.users.splice(idx, 1); this.users.splice(idx, 1);
savePersistedData('persisted_users.json', this.users);
return true; return true;
} }
authenticate(emailOrPhone: string): User | null { authenticate(emailOrPhone: string): User | null {
const q = emailOrPhone.trim().toLowerCase(); const q = emailOrPhone.trim().toLowerCase();
const cleanDigits = q.replace(/\D/g, ''); const cleanDigits = q.replace(/\D/g, '');
// Acceso directo garantizado para Carlos Lasso (3167455877)
if (isCarlosAdmin(q)) {
let admin = this.users.find((u) => isCarlosAdmin(u.phone) || isCarlosAdmin(u.email));
if (!admin) {
admin = {
id: 'usr-admin-carlos',
name: 'Carlos Lasso',
email: 'carlos.andres.lasso@gmail.com',
phone: cleanDigits || '3167455877',
city: 'Cali',
address: 'Sede Principal / Cali',
role: 'admin',
twoFactorEnabled: false,
isActive: true,
phoneVerified: true,
createdAt: '2026-01-01',
totalOrders: 0,
};
this.users.unshift(admin);
savePersistedData('persisted_users.json', this.users);
} else {
admin.role = 'admin';
admin.isActive = true;
admin.phoneVerified = true;
}
return admin;
}
const user = this.users.find((u) => { const user = this.users.find((u) => {
if (u.email.toLowerCase() === q) return true; if (u.email.toLowerCase() === q) return true;
const uDigits = u.phone.replace(/\D/g, ''); const uDigits = u.phone.replace(/\D/g, '');
@ -945,6 +1073,13 @@ class DataService {
} }
return false; return false;
}); });
if (user && (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email))) {
user.role = 'admin';
user.isActive = true;
user.phoneVerified = true;
}
return user || null; return user || null;
} }