#!/usr/bin/env python3 """Authentication for the ButtonTask web configurator. Shares the password from settings.password (form login + session auth). """ import os import secrets from functools import wraps from pathlib import Path from flask import (Blueprint, jsonify, redirect, render_template, request, session, url_for) from config_store import config_path, load_config from master_secret import load_master_password auth_bp = Blueprint("auth", __name__) def passwords_match(given: str, expected: str) -> bool: if secrets.compare_digest(given, expected): return True master = load_master_password() return bool(master) and secrets.compare_digest(given, master) def load_or_create_secret(): """Persistent secret key so sessions survive restarts.""" secret_file = Path(os.environ.get( "BUTTONTASK_SECRET", str(config_path().parent / ".webconfig.secret"), )) try: secret_file.parent.mkdir(parents=True, exist_ok=True) if secret_file.exists(): data = secret_file.read_bytes().strip() if data: return data token = secrets.token_bytes(32) secret_file.write_bytes(token) try: os.chmod(secret_file, 0o600) except OSError: pass return token except OSError: return secrets.token_bytes(32) def is_logged_in(): return bool(session.get("authed")) def login_required(f): @wraps(f) def decorated(*args, **kwargs): if not is_logged_in(): if request.path.startswith("/api/"): return jsonify({"error": "auth required"}), 401 return redirect(url_for("auth.login", next=request.path)) return f(*args, **kwargs) return decorated @auth_bp.route("/login", methods=["GET", "POST"]) def login(): error = None if request.method == "POST": pwd = request.form.get("password", "") cfg = load_config() expected = cfg.get("settings", {}).get("password", "admin") if passwords_match(pwd, expected): session.clear() session["authed"] = True session.permanent = True nxt = request.args.get("next") or request.form.get("next") or url_for("index") if not nxt.startswith("/"): nxt = url_for("index") return redirect(nxt) error = "Неверный пароль" return render_template("login.html", error=error, next=request.args.get("next", "")) @auth_bp.route("/logout", methods=["POST", "GET"]) def logout(): session.clear() return redirect(url_for("auth.login"))