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')) 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) 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) @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')