89 lines
2.6 KiB
Python
89 lines
2.6 KiB
Python
#!/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"))
|