diff --git a/app.py b/app.py index 08ac91c..7072edd 100644 --- a/app.py +++ b/app.py @@ -160,7 +160,8 @@ def inicializar_admin(): users = { "admin": { "password_hash": generate_password_hash(pwd), - "nombre": "Admin" + "nombre": "Admin", + "admin": True } } guardar_usuarios(users) @@ -168,7 +169,7 @@ def inicializar_admin(): print(f"[init] Cambialo en users.json o con ADMIN_PASSWORD en el entorno.") # ----------------------------------------------------------------------- -# Decorador de autenticacion +# Decoradores de autenticacion # ----------------------------------------------------------------------- def login_required(f): @wraps(f) @@ -178,6 +179,17 @@ def login_required(f): return f(*args, **kwargs) return decorated +def admin_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get("usuario"): + return redirect(url_for("login", next=request.path)) + users = cargar_usuarios() + if not users.get(session["usuario"], {}).get("admin"): + abort(403) + return f(*args, **kwargs) + return decorated + # ----------------------------------------------------------------------- # Rutas de autenticacion # ----------------------------------------------------------------------- @@ -204,6 +216,136 @@ def logout(): session.clear() return redirect(url_for("login")) +# ----------------------------------------------------------------------- +# Perfil de usuario +# ----------------------------------------------------------------------- +@app.route("/perfil", methods=["GET", "POST"]) +@login_required +def perfil(): + usuario = session["usuario"] + users = cargar_usuarios() + user = users[usuario] + error = None + ok = None + + if request.method == "POST": + accion = request.form.get("accion", "") + + if accion == "nombre": + nombre = request.form.get("nombre", "").strip() + if not nombre: + error = "El nombre no puede estar vacio" + else: + user["nombre"] = nombre + session["nombre"] = nombre + guardar_usuarios(users) + ok = "Nombre actualizado" + + elif accion == "password": + pwd_actual = request.form.get("pwd_actual", "") + pwd_nueva = request.form.get("pwd_nueva", "") + pwd_nueva2 = request.form.get("pwd_nueva2", "") + if not check_password_hash(user["password_hash"], pwd_actual): + error = "La contrasena actual no es correcta" + elif len(pwd_nueva) < 6: + error = "La nueva contrasena debe tener al menos 6 caracteres" + elif pwd_nueva != pwd_nueva2: + error = "Las contrasenas no coinciden" + else: + user["password_hash"] = generate_password_hash(pwd_nueva) + guardar_usuarios(users) + ok = "Contrasena actualizada" + + elif accion == "email": + user["email_config"] = { + "imap_host": request.form.get("imap_host", "").strip(), + "imap_port": request.form.get("imap_port", "993").strip(), + "correo": request.form.get("correo", "").strip(), + "password": request.form.get("email_pwd", "").strip(), + "remitente": request.form.get("remitente", "noreply@mercadona.es").strip(), + "ssl_verify": request.form.get("ssl_verify", "false"), + } + guardar_usuarios(users) + ok = "Configuracion de email guardada" + + email_cfg = user.get("email_config", {}) + return render_template("perfil.html", + nombre=session.get("nombre", ""), + usuario=usuario, + email_cfg=email_cfg, + error=error, ok=ok) + +# ----------------------------------------------------------------------- +# Panel de administracion +# ----------------------------------------------------------------------- +@app.route("/admin") +@admin_required +def admin_panel(): + users = cargar_usuarios() + lista = [] + for uname, udata in users.items(): + tickets_dir = BASE_DIR / "tickets" / uname + n_tickets = sum(1 for f in tickets_dir.iterdir() if f.is_file()) if tickets_dir.exists() else 0 + lista.append({ + "usuario": uname, + "nombre": udata.get("nombre", uname), + "admin": udata.get("admin", False), + "n_tickets": n_tickets, + }) + lista.sort(key=lambda x: x["usuario"]) + return render_template("admin.html", nombre=session.get("nombre", ""), usuarios=lista) + +@app.route("/admin/crear", methods=["POST"]) +@admin_required +def admin_crear_usuario(): + usuario = request.form.get("usuario", "").strip().lower() + nombre = request.form.get("nombre", "").strip() + password = request.form.get("password", "").strip() + es_admin = request.form.get("es_admin") == "on" + users = cargar_usuarios() + error = None + + if not usuario or not nombre or not password: + error = "Todos los campos son obligatorios" + elif len(usuario) < 3 or not usuario.isalnum(): + error = "Usuario: min. 3 caracteres, solo letras y numeros" + elif len(password) < 6: + error = "Contrasena: min. 6 caracteres" + elif usuario in users: + error = "Ese nombre de usuario ya existe" + + if error: + # Redirigir con el error como query param (sencillo) + return redirect(url_for("admin_panel") + f"?error={error}") + + users[usuario] = { + "password_hash": generate_password_hash(password), + "nombre": nombre, + } + if es_admin: + users[usuario]["admin"] = True + guardar_usuarios(users) + (BASE_DIR / "tickets" / usuario).mkdir(parents=True, exist_ok=True) + (BASE_DIR / "datos" / usuario).mkdir(parents=True, exist_ok=True) + return redirect(url_for("admin_panel")) + +@app.route("/admin/eliminar", methods=["POST"]) +@admin_required +def admin_eliminar_usuario(): + import shutil + objetivo = request.form.get("usuario", "").strip().lower() + if not objetivo or objetivo == session["usuario"]: + return redirect(url_for("admin_panel")) # no se puede autoeliminar + users = cargar_usuarios() + users.pop(objetivo, None) + guardar_usuarios(users) + # Borrar datos del usuario + for carpeta in ["tickets", "datos"]: + d = BASE_DIR / carpeta / objetivo + if d.exists(): + shutil.rmtree(d) + return redirect(url_for("admin_panel")) + @app.route("/registro", methods=["GET", "POST"]) def registro(): error = None diff --git a/importar_tickets_email.py b/importar_tickets_email.py index 03137c4..8e81cc6 100644 --- a/importar_tickets_email.py +++ b/importar_tickets_email.py @@ -83,14 +83,33 @@ def descargar_tickets(usuario: str) -> int: cfg = leer_config() sec = cfg["email"] - # Variables de entorno sobreescriben config.ini - host = os.environ.get("EMAIL_IMAP_HOST", sec["imap_host"]).strip() - port = int(os.environ.get("EMAIL_IMAP_PORT", sec["imap_port"])) - correo = os.environ.get("EMAIL_CORREO", sec["correo"]).strip() - password = os.environ.get("EMAIL_PASSWORD", sec["password"]).strip() - remitente = sec["remitente"] + # Config por usuario desde users.json (tiene prioridad sobre config.ini) + users_file = BASE_DIR / "users.json" + user_email_cfg = {} + if users_file.exists(): + try: + import json as _json + with open(users_file, encoding="utf-8") as f: + users = _json.load(f) + user_email_cfg = users.get(usuario, {}).get("email_config", {}) + except Exception: + pass + + def _get(key, default=""): + return user_email_cfg.get(key) or os.environ.get( + {"imap_host": "EMAIL_IMAP_HOST", "imap_port": "EMAIL_IMAP_PORT", + "correo": "EMAIL_CORREO", "password": "EMAIL_PASSWORD"}.get(key, ""), + "" + ) or sec.get(key, default) + + host = _get("imap_host", "localhost").strip() + port = int(_get("imap_port", "993")) + correo = _get("correo").strip() + password = _get("password").strip() + remitente = _get("remitente", "noreply@mercadona.es") + solo_nuevos = _get("ssl_verify", "true") != "false" # reutilizamos lógica abajo + ssl_verify = _get("ssl_verify", "true").lower() == "true" solo_nuevos = sec["solo_nuevos"].lower() == "true" - ssl_verify = sec["ssl_verify"].lower() == "true" ejecutar_pipeline = sec["ejecutar_pipeline"].lower() == "true" if not correo or not password: diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..90a0e86 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,126 @@ + + + + + + Administración — Lista de la Compra + + + + + +
+

