diff --git a/deploy_dripzone_vps.py b/deploy_dripzone_vps.py index 9479567..6257241 100644 --- a/deploy_dripzone_vps.py +++ b/deploy_dripzone_vps.py @@ -37,7 +37,19 @@ def deploy(): print(f"[2/4] Conectando a VPS {HOST} via SSH / SFTP...") client = paramiko.SSHClient() 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() print("[3/4] Subiendo archivo comprimido y APK al servidor...") @@ -115,20 +127,21 @@ def send_apk_to_whatsapp(): import urllib.request import json 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 = { - "apikey": "83B5BD230796-4324-AE73-3508F69E279D", + "apikey": "EE49029EEB85-4568-8F81-C9FDFC6E2943", "Content-Type": "application/json" } caption = ( - "📲 *¡DRIPZONE ACTUALIZADA EN PRODUCCIÓN!* 🚀👟\n" + "👑 *¡ACCESO DE ADMINISTRADOR RESTAURADO & APK COMPILADO!* 🚀👟\n" "━━━━━━━━━━━━━━━━━━━━━━\n" - "✨ *Novedades de esta versió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" - "• 🛒 *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" - "• 📱 *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" - "• 💬 *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" - "🌐 *Descarga directa del APK:*\n👉 https://drip-zone.online/DripZone.apk" + "✨ *Ajustes implementados y desplegados en Producción:*\n\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" + "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" + "3️⃣ 🚀 *Despliegue en Servidor VPS*: Contenedor `dripzone-production` reconstruido e iniciado con éxito en el servidor de producción.\n" + "4️⃣ 📲 *APK Android Actualizado*: Compilado y disponible para descarga directa.\n\n" + "👉 *Panel de Administración:* https://drip-zone.online/admin\n" + "📥 *Descarga directa del APK:* https://drip-zone.online/DripZone.apk" ) payload = { "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})") except Exception as e: 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 = { "number": "573167455877", "text": ( - "📲 *¡DRIPZONE APK ACTUALIZADA EN PRODUCCIÓN!* 🚀👟\n\n" f"{caption}\n\n" "📥 *Descarga e instala el APK actualizado aquí:*\n" "👉 https://drip-zone.online/DripZone.apk" diff --git a/dripzone-app/src/app/admin/layout.tsx b/dripzone-app/src/app/admin/layout.tsx index 0bf8c8c..483686a 100644 --- a/dripzone-app/src/app/admin/layout.tsx +++ b/dripzone-app/src/app/admin/layout.tsx @@ -27,8 +27,16 @@ import { AuthModal } from '@/components/auth/AuthModal'; export default function AdminLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); - const { user, openAuthModal } = useAuth(); - const isAdmin = Boolean(user && (user.role === 'admin' || user.role === 'operator')); + const { user, isAdmin: authIsAdmin, openAuthModal } = useAuth(); + 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 = [ { href: '/admin', label: 'Resumen & KPIs', icon: LayoutDashboard }, diff --git a/dripzone-app/src/app/api/auth/verify-otp/route.ts b/dripzone-app/src/app/api/auth/verify-otp/route.ts index 9e9f8bf..f8c8dcd 100644 --- a/dripzone-app/src/app/api/auth/verify-otp/route.ts +++ b/dripzone-app/src/app/api/auth/verify-otp/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { dataService } from '@/lib/data-service'; +import { dataService, isCarlosAdmin } from '@/lib/data-service'; import { otpStore } from '@/lib/otp-store'; 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 }); } + const isCarlos = isCarlosAdmin(cleanInput); + // Código válido -> Obtener o crear usuario registrado en la plataforma let user = dataService.authenticate(cleanInput); 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 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({ - id: `usr-${Date.now()}`, + id: isCarlos ? 'usr-admin-carlos' : `usr-${Date.now()}`, name: assignedName, - email: `${cleanPhone}@cliente.dripzone.co`, + email: isCarlos ? 'carlos.andres.lasso@gmail.com' : `${cleanPhone}@cliente.dripzone.co`, phone: cleanPhone, city: city?.trim() || 'Cali', - address: address?.trim() || '', - role: 'customer', + address: address?.trim() || (isCarlos ? 'Sede Principal / Cali' : ''), + role: isCarlos ? 'admin' : 'customer', isActive: true, phoneVerified: true, requiresShippingDeposit: false, - depositReason: 'Número verificado vía WhatsApp OTP', + depositReason: isCarlos ? 'Super Administrador' : 'Número verificado vía WhatsApp OTP', rejectedOrdersCount: 0, createdAt: new Date().toISOString().split('T')[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 const hadRejections = (user.rejectedOrdersCount || 0) > 0; 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, address: address?.trim() || user.address, + role: isCarlos ? 'admin' : user.role, phoneVerified: true, - requiresShippingDeposit: hadRejections ? true : false, - depositReason: hadRejections + requiresShippingDeposit: hadRejections && !isCarlos ? true : false, + depositReason: isCarlos + ? 'Super Administrador' + : hadRejections ? user.depositReason : 'Número de WhatsApp verificado con éxito (Pago 100% contraentrega habilitado)', lastLogin: 'Justo ahora', diff --git a/dripzone-app/src/app/api/users/route.ts b/dripzone-app/src/app/api/users/route.ts index d331bc7..23445a3 100644 --- a/dripzone-app/src/app/api/users/route.ts +++ b/dripzone-app/src/app/api/users/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { dataService } from '@/lib/data-service'; +import { dataService, isCarlosAdmin } from '@/lib/data-service'; import { User } from '@/types'; 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' : 'Cliente distinguido exento de anticipo (100% Contraentrega)'; + const isCarlos = isCarlosAdmin(cleanPhone); + const userRole = isCarlos ? 'admin' : (user?.role || 'customer'); + return NextResponse.json({ success: true, - isFirstPurchase: !hasCompletedPreviousOrder, - requiresShippingDeposit, - depositAmount, - depositReason, - phoneVerified: user ? user.phoneVerified === true : false, + isFirstPurchase: isCarlos ? false : !hasCompletedPreviousOrder, + requiresShippingDeposit: isCarlos ? false : requiresShippingDeposit, + depositAmount: isCarlos ? 0 : depositAmount, + depositReason: isCarlos ? 'Super Administrador DripZone' : depositReason, + phoneVerified: isCarlos ? true : (user ? user.phoneVerified === true : false), user: user ? { - id: user.id, - name: user.name, - phoneVerified: user.phoneVerified, - totalOrders: user.totalOrders || existingOrders.length, - requiresShippingDeposit, - depositReason, + ...user, + role: userRole, + phoneVerified: isCarlos ? true : user.phoneVerified, + requiresShippingDeposit: isCarlos ? false : requiresShippingDeposit, + depositReason: isCarlos ? 'Super Administrador DripZone' : 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, }); diff --git a/dripzone-app/src/components/auth/AuthModal.tsx b/dripzone-app/src/components/auth/AuthModal.tsx index 4100931..189983c 100644 --- a/dripzone-app/src/components/auth/AuthModal.tsx +++ b/dripzone-app/src/components/auth/AuthModal.tsx @@ -272,13 +272,17 @@ export const AuthModal: React.FC = () => {

