202 lines
7.3 KiB
Python
202 lines
7.3 KiB
Python
"""
|
|
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/<path:filename>")
|
|
@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) |