""" app.py — Lista de la Compra Inteligente Flask app con autenticacion de sesion. Arrancar en desarrollo: python app.py En produccion usa gunicorn detras de nginx: gunicorn -w 2 -b 127.0.0.1:5000 app:app """ import os import json import subprocess import sys from pathlib import Path from functools import wraps from datetime import timedelta from flask import ( Flask, render_template, request, redirect, url_for, session, jsonify, abort, send_from_directory ) from werkzeug.security import generate_password_hash, check_password_hash # ----------------------------------------------------------------------- # Configuracion # ----------------------------------------------------------------------- BASE_DIR = Path(__file__).parent DATOS_JSON = BASE_DIR / "datos.json" USERS_FILE = BASE_DIR / "users.json" TICKETS_DIR = BASE_DIR / "tickets" app = Flask(__name__, template_folder="templates", static_folder="static") app.secret_key = os.environ.get("SECRET_KEY", "cambia-esto-en-produccion-!!!!") app.permanent_session_lifetime = timedelta(days=30) # ----------------------------------------------------------------------- # Gestion de usuarios (users.json — NO subir al repo) # ----------------------------------------------------------------------- def cargar_usuarios(): if not USERS_FILE.exists(): return {} with open(USERS_FILE, encoding="utf-8") as f: return json.load(f) def guardar_usuarios(users): with open(USERS_FILE, "w", encoding="utf-8") as f: json.dump(users, f, indent=2, ensure_ascii=False) def inicializar_admin(): """Crea el usuario admin la primera vez si no existe users.json.""" if USERS_FILE.exists(): return pwd = os.environ.get("ADMIN_PASSWORD", "cambia-esta-password") users = { "admin": { "password_hash": generate_password_hash(pwd), "nombre": "Admin" } } guardar_usuarios(users) print(f"[init] Usuario admin creado. Password: {pwd}") print(f"[init] Cambialo en users.json o con ADMIN_PASSWORD en el entorno.") # ----------------------------------------------------------------------- # Decorador de autenticacion # ----------------------------------------------------------------------- def login_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get("usuario"): return redirect(url_for("login", next=request.path)) return f(*args, **kwargs) return decorated # ----------------------------------------------------------------------- # Rutas de autenticacion # ----------------------------------------------------------------------- @app.route("/login", methods=["GET", "POST"]) def login(): error = None if request.method == "POST": usuario = request.form.get("usuario", "").strip().lower() password = request.form.get("password", "") users = cargar_usuarios() user = users.get(usuario) if user and check_password_hash(user["password_hash"], password): session.permanent = True session["usuario"] = usuario session["nombre"] = user.get("nombre", usuario) next_url = request.form.get("next") or url_for("index") return redirect(next_url) error = "Usuario o contrasena incorrectos" return render_template("login.html", error=error, next=request.args.get("next", "")) @app.route("/logout") def logout(): session.clear() return redirect(url_for("login")) # ----------------------------------------------------------------------- # Pagina principal # ----------------------------------------------------------------------- @app.route("/") @login_required def index(): return render_template("index.html", nombre=session.get("nombre", "")) # ----------------------------------------------------------------------- # API: datos de predicciones # ----------------------------------------------------------------------- @app.route("/api/datos") @login_required def api_datos(): if not DATOS_JSON.exists(): return jsonify({"error": "datos.json no generado", "predicciones": []}), 404 with open(DATOS_JSON, encoding="utf-8") as f: datos = json.load(f) return jsonify(datos) # ----------------------------------------------------------------------- # API: forzar regeneracion del pipeline # ----------------------------------------------------------------------- @app.route("/api/regenerar", methods=["POST"]) @login_required def api_regenerar(): try: subprocess.run( [sys.executable, str(BASE_DIR / "autocompra7.py")], cwd=str(BASE_DIR), check=True, capture_output=True, timeout=120 ) subprocess.run( [sys.executable, str(BASE_DIR / "generar_lista.py")], cwd=str(BASE_DIR), check=True, capture_output=True, timeout=30 ) return jsonify({"ok": True, "mensaje": "Pipeline ejecutado correctamente"}) except subprocess.CalledProcessError as e: return jsonify({"ok": False, "mensaje": str(e)}), 500 except subprocess.TimeoutExpired: return jsonify({"ok": False, "mensaje": "Timeout al ejecutar el pipeline"}), 500 # ----------------------------------------------------------------------- # Archivos estaticos de tickets (solo autenticados) # ----------------------------------------------------------------------- @app.route("/tickets/") @login_required def ticket_file(filename): return send_from_directory(str(TICKETS_DIR), filename) # ----------------------------------------------------------------------- # Gestion de usuarios (solo admin) # ----------------------------------------------------------------------- @app.route("/admin/usuarios") @login_required def admin_usuarios(): if session.get("usuario") != "admin": abort(403) users = cargar_usuarios() lista = [{"usuario": k, "nombre": v.get("nombre", k)} for k, v in users.items()] return render_template("admin_usuarios.html", usuarios=lista) @app.route("/admin/usuarios/crear", methods=["POST"]) @login_required def admin_crear_usuario(): if session.get("usuario") != "admin": abort(403) usuario = request.form.get("usuario", "").strip().lower() nombre = request.form.get("nombre", "").strip() password = request.form.get("password", "") if not usuario or not password: abort(400) users = cargar_usuarios() users[usuario] = { "password_hash": generate_password_hash(password), "nombre": nombre or usuario } guardar_usuarios(users) return redirect(url_for("admin_usuarios")) @app.route("/admin/usuarios/eliminar", methods=["POST"]) @login_required def admin_eliminar_usuario(): if session.get("usuario") != "admin": abort(403) usuario = request.form.get("usuario", "").strip().lower() if usuario == "admin": abort(400) users = cargar_usuarios() users.pop(usuario, None) guardar_usuarios(users) return redirect(url_for("admin_usuarios")) # ----------------------------------------------------------------------- # Entry point # ----------------------------------------------------------------------- if __name__ == "__main__": inicializar_admin() debug = os.environ.get("FLASK_DEBUG", "false").lower() == "true" app.run(host="127.0.0.1", port=5000, debug=debug)