""" GKACHELE™ SaaS PageBuilder - Sistema Profesional © 2025 GKACHELE™. Todos los derechos reservados. Desarrollado desde noviembre 2025 por GKACHELE Código propiedad de GKACHELE © 2025 - Prohibida su reproducción sin autorización """ import os import json import sqlite3 import secrets from datetime import datetime from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_from_directory from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__, template_folder='templates', static_folder='static') app.secret_key = 'demo-secret-key-2025' app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Manejador de errores global - siempre devolver JSON @app.errorhandler(500) def handle_500(e): print(f"❌ Error 500: {e}") import traceback traceback.print_exc() # Asegurar que siempre devuelva JSON response = jsonify({'success': False, 'error': 'Error interno del servidor'}) response.status_code = 500 return response @app.errorhandler(404) def handle_404(e): response = jsonify({'success': False, 'error': 'No encontrado'}) response.status_code = 404 return response @app.errorhandler(403) def handle_403(e): response = jsonify({'success': False, 'error': 'No autorizado'}) response.status_code = 403 return response # Manejador para excepciones no capturadas - FORZAR JSON @app.errorhandler(Exception) def handle_exception(e): print(f"❌❌❌ EXCEPCIÓN NO CAPTURADA: {e}") import traceback traceback.print_exc() # SIEMPRE devolver JSON, nunca HTML response = jsonify({'success': False, 'error': f'Error: {str(e)}'}) response.status_code = 500 response.headers['Content-Type'] = 'application/json' return response # Forzar JSON en TODAS las rutas /register, /login, /api/* @app.before_request def before_request(): # Si es POST a /register o /login, forzar JSON if request.method == 'POST' and (request.path.startswith('/register') or request.path.startswith('/login') or request.path.startswith('/api/')): # Asegurar que acepta JSON if not request.is_json and request.content_type and 'application/json' in request.content_type: try: request.get_json(force=True) except: pass # Directorios BASE_DIR = os.path.dirname(__file__) DATABASE_DIR = os.path.join(BASE_DIR, 'database') SITES_DIR = os.path.join(BASE_DIR, 'sites') THEMES_DIR = os.path.join(BASE_DIR, 'themes') STATIC_DIR = os.path.join(BASE_DIR, 'static') UPLOADS_DIR = os.path.join(BASE_DIR, 'uploads') # Crear directorios # NOTA: Ya no se crean bases de datos separadas (cliente-X.db) # TODO está en main.db (sistema multi-tenant) for d in [DATABASE_DIR, SITES_DIR, THEMES_DIR, UPLOADS_DIR]: os.makedirs(d, exist_ok=True) # DB principal MAIN_DB = os.path.join(DATABASE_DIR, 'main.db') def init_db(): """Inicializar base de datos principal - TODO EN UNA SOLA DB (sistema multi-tenant)""" try: conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Usuarios (todos los clientes en una sola tabla) c.execute('''CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, plan TEXT DEFAULT 'base', rubro TEXT DEFAULT 'gimnasio', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )''') # Sitios (todos los sitios, filtrados por user_id) c.execute('''CREATE TABLE IF NOT EXISTS sites ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, slug TEXT UNIQUE NOT NULL, theme TEXT DEFAULT 'default', status TEXT DEFAULT 'draft', content_json TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) )''') # Solicitudes c.execute('''CREATE TABLE IF NOT EXISTS requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, site_id INTEGER NOT NULL, user_id INTEGER NOT NULL, status TEXT DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (site_id) REFERENCES sites(id), FOREIGN KEY (user_id) REFERENCES users(id) )''') # Media (fotos, imágenes del cliente) c.execute('''CREATE TABLE IF NOT EXISTS media ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, site_id INTEGER NOT NULL, filename TEXT NOT NULL, filepath TEXT NOT NULL, file_type TEXT DEFAULT 'image', uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (site_id) REFERENCES sites(id) )''') # Content (contenido por sección, filtrado por user_id y site_id) c.execute('''CREATE TABLE IF NOT EXISTS content ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, site_id INTEGER NOT NULL, section TEXT NOT NULL, data_json TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (site_id) REFERENCES sites(id) )''') # Settings (configuraciones por usuario/sitio) c.execute('''CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, site_id INTEGER, key TEXT NOT NULL, value TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (site_id) REFERENCES sites(id), UNIQUE(user_id, site_id, key) )''') # Menus (sistema de menús dinámicos) c.execute('''CREATE TABLE IF NOT EXISTS menus ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, site_id INTEGER NOT NULL, location TEXT NOT NULL, title TEXT NOT NULL, url TEXT NOT NULL, order_index INTEGER DEFAULT 0, parent_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (site_id) REFERENCES sites(id), FOREIGN KEY (parent_id) REFERENCES menus(id) )''') # Widgets (áreas de widgets dinámicos) c.execute('''CREATE TABLE IF NOT EXISTS widgets ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, site_id INTEGER NOT NULL, area TEXT NOT NULL, type TEXT NOT NULL, title TEXT, content TEXT, order_index INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (site_id) REFERENCES sites(id) )''') conn.commit() conn.close() print("✅ Base de datos inicializada (multi-tenant - todos los clientes en main.db)") except Exception as e: print(f"❌ Error inicializando DB: {e}") import traceback traceback.print_exc() init_db() # ============================================================================ # FUNCIONES HELPER - GKACHELE™ Template System # ============================================================================ def get_site_menus(site_id, user_id): """Obtener menús del sitio organizados por ubicación""" conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('''SELECT location, title, url, order_index, parent_id FROM menus WHERE site_id = ? AND user_id = ? ORDER BY location, order_index''', (site_id, user_id)) rows = c.fetchall() conn.close() menus = {'header': [], 'footer': [], 'sidebar': []} for row in rows: location, title, url, order_index, parent_id = row menu_item = { 'title': title, 'url': url, 'order': order_index, 'parent_id': parent_id } if location in menus: menus[location].append(menu_item) return menus def get_site_widgets(site_id, user_id, area='sidebar'): """Obtener widgets del sitio por área""" conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('''SELECT type, title, content, order_index FROM widgets WHERE site_id = ? AND user_id = ? AND area = ? ORDER BY order_index''', (site_id, user_id, area)) rows = c.fetchall() conn.close() widgets = [] for row in rows: widget_type, title, content, order_index = row widgets.append({ 'type': widget_type, 'title': title, 'content': content, 'order': order_index }) return widgets def render_gkachele_template(theme, content, site_id=None, user_id=None): """Renderizar template usando estructura GKACHELE (header.php, footer.php, sidebar.php)""" # Obtener menús y widgets si hay site_id menus = {'header': [], 'footer': [], 'sidebar': []} widgets = [] if site_id and user_id: try: menus = get_site_menus(site_id, user_id) widgets = get_site_widgets(site_id, user_id) except Exception as e: print(f"⚠️ Error obteniendo menús/widgets: {e}") # Cargar template del tema PRIMERO (es el contenido principal) theme_template = '' theme_path = os.path.join(THEMES_DIR, theme, 'template.html') if os.path.exists(theme_path): with open(theme_path, 'r', encoding='utf-8') as f: theme_template = f.read() else: # Si no existe template del tema, usar template básico theme_template = '''