{user.email}

- {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'}
@@ -328,11 +332,11 @@ export const AuthModal: React.FC = () => { {user.address &&

🏠 Dirección: {user.address}

} - {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' ? ( Ir al Panel de Control Admin diff --git a/dripzone-app/src/components/mobile/MobileBottomDock.tsx b/dripzone-app/src/components/mobile/MobileBottomDock.tsx index d3c8913..ac3da16 100644 --- a/dripzone-app/src/components/mobile/MobileBottomDock.tsx +++ b/dripzone-app/src/components/mobile/MobileBottomDock.tsx @@ -21,7 +21,7 @@ export const MobileBottomDock: React.FC = ({ onOpenSearch, }) => { const { currentBrand } = useBrand(); - const { user, openAuthModal } = useAuth(); + const { user, isAdmin: authIsAdmin, openAuthModal } = useAuth(); const pathname = usePathname(); const [activeTab, setActiveTab] = useState('home'); @@ -35,8 +35,14 @@ export const MobileBottomDock: React.FC = ({ window.open(`https://wa.me/${currentBrand.whatsappNumber}?text=${text}`, '_blank'); }; - - 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')) + )) + ); const isAdminActive = pathname?.startsWith('/admin'); const isHomeActive = pathname === '/' && activeTab === 'home'; diff --git a/dripzone-app/src/components/store/Navbar.tsx b/dripzone-app/src/components/store/Navbar.tsx index bb11e50..732a97a 100644 --- a/dripzone-app/src/components/store/Navbar.tsx +++ b/dripzone-app/src/components/store/Navbar.tsx @@ -28,7 +28,7 @@ export const Navbar: React.FC = ({ onOpenCustomRequest, }) => { const { currentBrand } = useBrand(); - const { user, openAuthModal } = useAuth(); + const { user, isAdmin, openAuthModal } = useAuth(); return (
@@ -151,15 +151,24 @@ export const Navbar: React.FC = ({ Mis Pedidos - {/* Admin Link - Solo para Administradores */} - {user && (user.role === 'admin' || user.role === 'operator') && ( + {/* Admin Link - Acceso Directo y Permanente */} + {isAdmin ? ( + + Admin + + ) : ( + - Admin + Admin )} diff --git a/dripzone-app/src/context/AuthContext.tsx b/dripzone-app/src/context/AuthContext.tsx index c50dcf3..2241e25 100644 --- a/dripzone-app/src/context/AuthContext.tsx +++ b/dripzone-app/src/context/AuthContext.tsx @@ -5,6 +5,7 @@ import { User } from '@/types'; interface AuthContextType { user: User | null; + isAdmin: boolean; sendOtp: (identifier: string) => Promise<{ success: boolean; error?: string; message?: string; phone?: string }>; verifyOtp: (identifier: string, code: string) => Promise<{ success: boolean; error?: string }>; login: (identifier: string) => Promise<{ success: boolean; error?: string }>; @@ -15,6 +16,19 @@ interface AuthContextType { 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(undefined); 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'); if (saved) { 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) {} } }, []); @@ -56,6 +84,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }); const data = await res.json(); 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); localStorage.setItem('dripzone_user', JSON.stringify(data.user)); setIsAuthModalOpen(false); @@ -105,6 +145,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children ('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[] { + // 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; } 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 { + if (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email)) { + user.role = 'admin'; + user.isActive = true; + user.phoneVerified = true; + } this.users.unshift(user); + savePersistedData('persisted_users.json', this.users); return user; } @@ -924,6 +1016,12 @@ class DataService { const idx = this.users.findIndex((u) => u.id === id); if (idx === -1) return null; 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]; } @@ -931,12 +1029,42 @@ class DataService { const idx = this.users.findIndex((u) => u.id === id); if (idx === -1) return false; this.users.splice(idx, 1); + savePersistedData('persisted_users.json', this.users); return true; } authenticate(emailOrPhone: string): User | null { const q = emailOrPhone.trim().toLowerCase(); 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) => { if (u.email.toLowerCase() === q) return true; const uDigits = u.phone.replace(/\D/g, ''); @@ -945,6 +1073,13 @@ class DataService { } return false; }); + + if (user && (isCarlosAdmin(user.phone) || isCarlosAdmin(user.email))) { + user.role = 'admin'; + user.isActive = true; + user.phoneVerified = true; + } + return user || null; }