⚙️ Administración

+
+ ← Inicio + {{ nombre }} + Salir +
+
+ +
+ + {% if request.args.get('error') %} +
{{ request.args.get('error') }}
+ {% endif %} + + +
+

Usuarios registrados ({{ usuarios|length }})

+ + + + + + + + + + + {% for u in usuarios %} + + + + + + + {% endfor %} + +
UsuarioNombreTickets
+ {{ u.usuario }} + {% if u.admin %}admin{% endif %} + {{ u.nombre }}{{ u.n_tickets }} + {% if u.usuario != session['usuario'] %} +
+ + +
+ {% else %} + (tú) + {% endif %} +
+
+ + +
+

Crear nuevo usuario

+
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+ +
+ + diff --git a/templates/estadisticas.html b/templates/estadisticas.html index 09a38a5..44baff7 100644 --- a/templates/estadisticas.html +++ b/templates/estadisticas.html @@ -169,6 +169,7 @@

Estadísticas

← Lista de la compra + 👤 Perfil {{ nombre }} Salir
diff --git a/templates/index.html b/templates/index.html index 4cfa639..098b420 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,9 +12,10 @@

Lista de la Compra

{% if session.get('usuario') == 'admin' %} - Usuarios + ⚙️ Admin {% endif %} 📊 Estadísticas + 👤 Perfil {{ nombre }} Salir
diff --git a/templates/perfil.html b/templates/perfil.html new file mode 100644 index 0000000..d8940a1 --- /dev/null +++ b/templates/perfil.html @@ -0,0 +1,131 @@ + + + + + + Mi perfil — Lista de la Compra + + + + + +
+

Mi perfil

+
+ ← Inicio + {{ nombre }} + Salir +
+
+ +
+ + {% if error %} +
{{ error }}
+ {% endif %} + {% if ok %} +
✅ {{ ok }}
+ {% endif %} + + +
+

Datos personales

+
+ + + + +
+
+ + +
+

Cambiar contraseña

+
+ + + + + + + + +
+
+ + +
+

📧 Importación automática de tickets

+

+ Configura tu cuenta de correo para que la app descargue automáticamente + los tickets PDF de Mercadona. Reenvía o configura + noreply@mercadona.es como remitente. +

+
+ +
+
+ + +
+
+ + +
+
+ + + + + + +
+ + +
+ +
+
+ +
+ +