fix(auth): restaurar boton admin permanente en web y asegurar rol de Super Administrador para Carlos (3167455877)
This commit is contained in:
parent
daa24a25fc
commit
a2524211f2
@ -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"
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user