@@ -0,0 +1,246 @@
|
||||
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": "Учетные данные обновлены"})
|
||||
|
||||
Reference in New Issue
Block a user