{{ hero_title or 'Bienvenido' }}

{{ hero_description or 'Descripción del sitio' }}

''' # Cargar header, footer, sidebar header = '' footer = '' sidebar = '' header_path = os.path.join(THEMES_DIR, '_gkachele', 'header.php') footer_path = os.path.join(THEMES_DIR, '_gkachele', 'footer.php') sidebar_path = os.path.join(THEMES_DIR, '_gkachele', 'sidebar.php') if os.path.exists(header_path): with open(header_path, 'r', encoding='utf-8') as f: header = f.read() else: # Header básico si no existe header = ''' {{ site_name or 'GKACHELE Site' }}
''' if os.path.exists(footer_path): with open(footer_path, 'r', encoding='utf-8') as f: footer = f.read() else: footer = '''
''' if os.path.exists(sidebar_path): with open(sidebar_path, 'r', encoding='utf-8') as f: sidebar = f.read() # Preparar contexto completo template_data = { 'site_name': content.get('site_name', 'GKACHELE Site'), 'hero_title': content.get('hero_title', 'Bienvenido'), 'hero_description': content.get('hero_description', ''), 'colors': content.get('colors', {'primary': '#d32f2f', 'secondary': '#ff6f00', 'text': '#2c2c2c'}), 'typography': content.get('typography', {'font_family': 'Roboto'}), 'menus': menus, 'widgets': widgets, **content # Incluir todo el contenido adicional } # Renderizar usando Jinja2 from jinja2 import Template # Construir template completo: header + contenido del tema + sidebar + footer full_template = header + theme_template + sidebar + footer template = Template(full_template) return template.render(**template_data) # ============================================================================ # RUTAS PÚBLICAS # ============================================================================ @app.route('/') def landing(): """Landing page""" return render_template('landing_real.html') @app.route('/register', methods=['GET', 'POST']) def register(): """Registro - Sistema Simple y Profesional""" if request.method == 'POST': try: # Datos JSON data = request.get_json() if request.is_json else (request.form.to_dict() if request.form else {}) if not data: return jsonify({'success': False, 'error': 'Sin datos'}), 400 email = str(data.get('email', '')).strip() password = str(data.get('password', '')).strip() plan = str(data.get('plan', 'base')) rubro = str(data.get('rubro', 'gimnasio')) # Validar if not email or '@' not in email: return jsonify({'success': False, 'error': 'Email inválido'}), 400 if not password: return jsonify({'success': False, 'error': 'Contraseña requerida'}), 400 # DB conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Usuario try: c.execute('INSERT INTO users (email, password, plan, rubro) VALUES (?, ?, ?, ?)', (email, generate_password_hash(password), plan, rubro)) user_id = c.lastrowid except sqlite3.IntegrityError: conn.close() return jsonify({'success': False, 'error': 'Email ya existe'}), 400 # Tema import random theme = 'default' if rubro == 'restaurante': theme = random.choice(['restaurante-moderno', 'restaurante-elegante']) elif rubro in ['gimnasio', 'gimnasios']: theme = 'gimnasio-claro' # Sitio site_name = email.split('@')[0].title() content = json.dumps({ 'site_name': site_name + ' Site', 'hero_title': 'Bienvenido', 'hero_description': '', 'colors': {'primary': '#d32f2f', 'secondary': '#ff6f00', 'text': '#2c2c2c'}, 'typography': {'font_family': 'Roboto'} }) slug = f'site-{secrets.token_hex(4)}' c.execute('INSERT INTO sites (user_id, slug, theme, content_json) VALUES (?, ?, ?, ?)', (user_id, slug, theme, content)) site_id = c.lastrowid # Crear menús por defecto (header, footer) default_menus = [ ('header', 'Inicio', '#inicio', 0, None), ('header', 'Menú', '#menu', 1, None), ('header', 'Horarios', '#horarios', 2, None), ('header', 'Reservas', '#reservas', 3, None), ('header', 'Contacto', '#contacto', 4, None), ('footer', 'Inicio', '#inicio', 0, None), ('footer', 'Contacto', '#contacto', 1, None), ] for location, title, url, order_idx, parent_id in default_menus: c.execute('''INSERT INTO menus (user_id, site_id, location, title, url, order_index, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?)''', (user_id, site_id, location, title, url, order_idx, parent_id)) conn.commit() conn.close() # ✅ TODO está en main.db - no se necesitan bases separadas # El cliente accede a /dashboard (como wp-admin) y solo ve SUS datos filtrados por user_id # NO crear sesión automáticamente - el usuario debe hacer login # ÉXITO - Redirigir al login para que inicie sesión print(f"✅ Usuario registrado: user_id={user_id}, site_id={site_id}") return jsonify({'success': True, 'message': 'Registro exitoso. Por favor inicia sesión.', 'redirect': '/login'}) except Exception as e: import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 # GET return render_template('register.html', plan=request.args.get('plan', 'base'), rubro=request.args.get('rubro', 'gimnasio')) @app.route('/login', methods=['GET', 'POST']) def login(): """Login""" if request.method == 'POST': try: data = request.get_json() if not data: return jsonify({'success': False, 'error': 'No se recibieron datos'}), 400 email = data.get('email') password = data.get('password') if not email or not password: return jsonify({'success': False, 'error': 'Email y contraseña son requeridos'}), 400 conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT id, password FROM users WHERE email = ?', (email,)) user = c.fetchone() conn.close() if user and check_password_hash(user[1], password): session['user_id'] = user[0] print(f"✅ Login exitoso: user_id={user[0]}") # Después del login, redirigir al dashboard (el usuario puede ir al customizer desde ahí) return jsonify({'success': True, 'redirect': '/dashboard'}) return jsonify({'success': False, 'error': 'Credenciales inválidas'}), 401 except Exception as e: print(f"❌ Error en login: {e}") import traceback traceback.print_exc() return jsonify({'success': False, 'error': f'Error al iniciar sesión: {str(e)}'}), 500 return render_template('login.html') @app.route('/logout') def logout(): session.pop('user_id', None) return redirect(url_for('landing')) # ============================================================================ # DASHBOARD CLIENTE # ============================================================================ @app.route('/dashboard') def dashboard(): """Panel del cliente""" if 'user_id' not in session: return redirect(url_for('login')) user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Info del usuario c.execute('SELECT email, plan FROM users WHERE id = ?', (user_id,)) user_info = c.fetchone() # Sitios c.execute('SELECT id, slug, theme, status, created_at FROM sites WHERE user_id = ?', (user_id,)) sites = [{'id': r[0], 'slug': r[1], 'theme': r[2], 'status': r[3], 'created_at': r[4]} for r in c.fetchall()] # Contar media c.execute('SELECT COUNT(*) FROM media WHERE user_id = ?', (user_id,)) media_count = c.fetchone()[0] conn.close() return render_template('dashboard.html', sites=sites, user_email=user_info[0] if user_info else '', user_plan=user_info[1] if user_info else 'base', media_count=media_count) @app.route('/dashboard/admin') def client_admin(): """Admin del cliente - gestionar media, config, etc.""" if 'user_id' not in session: return redirect(url_for('login')) user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Info del usuario c.execute('SELECT email, plan FROM users WHERE id = ?', (user_id,)) user_info = c.fetchone() # Sitios del usuario c.execute('SELECT id, slug, theme, status FROM sites WHERE user_id = ?', (user_id,)) sites = [{'id': r[0], 'slug': r[1], 'theme': r[2], 'status': r[3]} for r in c.fetchall()] conn.close() return render_template('client_admin.html', user_email=user_info[0] if user_info else '', user_plan=user_info[1] if user_info else 'base', sites=sites) @app.route('/dashboard/create', methods=['GET', 'POST']) def create_site(): """Crear nuevo sitio""" if 'user_id' not in session: return redirect(url_for('login')) if request.method == 'POST': data = request.get_json() user_id = session['user_id'] slug = data.get('slug', f'site-{secrets.token_hex(4)}') theme = data.get('theme', 'default') # Contenido inicial content = { 'site_name': data.get('site_name', 'Mi Sitio'), 'hero_title': data.get('hero_title', 'Bienvenido'), 'hero_description': data.get('hero_description', ''), 'colors': { 'primary': '#ff4d4d', 'secondary': '#1a1a1a', 'text': '#333333' }, 'typography': { 'font_family': 'Arial' } } conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('INSERT INTO sites (user_id, slug, theme, content_json) VALUES (?, ?, ?, ?)', (user_id, slug, theme, json.dumps(content))) site_id = c.lastrowid conn.commit() conn.close() # ✅ TODO está en main.db - no se necesitan bases separadas return jsonify({'success': True, 'site_id': site_id, 'slug': slug}) return render_template('create_site.html') # ============================================================================ # CUSTOMIZER (Sistema de Personalización) # ============================================================================ @app.route('/customizer/') def customizer(site_id): """Customizer: Sidebar + Preview""" # Verificar que el sitio existe conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,)) site = c.fetchone() conn.close() if not site: return "Sitio no encontrado", 404 # Si hay sesión, verificar que pertenece al usuario if 'user_id' in session and site[0] != session['user_id']: return "No autorizado", 403 content = json.loads(site[3]) if site[3] else {} theme = site[2] # Cargar template del tema si existe theme_template = None theme_path = os.path.join(THEMES_DIR, theme, 'template.html') if os.path.exists(theme_path): with open(theme_path, 'r', encoding='utf-8') as f: theme_template = f.read() return render_template('customizer.html', site_id=site_id, slug=site[1], theme=theme, content=content, theme_template=theme_template) @app.route('/api/customizer/save', methods=['POST']) def save_customizer(): """Guardar cambios del customizer""" data = request.get_json() site_id = data.get('site_id') content = data.get('content') conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(content), site_id)) conn.commit() conn.close() return jsonify({'success': True}) @app.route('/api/customizer/preview-frame/') def preview_frame(site_id): """Frame del preview - renderiza el template completo con estructura GKACHELE""" # Obtener datos del sitio conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT theme, content_json, user_id FROM sites WHERE id = ?', (site_id,)) site = c.fetchone() conn.close() if not site: return "Sitio no encontrado", 404 theme = site[0] content = json.loads(site[1]) if site[1] else {} user_id = site[2] # Usar función helper para renderizar con estructura GKACHELE try: rendered = render_gkachele_template(theme, content, site_id, user_id) print(f"✅ Template renderizado correctamente para site_id={site_id}, theme={theme}") return rendered except Exception as e: import traceback print(f"❌ Error renderizando template: {e}") traceback.print_exc() # Fallback: cargar template directamente si existe theme_path = os.path.join(THEMES_DIR, theme, 'template.html') if os.path.exists(theme_path): with open(theme_path, 'r', encoding='utf-8') as f: template_str = f.read() from jinja2 import Template template = Template(template_str) template_data = { 'site_name': content.get('site_name', 'GKACHELE Site'), 'hero_title': content.get('hero_title', 'Bienvenido'), 'hero_description': content.get('hero_description', ''), 'colors': content.get('colors', {'primary': '#d32f2f', 'secondary': '#ff6f00', 'text': '#2c2c2c'}), 'typography': content.get('typography', {'font_family': 'Roboto'}), **content } return template.render(**template_data) # Fallback final a preview simple return f""" Preview

{content.get('hero_title', 'Título')}

{content.get('hero_description', 'Descripción')}

© 2025 GKACHELE™. Todos los derechos reservados.

""" # ============================================================================ # SOLICITUDES # ============================================================================ @app.route('/dashboard/submit/', methods=['POST']) def submit_site(site_id): """Cliente envía sitio para aprobación""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Cambiar status a pending c.execute('UPDATE sites SET status = ? WHERE id = ?', ('pending', site_id)) # Crear solicitud c.execute('INSERT INTO requests (site_id, user_id) VALUES (?, ?)', (site_id, session['user_id'])) conn.commit() conn.close() return jsonify({'success': True}) # ============================================================================ # ADMIN # ============================================================================ @app.route('/admin') def admin(): """Panel admin""" # Admin simple: user_id = 1 if 'user_id' not in session or session['user_id'] != 1: return "Solo admin", 403 conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Solicitudes pendientes c.execute('''SELECT r.id, r.site_id, r.status, s.slug, u.email, r.created_at FROM requests r JOIN sites s ON r.site_id = s.id JOIN users u ON r.user_id = u.id WHERE r.status = 'pending' ORDER BY r.created_at DESC''') requests = [{'id': r[0], 'site_id': r[1], 'status': r[2], 'slug': r[3], 'email': r[4], 'created_at': r[5]} for r in c.fetchall()] # Todos los sitios c.execute('SELECT id, slug, theme, status, user_id FROM sites') sites = [{'id': r[0], 'slug': r[1], 'theme': r[2], 'status': r[3], 'user_id': r[4]} for r in c.fetchall()] conn.close() return render_template('admin.html', requests=requests, sites=sites) @app.route('/admin/approve/', methods=['POST']) def approve_site(request_id): """Admin aprueba sitio""" if 'user_id' not in session or session['user_id'] != 1: return jsonify({'error': 'No autorizado'}), 403 conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Obtener site_id c.execute('SELECT site_id FROM requests WHERE id = ?', (request_id,)) result = c.fetchone() if not result: conn.close() return jsonify({'error': 'Solicitud no encontrada'}), 404 site_id = result[0] # Aprobar: cambiar status a approved y publicar c.execute('UPDATE sites SET status = ? WHERE id = ?', ('published', site_id)) c.execute('UPDATE requests SET status = ? WHERE id = ?', ('approved', request_id)) conn.commit() conn.close() return jsonify({'success': True}) # ============================================================================ # SITIO PÚBLICO # ============================================================================ @app.route('/site/') def public_site(slug): """Sitio público del cliente - usando estructura GKACHELE""" conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT id, theme, content_json, status, user_id FROM sites WHERE slug = ?', (slug,)) site = c.fetchone() conn.close() if not site or site[3] != 'published': return "Sitio no encontrado o no publicado", 404 site_id = site[0] theme = site[1] content = json.loads(site[2]) if site[2] else {} user_id = site[4] # Renderizar con estructura GKACHELE try: return render_gkachele_template(theme, content, site_id, user_id) except Exception as e: import traceback traceback.print_exc() return render_template('public_site.html', content=content, theme=theme) # ============================================================================ # MAIN # ============================================================================ # ============================================================================ # API ADMIN CLIENTE # ============================================================================ @app.route('/api/admin/upload', methods=['POST']) def upload_media(): """Subir imágenes""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 user_id = session['user_id'] files = request.files.getlist('files') if not files: return jsonify({'error': 'No hay archivos'}), 400 uploaded = [] for file in files: if file.filename: filename = f"{user_id}_{secrets.token_hex(8)}_{file.filename}" filepath = os.path.join(UPLOADS_DIR, filename) file.save(filepath) # Guardar en DB conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT id FROM sites WHERE user_id = ? LIMIT 1', (user_id,)) site = c.fetchone() site_id = site[0] if site else None c.execute('INSERT INTO media (user_id, site_id, filename, filepath, file_type) VALUES (?, ?, ?, ?, ?)', (user_id, site_id, filename, filepath, 'image')) conn.commit() conn.close() uploaded.append(filename) return jsonify({'success': True, 'files': uploaded}) @app.route('/api/admin/media') def get_media(): """Obtener media del usuario""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT id, filename, filepath, uploaded_at FROM media WHERE user_id = ? ORDER BY uploaded_at DESC', (user_id,)) media = [{'id': r[0], 'filename': r[1], 'filepath': r[2], 'uploaded_at': r[3]} for r in c.fetchall()] conn.close() return jsonify({'success': True, 'media': media}) @app.route('/api/admin/media/', methods=['DELETE']) def delete_media(media_id): """Eliminar media""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Verificar que pertenece al usuario c.execute('SELECT filepath FROM media WHERE id = ? AND user_id = ?', (media_id, user_id)) media = c.fetchone() if media: # Eliminar archivo if os.path.exists(media[0]): os.remove(media[0]) # Eliminar de DB c.execute('DELETE FROM media WHERE id = ?', (media_id,)) conn.commit() conn.close() return jsonify({'success': True}) @app.route('/api/admin/settings', methods=['POST']) def update_settings(): """Actualizar configuración del usuario""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 data = request.get_json() user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() if data.get('password'): c.execute('UPDATE users SET password = ? WHERE id = ?', (generate_password_hash(data['password']), user_id)) conn.commit() conn.close() return jsonify({'success': True}) @app.route('/uploads/') def serve_upload(filename): """Servir archivos subidos""" return send_from_directory(UPLOADS_DIR, filename) # ============================================================================ # API MENÚS Y WIDGETS # ============================================================================ @app.route('/api/menus/', methods=['GET']) def get_menus(site_id): """Obtener menús del sitio""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 user_id = session['user_id'] menus = get_site_menus(site_id, user_id) return jsonify({'success': True, 'menus': menus}) @app.route('/api/menus/', methods=['POST']) def save_menu(site_id): """Guardar/actualizar menú""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 data = request.get_json() user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # Eliminar menús existentes del sitio c.execute('DELETE FROM menus WHERE site_id = ? AND user_id = ?', (site_id, user_id)) # Insertar nuevos menús for menu_item in data.get('menus', []): c.execute('''INSERT INTO menus (user_id, site_id, location, title, url, order_index, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?)''', (user_id, site_id, menu_item['location'], menu_item['title'], menu_item['url'], menu_item.get('order', 0), menu_item.get('parent_id'))) conn.commit() conn.close() return jsonify({'success': True}) @app.route('/api/widgets/', methods=['GET']) def get_widgets(site_id): """Obtener widgets del sitio""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 user_id = session['user_id'] area = request.args.get('area', 'sidebar') widgets = get_site_widgets(site_id, user_id, area) return jsonify({'success': True, 'widgets': widgets}) @app.route('/api/widgets/', methods=['POST']) def save_widget(site_id): """Guardar/actualizar widget""" if 'user_id' not in session: return jsonify({'error': 'No autorizado'}), 401 data = request.get_json() user_id = session['user_id'] conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('''INSERT INTO widgets (user_id, site_id, area, type, title, content, order_index) VALUES (?, ?, ?, ?, ?, ?, ?)''', (user_id, site_id, data.get('area', 'sidebar'), data.get('type', 'text'), data.get('title'), data.get('content'), data.get('order', 0))) conn.commit() conn.close() return jsonify({'success': True}) # ============================================================================ # MAIN # ============================================================================ # Inicializar DB al importar (con manejo de errores) try: init_db() except Exception as e: print(f"⚠️ Error en init_db: {e}") # Continuar aunque falle, se creará cuando se use if __name__ == '__main__': port = int(os.environ.get('PORT', 5001)) print("🚀 Demo SaaS iniciado") print(f"📍 http://localhost:{port}") app.run(debug=True, host='0.0.0.0', port=port)