247 lines
8.0 KiB
Python
247 lines
8.0 KiB
Python
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
from flask import Blueprint, current_app, jsonify, request, session
|
|
|
|
from app import db
|
|
from app.models import WebUser
|
|
from config import Config
|
|
from app.routes.auth_decorators import can_access_lab, require_auth
|
|
|
|
bp = Blueprint("auth", __name__, url_prefix="/api/auth")
|
|
|
|
_PASSWORD_MIN_LEN = 8
|
|
_USER_RULES_PAYLOAD = {
|
|
"password_min_len": _PASSWORD_MIN_LEN,
|
|
}
|
|
|
|
|
|
def load_credentials():
|
|
"""Получение учетных данных из переменных окружения."""
|
|
return (
|
|
current_app.config.get("AUTH_LOGIN", getattr(Config, "AUTH_LOGIN", "admin")),
|
|
current_app.config.get(
|
|
"AUTH_PASSWORD", getattr(Config, "AUTH_PASSWORD", "admin")
|
|
),
|
|
)
|
|
|
|
|
|
def _ensure_default_superuser() -> None:
|
|
saved_login, saved_password = load_credentials()
|
|
login_key = saved_login.strip()
|
|
if db.session.scalar(select(WebUser.id).where(WebUser.login == login_key).limit(1)):
|
|
return
|
|
if db.session.scalar(select(WebUser.id).limit(1)):
|
|
return
|
|
|
|
user = WebUser(
|
|
login=login_key,
|
|
password_hash=generate_password_hash(saved_password),
|
|
is_superuser=True,
|
|
)
|
|
db.session.add(user)
|
|
try:
|
|
db.session.commit()
|
|
except IntegrityError:
|
|
db.session.rollback()
|
|
|
|
|
|
def _get_user_by_login(login: str) -> WebUser | None:
|
|
return db.session.execute(
|
|
select(WebUser).where(WebUser.login == login.strip())
|
|
).scalar_one_or_none()
|
|
|
|
|
|
def save_credentials(login: str, password: str) -> None:
|
|
"""Смена учетных данных через API отключена (env-only)."""
|
|
_ = (login, password)
|
|
raise RuntimeError(
|
|
"Смена учетных данных через API отключена. Используйте WESP_AUTH_LOGIN/WESP_AUTH_PASSWORD."
|
|
)
|
|
|
|
|
|
@bp.get("/ping")
|
|
def auth_ping():
|
|
return jsonify({"status": "ok"}), 200
|
|
|
|
|
|
@bp.post("/login")
|
|
def authenticate_login():
|
|
data = request.json or {}
|
|
login = (data.get("login") or "").strip()
|
|
password = data.get("password") or ""
|
|
|
|
if not login:
|
|
return jsonify({"status": "error", "message": "Не указан логин"}), 400
|
|
if not password:
|
|
return jsonify({"status": "error", "message": "Не указан пароль"}), 400
|
|
|
|
_ensure_default_superuser()
|
|
user = _get_user_by_login(login)
|
|
if user is not None and check_password_hash(user.password_hash, password):
|
|
remember = bool(data.get("remember"))
|
|
session["authenticated"] = True
|
|
session["user_login"] = user.login
|
|
session["is_superuser"] = bool(user.is_superuser)
|
|
session["lab_access"] = bool(user.is_superuser or user.lab_access)
|
|
session["remember_login"] = remember
|
|
if remember:
|
|
session.permanent = True
|
|
else:
|
|
session.permanent = False
|
|
return jsonify(
|
|
{
|
|
"status": "success",
|
|
"message": "Авторизация успешна",
|
|
"authenticated": True,
|
|
}
|
|
)
|
|
|
|
return (
|
|
jsonify(
|
|
{
|
|
"status": "error",
|
|
"message": "Неверный логин или пароль",
|
|
"authenticated": False,
|
|
}
|
|
),
|
|
401,
|
|
)
|
|
|
|
|
|
@bp.post("/logout")
|
|
def logout():
|
|
session.clear()
|
|
return jsonify({"status": "success", "message": "Выход выполнен успешно"})
|
|
|
|
|
|
@bp.get("/check")
|
|
def check_auth():
|
|
authenticated = session.get("authenticated", False)
|
|
user_login = session.get("user_login", "")
|
|
remember_login = bool(session.get("remember_login"))
|
|
is_superuser = bool(session.get("is_superuser", False))
|
|
can_lab = can_access_lab() if authenticated else False
|
|
if authenticated:
|
|
session["lab_access"] = can_lab
|
|
return jsonify(
|
|
{
|
|
"status": "success",
|
|
"authenticated": authenticated,
|
|
"user_login": user_login,
|
|
"remember_login": remember_login,
|
|
"is_superuser": is_superuser,
|
|
"can_lab": can_lab,
|
|
"user_rules": _USER_RULES_PAYLOAD,
|
|
}
|
|
)
|
|
|
|
|
|
@bp.get("/get_current_credentials")
|
|
def get_current_credentials():
|
|
current_login = (session.get("user_login") or "").strip()
|
|
if not current_login:
|
|
saved_login, _saved_password = load_credentials()
|
|
current_login = saved_login
|
|
return jsonify(
|
|
{
|
|
"status": "success",
|
|
"login": current_login,
|
|
# Никогда не возвращаем пароль в API.
|
|
"password": "",
|
|
"user_rules": _USER_RULES_PAYLOAD,
|
|
}
|
|
)
|
|
|
|
|
|
@bp.post("/change_credentials")
|
|
@require_auth
|
|
def change_credentials():
|
|
from app.routes.admin import (
|
|
_WEB_USER_PASSWORD_MIN_LEN,
|
|
_reject_if_weak_login_or_password,
|
|
_valid_login,
|
|
)
|
|
|
|
data = request.json or {}
|
|
old_login = (data.get("old_login") or "").strip()
|
|
old_password = data.get("old_password") or ""
|
|
new_login = (data.get("new_login") or "").strip()
|
|
new_password = data.get("new_password") or ""
|
|
confirm_password = data.get("confirm_password") or new_password
|
|
|
|
session_login = (session.get("user_login") or "").strip()
|
|
if not session_login:
|
|
return jsonify({"status": "error", "message": "Сессия не найдена"}), 401
|
|
|
|
user = _get_user_by_login(session_login)
|
|
if user is None:
|
|
return jsonify({"status": "error", "message": "Пользователь не найден"}), 404
|
|
|
|
if old_login != user.login:
|
|
return (
|
|
jsonify({"status": "error", "message": "Неверный текущий логин или пароль"}),
|
|
401,
|
|
)
|
|
if not check_password_hash(user.password_hash, old_password):
|
|
return (
|
|
jsonify({"status": "error", "message": "Неверный текущий логин или пароль"}),
|
|
401,
|
|
)
|
|
|
|
if not new_login:
|
|
return jsonify({"status": "error", "message": "Новый логин не может быть пустым"}), 400
|
|
if not new_password:
|
|
return jsonify({"status": "error", "message": "Новый пароль не может быть пустым"}), 400
|
|
if new_password != confirm_password:
|
|
return jsonify({"status": "error", "message": "Новые пароли не совпадают"}), 400
|
|
|
|
if not _valid_login(new_login):
|
|
return (
|
|
jsonify(
|
|
{
|
|
"status": "error",
|
|
"message": "Логин не длиннее 64 символов",
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
|
|
if len(new_password) < _WEB_USER_PASSWORD_MIN_LEN:
|
|
return (
|
|
jsonify(
|
|
{
|
|
"status": "error",
|
|
"message": f"Пароль должен быть не короче {_WEB_USER_PASSWORD_MIN_LEN} символов",
|
|
}
|
|
),
|
|
400,
|
|
)
|
|
|
|
err_body, err_code = _reject_if_weak_login_or_password(new_login, new_password)
|
|
if err_body is not None:
|
|
return jsonify(err_body), err_code
|
|
|
|
if new_login != user.login:
|
|
existing = _get_user_by_login(new_login)
|
|
if existing is not None and existing.id != user.id:
|
|
return (
|
|
jsonify({"status": "error", "message": "Пользователь с таким логином уже существует"}),
|
|
409,
|
|
)
|
|
|
|
user.login = new_login
|
|
user.password_hash = generate_password_hash(new_password)
|
|
try:
|
|
db.session.commit()
|
|
except IntegrityError:
|
|
db.session.rollback()
|
|
return (
|
|
jsonify({"status": "error", "message": "Пользователь с таким логином уже существует"}),
|
|
409,
|
|
)
|
|
|
|
session["user_login"] = new_login
|
|
return jsonify({"status": "success", "message": "Учетные данные обновлены"})
|
|
|