From 332b0c23131f09b0be9ad322a2613b0279c760d4 Mon Sep 17 00:00:00 2001 From: Erik <80664141+fr0st87@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:29:06 -0400 Subject: [PATCH] Revert "Update from task ae80d18e-ddd4-4508-99f2-7b26d4e5f77c" --- .gitignore | 1 - SEGURIDAD.md | 139 ----------------------------------------- admin-auth.js | 13 +--- admin-login.js | 116 ++-------------------------------- admin.html | 11 ---- index.html | 12 ---- store.js | 139 ++++------------------------------------- supabase-config.js | 24 -------- supabase-schema.sql | 147 -------------------------------------------- 9 files changed, 20 insertions(+), 582 deletions(-) delete mode 100644 .gitignore delete mode 100644 SEGURIDAD.md delete mode 100644 supabase-config.js delete mode 100644 supabase-schema.sql diff --git a/.gitignore b/.gitignore deleted file mode 100644 index ba999ff..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Nothing needs to be added to .gitignore since only JavaScript source files (admin-auth.js, admin-login.js) were modified and no build artifacts, dependencies, or temporary files are present in the changes. \ No newline at end of file diff --git a/SEGURIDAD.md b/SEGURIDAD.md deleted file mode 100644 index 82cfd19..0000000 --- a/SEGURIDAD.md +++ /dev/null @@ -1,139 +0,0 @@ -# 🔒 Guía de Seguridad - MegaOfertas con Supabase - -## ✅ Cambios Implementados - -### 1. **Autenticación Segura con Supabase Auth** -- Login contra servidor (no solo hash local) -- Tokens JWT con expiración -- Sesiones persistentes seguras -- Rate limiting real en Supabase - -### 2. **Base de Datos en la Nube** -- Catálogo almacenado en Supabase PostgreSQL -- Los clientes NO pueden modificar precios/stock desde el navegador -- Row Level Security (RLS) activado - -### 3. **Headers de Seguridad** -- Content Security Policy (CSP) -- X-Content-Type-Options: nosniff -- X-Frame-Options: DENY -- X-XSS-Protection - -### 4. **Cache Local Temporal** -- Solo para performance (5 minutos TTL) -- Siempre se valida contra servidor -- Fallback offline seguro - ---- - -## 📋 Pasos para Configurar Supabase - -### Paso 1: Ejecutar Script SQL -1. Ve a https://supabase.com/dashboard -2. Selecciona tu proyecto: `hkgdbealspivhkmvtqcv` -3. Ve a **SQL Editor** -4. Copia y pega el contenido de `supabase-schema.sql` -5. Ejecuta el script - -### Paso 2: Crear Usuario Admin -1. En Supabase Dashboard, ve a **Authentication > Users** -2. Click en **Add user** -3. Ingresa email y contraseña segura -4. Guarda las credenciales - -### Paso 3: Configurar Credenciales en el Código -Edita `admin-auth.js`: -```javascript -window.ELECTROSTORE_ADMIN_PASSWORD_SHA256 = 'TU_HASH_AQUI'; -window.SUPABASE_ADMIN_EMAIL = 'tu-email@dominio.com'; -``` - -Para generar el hash SHA-256: -- Abre `tools/generar-hash.html` en tu navegador -- Ingresa tu contraseña -- Copia el hash generado - -### Paso 4: Verificar RLS -En Supabase Dashboard: -1. Ve a **Authentication > Policies** -2. Verifica que las tablas tengan RLS activado -3. Confirma las políticas de lectura/escritura - ---- - -## 🚀 Despliegue en GitHub Pages - -### Antes de Desplegar: -```bash -# Verifica que todos los archivos estén commiteados -git add . -git commit -m "Seguridad: integración con Supabase" -git push origin main -``` - -### En GitHub: -1. Ve a Settings > Pages -2. Source: Deploy from branch -3. Branch: main, folder: / (root) -4. Save - -### URL de producción: -`https://TU_USUARIO.github.io/TU_REPO/` - ---- - -## ⚠️ Consideraciones de Seguridad - -### Lo que SÍ está protegido: -✅ Autenticación contra servidor -✅ Precios y stock no manipulables -✅ Sesiones con tokens JWT -✅ Políticas RLS en base de datos -✅ Headers de seguridad HTTP - -### Lo que DEBES hacer adicionalmente: -🔲 Usar HTTPS (GitHub Pages lo incluye) -🔲 No exponer claves secretas en el repo -🔲 Actualizar contraseñas regularmente -🔲 Monitorear logs de Supabase -🔲 Hacer backup de la base de datos - -### Limitaciones de GitHub Pages: -⚠️ Todo el código frontend es público -⚠️ No hay backend propio (usamos Supabase) -⚠️ Los pagos deben procesarse externamente - ---- - -## 🛡️ Mejoras Futuras Recomendadas - -1. **Edge Functions para Pedidos** - - Validar totales en servidor - - Prevenir manipulación de precios - -2. **Integración con Pasarela de Pago** - - Stripe o PayPal - - Nunca procesar tarjetas en frontend - -3. **Monitoreo y Logs** - - Supabase Logs - - Alertas de actividad sospechosa - -4. **Backup Automático** - - Exportar DB periódicamente - - Versionar datos críticos - ---- - -## 📞 Soporte - -Si tienes problemas: -1. Revisa la consola del navegador (F12) -2. Verifica logs en Supabase Dashboard -3. Confirma que RLS esté activo -4. Prueba en modo incógnito - ---- - -**Fecha de actualización:** Enero 2025 -**Versión:** 2.0.0 (con Supabase) diff --git a/admin-auth.js b/admin-auth.js index 22f2da3..016b768 100644 --- a/admin-auth.js +++ b/admin-auth.js @@ -5,14 +5,7 @@ * Generala con tools/generar-hash.html (o cualquier herramienta SHA-256) * y pega el resultado entre comillas. * - * EN PRODUCCIÓN CON SUPABASE: - * - La autenticación real se hace contra Supabase Auth - * - Este hash es SOLO como fallback/backup si falla la conexión a Supabase - * - Configura usuarios en el dashboard de Supabase > Authentication > Users - * - El sistema intentará primero login con Supabase, si falla usará este hash local + * En localhost sin valor configurado el admin se abre con aviso (solo desarrollo). + * En github.io u otros dominios públicos hace falta el hash obligatoriamente. */ -window.ELECTROSTORE_ADMIN_PASSWORD_SHA256 = ''; // DEJAR VACÍO - Usando Supabase Auth - -// Configuración de Supabase Auth para admin -// IMPORTANTE: Poné aquí el email EXACTO que creaste en Supabase > Authentication > Users -window.SUPABASE_ADMIN_EMAIL = 'skynet871012@gmail.com'; // ← Email configurado +window.ELECTROSTORE_ADMIN_PASSWORD_SHA256 = ''; diff --git a/admin-login.js b/admin-login.js index f93e073..56a5080 100644 --- a/admin-login.js +++ b/admin-login.js @@ -1,14 +1,8 @@ /** - * Acceso al panel: autenticación con Supabase Auth + sesión en sessionStorage. - * - * SEGURIDAD: - * - La autenticación se realiza contra Supabase Auth (servidor) - * - El hash local es solo como fallback para desarrollo - * - Rate limiting real implementado en Supabase Edge Functions + * Acceso al panel: sesión en sessionStorage, límite de intentos, SHA-256. */ (function () { const SESSION_KEY = 'electrostore_admin_exp'; - const SUPABASE_SESSION_KEY = 'supabase_admin_session'; const SESSION_MS = 8 * 60 * 60 * 1000; const ATT_KEY = 'electrostore_login_att_v1'; @@ -19,7 +13,7 @@ function configuredHash() { const h = window.ELECTROSTORE_ADMIN_PASSWORD_SHA256; - return typeof h === 'string' && /^[a-f0-9]{64}$/i.test(h.trim()) && h !== 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; + return typeof h === 'string' && /^[a-f0-9]{64}$/i.test(h.trim()); } function getHashHex() { @@ -39,7 +33,6 @@ function clearSession() { sessionStorage.removeItem(SESSION_KEY); - sessionStorage.removeItem(SUPABASE_SESSION_KEY); } async function sha256Hex(text) { @@ -47,66 +40,6 @@ return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join(''); } - /** - * Autenticación con Supabase Auth - */ - async function signInWithSupabase(email, password) { - try { - if (!window.sbClient) { - throw new Error('Supabase no disponible'); - } - - const { data, error } = await window.sbClient.auth.signInWithPassword({ - email: email, - password: password - }); - - if (error) throw error; - - // Guardar token de sesión - if (data.user && data.session) { - sessionStorage.setItem(SUPABASE_SESSION_KEY, JSON.stringify({ - access_token: data.session.access_token, - refresh_token: data.session.refresh_token, - user_id: data.user.id, - email: data.user.email, - expires_at: data.session.expires_at - })); - return { success: true, user: data.user }; - } - - throw new Error('No se recibió sesión válida'); - } catch (error) { - console.error('Error en Supabase Auth:', error); - throw error; - } - } - - /** - * Verificar si hay sesión activa en Supabase - */ - async function checkSupabaseSession() { - try { - if (!window.sbClient) return null; - - const { data: { session }, error } = await window.sbClient.auth.getSession(); - - if (error || !session) return null; - - // Verificar que el email coincida con el de admin configurado - const adminEmail = window.SUPABASE_ADMIN_EMAIL; - if (adminEmail && session.user.email !== adminEmail) { - console.warn('Email de usuario no coincide con admin configurado'); - return null; - } - - return session; - } catch (error) { - console.error('Error verificando sesión Supabase:', error); - return null; - } - } - function getAttemptsState() { try { return JSON.parse(sessionStorage.getItem(ATT_KEY) || '{}') || {}; @@ -152,11 +85,7 @@ const btn = document.getElementById('admin-logout'); if (btn && !btn.dataset.bound) { btn.dataset.bound = '1'; - btn.addEventListener('click', async () => { - // Cerrar sesión en Supabase si existe - if (window.sbClient) { - await window.sbClient.auth.signOut(); - } + btn.addEventListener('click', () => { clearSession(); location.reload(); }); @@ -179,35 +108,23 @@ if (htmlMessage) msgEl.innerHTML = htmlMessage; } - async function init() { + function init() { attachLogout(); - - // Primero verificar sesión de Supabase - const supabaseSession = await checkSupabaseSession(); - if (supabaseSession) { - setSession(); - showMain(false); - console.log('✅ Sesión de Supabase restaurada'); - return; - } - - // Fallback: verificar sesión local if (isSessionValid()) { showMain(false); return; } const hashHex = getHashHex(); - const adminEmail = window.SUPABASE_ADMIN_EMAIL; - if (!hashHex && !adminEmail) { + if (!hashHex) { if (isLocalDev()) { showMain(true); setSession(); return; } showGate( - 'Falta configurar credenciales de Supabase.

