"""
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)