152 lines
6.1 KiB
Python
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') |