1. Editá admin-auth.js y poné tu email en SUPABASE_ADMIN_EMAIL.
2. Asegurate de haber creado ese usuario en Supabase Dashboard > Authentication > Users.
3. Volvé a desplegar en GitHub Pages.

El sistema usará Supabase Auth para validar tu acceso de forma segura.' + 'Falta configurar la contraseña.

Editá admin-auth.js en el repositorio: generá el SHA-256 de tu contraseña con tools/generar-hash.html y pegá el valor en ELECTROSTORE_ADMIN_PASSWORD_SHA256. Volvé a desplegar en GitHub Pages.' ); if (form) form.classList.add('hidden'); return; @@ -230,26 +147,6 @@ msgEl.textContent = 'Ingresá la contraseña.'; return; } - - // Intentar primero con Supabase Auth - if (adminEmail && adminEmail !== 'admin@megaofertas.local' && adminEmail !== 'tu-email@dominio.com' && window.sbClient) { - try { - msgEl.textContent = 'Autenticando...'; - const result = await signInWithSupabase(adminEmail, pass); - if (result.success) { - resetAttempts(); - passInput.value = ''; - setSession(); - showMain(false); - console.log('✅ Login exitoso con Supabase Auth'); - return; - } - } catch (supabaseError) { - console.warn('Supabase Auth falló, usando fallback local:', supabaseError.message); - } - } - - // Fallback: autenticación local con hash (solo desarrollo o sin Supabase) try { const got = (await sha256Hex(pass)).toLowerCase(); if (got === hashHex) { @@ -257,7 +154,6 @@ passInput.value = ''; setSession(); showMain(false); - console.log('✅ Login exitoso con hash local (modo fallback)'); } else { recordFailedAttempt(); msgEl.textContent = 'Contraseña incorrecta.'; diff --git a/admin.html b/admin.html index ca53b05..d1fb0c0 100644 --- a/admin.html +++ b/admin.html @@ -4,21 +4,11 @@ - - - - - - - MegaOfertas — Administración - - - @@ -131,7 +121,6 @@

