Files
admin-edu-space/backend/app/routes/auth.py
T

152 lines
6.1 KiB
Python

from flask import Blueprint, render_template, redirect, url_for, flash, request, session, jsonify
from flask_login import login_user, logout_user, login_required, current_user
from flask_babel import gettext as _
from app import db
from app.models.user import User
from app.forms.auth import LoginForm
from datetime import datetime
auth_bp = Blueprint('auth', __name__)
from sqlalchemy import func
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""Handle user login with case-insensitive email lookup and whitespace trimming"""
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
from app.models.setting import SystemSetting
demo_access_enabled = SystemSetting.get_value('demo_access_enabled', 'true') in ['true', 'True', '1', True]
form = LoginForm()
if form.validate_on_submit():
clean_email = (form.email.data or '').strip().lower()
user = User.query.filter(func.lower(User.email) == clean_email).first()
if user and user.check_password(form.password.data):
if not user.is_active:
flash(_('Tu cuenta ha sido desactivada. Por favor contacta al administrador.'), 'error')
return render_template('auth/login.html', form=form, demo_access_enabled=demo_access_enabled)
login_user(user, remember=form.remember_me.data)
user.update_last_login()
# Registrar en bitácora de auditoría
try:
from app.services.audit_service import AuditService
AuditService.log(
action='LOGIN',
module='auth',
user_id=user.id,
user_email=user.email,
details=f"Inicio de sesión exitoso ({user.role})"
)
except Exception:
pass
# Si el usuario tiene idioma configurado en su perfil, sobrescribe el navegador en toda la sesión
if user.preferred_language and user.preferred_language in ['es', 'en']:
session['language'] = user.preferred_language
else:
session.pop('language', None)
session.pop('login_lang_explicit', None)
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('main.dashboard')
user_full_name = f'{user.first_name} {user.last_name}'.strip() or user.name
flash(_('¡Bienvenido de nuevo, %(name)s!', name=user_full_name), 'success')
return redirect(next_page)
else:
try:
from app.services.audit_service import AuditService
AuditService.log(
action='LOGIN_FAILED',
module='auth',
details=f"Intento fallido de login con email: {clean_email}"
)
except Exception:
pass
flash(_('Correo electrónico o contraseña inválidos.'), 'error')
return render_template('auth/login.html', form=form, demo_access_enabled=demo_access_enabled)
@auth_bp.route('/logout')
@login_required
def logout():
"""Handle user logout"""
logout_user()
# Al cerrar sesión, se limpian las cookies de idioma para que el login detecte el navegador o use español
session.pop('language', None)
session.pop('login_lang_explicit', None)
flash(_('Has cerrado sesión correctamente.'), 'info')
return redirect(url_for('auth.login'))
@auth_bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
"""User profile page with language and theme settings"""
if request.method == 'POST':
lang = request.form.get('preferred_language')
if lang in ['es', 'en']:
current_user.preferred_language = lang
session['language'] = lang
else:
# Cadena vacía o None restablece la detección automática del navegador
current_user.preferred_language = None
session.pop('language', None)
theme = request.form.get('theme_preference')
if theme in ['auto', 'light', 'dark']:
current_user.theme_preference = theme
db.session.commit()
flash(_('Preferencias de usuario actualizadas correctamente.'), 'success')
return redirect(url_for('auth.profile'))
return render_template('auth/profile.html')
@auth_bp.route('/api/user/theme', methods=['POST'])
@login_required
def update_theme_api():
"""Update user theme preference via fetch/AJAX from navbar toggle"""
data = request.get_json(silent=True) or request.form
theme = data.get('theme') if data else None
if theme in ['auto', 'light', 'dark']:
current_user.theme_preference = theme
db.session.commit()
return jsonify({'status': 'success', 'theme': theme})
return jsonify({'status': 'error', 'message': 'Invalid theme'}), 400
@auth_bp.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
"""Change user password"""
if request.method == 'POST':
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if not current_user.check_password(current_password):
flash('Current password is incorrect.', 'error')
return redirect(url_for('auth.change_password'))
if new_password != confirm_password:
flash('New passwords do not match.', 'error')
return redirect(url_for('auth.change_password'))
if len(new_password) < 6:
flash('Password must be at least 6 characters long.', 'error')
return redirect(url_for('auth.change_password'))
current_user.set_password(new_password)
db.session.commit()
flash('Password changed successfully!', 'success')
return redirect(url_for('auth.profile'))
return render_template('auth/change_password.html')