You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
107 lines
3.3 KiB
107 lines
3.3 KiB
"""Маршруты auth: HTML (`/login`, `/logout`) и JSON (`/api/auth/*`).""" |
|
from __future__ import annotations |
|
|
|
import logging |
|
|
|
from flask import ( |
|
Blueprint, |
|
flash, |
|
jsonify, |
|
redirect, |
|
render_template, |
|
request, |
|
session, |
|
url_for, |
|
) |
|
|
|
from ..config import is_assignment_feature_enabled, is_dev_ui |
|
from ..messages import RU |
|
from .decorators import login_required, current_user |
|
from .services import AuthError, authenticate_credentials |
|
|
|
log = logging.getLogger(__name__) |
|
|
|
auth_bp = Blueprint('auth', __name__) |
|
|
|
|
|
def _safe_next(default: str = '/') -> str: |
|
"""Защита от open-redirect: разрешаем только относительные пути.""" |
|
nxt = request.values.get('next') or default |
|
if not nxt.startswith('/') or nxt.startswith('//'): |
|
return default |
|
return nxt |
|
|
|
|
|
def _do_login(login: str, password: str): |
|
user = authenticate_credentials(login, password) |
|
session.clear() |
|
session['user_id'] = user.id |
|
session.permanent = True |
|
return user |
|
|
|
|
|
# ─── HTML ──────────────────────────────────────────────────────────── |
|
|
|
@auth_bp.route('/login', methods=['GET']) |
|
def login_page(): |
|
if current_user() is not None: |
|
return redirect(_safe_next('/')) |
|
return render_template('auth/login.html', next=_safe_next('/')) |
|
|
|
|
|
@auth_bp.route('/login', methods=['POST']) |
|
def login_submit(): |
|
login = (request.form.get('login') or '').strip() |
|
password = request.form.get('password') or '' |
|
try: |
|
_do_login(login, password) |
|
except AuthError as e: |
|
flash(e.message, 'error') |
|
return render_template('auth/login.html', next=_safe_next('/'), login=login), e.status |
|
except Exception: |
|
log.exception('login_submit failed') |
|
flash(RU['loginFailed'], 'error') |
|
return render_template('auth/login.html', next=_safe_next('/'), login=login), 500 |
|
return redirect(_safe_next('/')) |
|
|
|
|
|
@auth_bp.route('/logout', methods=['POST', 'GET']) |
|
def logout(): |
|
session.clear() |
|
if request.method == 'GET': |
|
return redirect(url_for('auth.login_page')) |
|
return redirect(url_for('auth.login_page')) |
|
|
|
|
|
# ─── JSON API ──────────────────────────────────────────────────────── |
|
|
|
@auth_bp.route('/api/auth/login', methods=['POST']) |
|
def api_login(): |
|
data = request.get_json(silent=True) or {} |
|
login = (data.get('login') or '').strip() |
|
password = data.get('password') or '' |
|
try: |
|
user = _do_login(login, password) |
|
except AuthError as e: |
|
return jsonify(error=e.message), e.status |
|
except Exception: |
|
log.exception('api_login failed') |
|
return jsonify(error=RU['loginFailed']), 500 |
|
return jsonify(user=user.to_public_dict()) |
|
|
|
|
|
@auth_bp.route('/api/auth/logout', methods=['POST']) |
|
def api_logout(): |
|
session.clear() |
|
return jsonify(message=RU['loggedOut']) |
|
|
|
|
|
@auth_bp.route('/api/auth/me', methods=['GET']) |
|
@login_required |
|
def api_me(): |
|
user = current_user() |
|
return jsonify( |
|
user=user.to_public_dict() if user else None, |
|
devUi=is_dev_ui(), |
|
assignmentUi=is_assignment_feature_enabled(), |
|
)
|
|
|