autocompra/app.py

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)