Productos

- diff --git a/index.html b/index.html index 200aa2c..4124de2 100644 --- a/index.html +++ b/index.html @@ -5,22 +5,11 @@ - - - - - - - - MegaOfertas — Tienda online - - - @@ -162,7 +151,6 @@ - diff --git a/store.js b/store.js index ea02863..99e1955 100644 --- a/store.js +++ b/store.js @@ -1,33 +1,10 @@ /** - * Catálogo, carrito y favoritos integrados con Supabase. - * - * SEGURIDAD: - * - Los datos se guardan en Supabase, NO en localStorage del cliente - * - Precios y stock se validan en el servidor mediante Row Level Security - * - El cliente solo puede LECTURA de productos, no puede modificar precios + * Catálogo, carrito y favoritos en localStorage (tienda + admin). */ - -const STORE_KEY = 'electrostore_catalog_cache'; +const STORE_KEY = 'electrostore_catalog'; const CART_KEY = 'electrostore_cart'; const WISHLIST_KEY = 'electrostore_wishlist'; -// Referencia al cliente de Supabase -function getSupabaseClient() { - if (!window.sbClient) { - throw new Error('Supabase no inicializado. Verifica supabase-config.js'); - } - return window.sbClient; -} - -// Cache local temporal (solo para mejorar performance, no como fuente de verdad) -let cachedCatalog = null; -let cacheTimestamp = 0; -const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutos - -function isCacheValid() { - return cachedCatalog && (Date.now() - cacheTimestamp < CACHE_TTL_MS); -} - function getSiteRoot() { if (typeof window === 'undefined') return ''; let r = ''; @@ -183,117 +160,23 @@ function migrateCatalog(data) { return data; } -/** - * Carga el catálogo desde Supabase (con cache local temporal) - * Los datos vienen del servidor - el cliente NO puede modificarlos - */ -async function loadCatalog() { - // Si hay cache válido, devolverlo (para performance) - if (isCacheValid()) { - return cachedCatalog; - } - - try { - const client = getSupabaseClient(); - - // Cargar categorías - const { data: categories, error: catError } = await client - .from('categories') - .select('*') - .order('name'); - - if (catError) throw catError; - - // Cargar productos - const { data: products, error: prodError } = await client - .from('products') - .select('*') - .order('name'); - - if (prodError) throw prodError; - - cachedCatalog = { - categories: categories || [], - products: products || [] - }; - cacheTimestamp = Date.now(); - - // Guardar cache en sessionStorage como fallback offline - sessionStorage.setItem(STORE_KEY, JSON.stringify(cachedCatalog)); - - return cachedCatalog; - } catch (e) { - console.warn('Error cargando desde Supabase, usando cache local:', e); - // Fallback: intentar leer cache local - try { - const raw = sessionStorage.getItem(STORE_KEY); - if (raw) { - const data = JSON.parse(raw); - if (data.categories && data.products) { - cachedCatalog = data; - cacheTimestamp = Date.now(); - return data; - } - } - } catch (fallbackError) { - console.error('No se pudo leer cache local:', fallbackError); - } - - // Último recurso: catálogo por defecto (solo desarrollo) - const def = getDefaultCatalog(); - return def; - } -} - -/** - * Guarda catálogo en Supabase (SOLO para admin autenticado) - * Requiere sesión de admin válida - */ -async function saveCatalogToSupabase(data) { +function loadCatalog() { try { - const client = getSupabaseClient(); - - // Verificar sesión de admin - const adminSession = sessionStorage.getItem('supabase_admin_session'); - if (!adminSession) { - throw new Error('No autorizado: sesión de admin requerida'); - } - - // Actualizar categorías (upsert) - for (const cat of data.categories) { - const { error } = await client - .from('categories') - .upsert(cat, { onConflict: 'id' }); - if (error) throw error; - } - - // Actualizar productos (upsert) - for (const prod of data.products) { - const { error } = await client - .from('products') - .upsert(prod, { onConflict: 'id' }); - if (error) throw error; + const raw = sessionStorage.getItem(STORE_KEY); + if (raw) { + const data = JSON.parse(raw); + if (data.categories && data.products) return migrateCatalog(data); } - - // Invalidar cache local - cachedCatalog = null; - cacheTimestamp = 0; - - console.log('✅ Catálogo guardado en Supabase'); - return true; } catch (e) { - console.error('❌ Error guardando en Supabase:', e); - throw e; + console.warn('No se pudo leer el catálogo', e); } + const def = getDefaultCatalog(); + saveCatalog(def); + return def; } function saveCatalog(data) { - // En producción, esto debería llamar a saveCatalogToSupabase - // Por ahora guardamos en cache local para compatibilidad - cachedCatalog = data; - cacheTimestamp = Date.now(); sessionStorage.setItem(STORE_KEY, JSON.stringify(data)); - console.warn('⚠️ Datos guardados solo en cache local. Configura Supabase para persistencia.'); } /** Líneas del carrito: { productId, qty } */ diff --git a/supabase-config.js b/supabase-config.js deleted file mode 100644 index 39bec1b..0000000 --- a/supabase-config.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Configuración de Supabase para MegaOfertas - * - * IMPORTANTE: Estas claves SON PÚBLICAS y seguras para usar en el frontend - * siempre que tengas Row Level Security (RLS) activado en tus tablas. - */ - -window.SUPABASE_CONFIG = { - url: 'https://hkgdbealspivhkmvtqcv.supabase.co', - anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhrZ2RiZWFsc3BpdmhrbXZ0cWN2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUzNzg4NTYsImV4cCI6MjA5MDk1NDg1Nn0.nd_JyTHflGDTPkluZ-J4Al9zvD30zkRN_Xy4SUrTMUw' -}; - -// Inicializar cliente de Supabase (se cargará desde CDN) -window.addEventListener('DOMContentLoaded', () => { - if (typeof supabase !== 'undefined') { - window.sbClient = supabase.createClient( - window.SUPABASE_CONFIG.url, - window.SUPABASE_CONFIG.anonKey - ); - console.log('✅ Supabase inicializado correctamente'); - } else { - console.warn('⚠️ Cliente de Supabase no disponible. Verifica que el script se cargó.'); - } -}); diff --git a/supabase-schema.sql b/supabase-schema.sql deleted file mode 100644 index 9072015..0000000 --- a/supabase-schema.sql +++ /dev/null @@ -1,147 +0,0 @@ --- ============================================ --- SCRIPT SQL PARA CONFIGURAR SUPABASE --- MegaOfertas - Tienda Segura --- ============================================ - --- 1. CREAR TABLAS --- ============================================ - --- Tabla de categorías -CREATE TABLE IF NOT EXISTS categories ( - id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, - name TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Tabla de productos -CREATE TABLE IF NOT EXISTS products ( - id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, - code TEXT UNIQUE, - name TEXT NOT NULL, - price NUMERIC(10,2) NOT NULL DEFAULT 0, - currency TEXT NOT NULL DEFAULT 'USD', - category_id TEXT REFERENCES categories(id), - img TEXT, - img_file_name TEXT, - description TEXT DEFAULT '', - featured BOOLEAN DEFAULT FALSE, - stock INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Tabla de pedidos (para futuro uso con Edge Functions) -CREATE TABLE IF NOT EXISTS orders ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - customer_name TEXT NOT NULL, - customer_email TEXT NOT NULL, - customer_phone TEXT, - customer_address TEXT NOT NULL, - total_amount NUMERIC(10,2) NOT NULL, - status TEXT DEFAULT 'pending', - items JSONB NOT NULL DEFAULT '[]', - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 2. ÍNDICES PARA MEJORAR PERFORMANCE --- ============================================ - -CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id); -CREATE INDEX IF NOT EXISTS idx_products_featured ON products(featured); -CREATE INDEX IF NOT EXISTS idx_products_price ON products(price); -CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status); -CREATE INDEX IF NOT EXISTS idx_orders_created ON orders(created_at); - --- 3. ROW LEVEL SECURITY (RLS) - CRÍTICO PARA SEGURIDAD --- ============================================ - --- Activar RLS en todas las tablas -ALTER TABLE categories ENABLE ROW LEVEL SECURITY; -ALTER TABLE products ENABLE ROW LEVEL SECURITY; -ALTER TABLE orders ENABLE ROW LEVEL SECURITY; - --- POLÍTICAS DE SEGURIDAD: - --- Categorías: TODOS pueden leer, solo admin autenticado puede escribir -CREATE POLICY "Cualquiera puede ver categorías" - ON categories FOR SELECT - USING (true); - -CREATE POLICY "Solo admin autenticado puede modificar categorías" - ON categories FOR ALL - USING (auth.role() = 'authenticated') - WITH CHECK (auth.role() = 'authenticated'); - --- Productos: TODOS pueden leer, solo admin autenticado puede escribir -CREATE POLICY "Cualquiera puede ver productos" - ON products FOR SELECT - USING (true); - -CREATE POLICY "Solo admin autenticado puede modificar productos" - ON products FOR ALL - USING (auth.role() = 'authenticated') - WITH CHECK (auth.role() = 'authenticated'); - --- Pedidos: Solo admin puede ver, cualquier usuario puede crear -CREATE POLICY "Solo admin puede ver pedidos" - ON orders FOR SELECT - USING (auth.role() = 'authenticated'); - -CREATE POLICY "Cualquiera puede crear pedidos" - ON orders FOR INSERT - WITH CHECK (true); - --- 4. TRIGGERS PARA ACTUALIZAR FECHAS --- ============================================ - --- Función para actualizar updated_at -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Trigger para categorías -CREATE TRIGGER update_categories_updated_at - BEFORE UPDATE ON categories - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column(); - --- Trigger para productos -CREATE TRIGGER update_products_updated_at - BEFORE UPDATE ON products - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column(); - --- 5. DATOS DE EJEMPLO (OPCIONAL) --- ============================================ - --- Insertar categorías por defecto -INSERT INTO categories (id, name) VALUES - ('cat-tv-001', 'TV y video'), - ('cat-lav-001', 'Lavado'), - ('cat-ref-001', 'Refrigeración'), - ('cat-coc-001', 'Cocina'), - ('cat-cli-001', 'Climatización') -ON CONFLICT (id) DO NOTHING; - --- Insertar productos de ejemplo -INSERT INTO products (id, code, name, price, currency, category_id, img, description, featured, stock) VALUES - ('prod-001', 'CP-REF-001', 'Smart TV 50''''', 499, 'USD', 'cat-tv-001', 'https://placehold.co/480x320/141414/dc2626?text=TV+50', 'Panel 4K UHD, HDR y smart TV integrado. Ideal para salón.', true, null), - ('prod-002', 'CP-NEV-001', 'Refrigerador no frost', 789, 'USD', 'cat-ref-001', 'https://placehold.co/480x320/141414/dc2626?text=Nevera', 'No frost, dispensador y gran capacidad familiar.', false, null), - ('prod-003', 'CP-LAV-001', 'Lavadora 9 kg', 329, 'USD', 'cat-lav-001', 'https://placehold.co/480x320/141414/dc2626?text=Lavadora', 'Carga frontal, ahorro energético y múltiples programas.', false, 12), - ('prod-004', 'CP-MIC-001', 'Microondas digital', 89, 'USD', 'cat-coc-001', 'https://placehold.co/480x320/141414/dc2626?text=Microondas', '25 L, grill, programas automáticos y panel táctil.', false, 20), - ('prod-005', 'CP-CLI-001', 'Aire split 3500 frigorías', 459, 'USD', 'cat-cli-001', 'https://placehold.co/480x320/141414/dc2626?text=Aire+A/C', 'Frío/calor, eficiencia A, kit de instalación básico.', false, 8) -ON CONFLICT (id) DO NOTHING; - --- ============================================ --- INSTRUCCIONES POST-INSTALACIÓN: --- ============================================ --- 1. Ejecutar este script en el SQL Editor de Supabase --- 2. Ir a Authentication > Users y crear usuario admin --- 3. Configurar email y contraseña en admin-auth.js --- 4. Probar login en /admin.html --- ============================================