Chatty / auth.py
findEthics
Complete user authentication system implementation
f6278c5
Raw
History Blame Contribute Delete
9.32 kB
"""
Authentication utilities and decorators for Flask-Login integration
"""
import re
from functools import wraps
from flask import redirect, url_for, request, flash, current_app
from flask_login import current_user, LoginManager
from email_validator import validate_email, EmailNotValidError
import logging
from models import User
logger = logging.getLogger(__name__)
# Initialize Flask-Login
login_manager = LoginManager()
def init_login_manager(app):
"""Initialize Flask-Login with the Flask app"""
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
@login_manager.user_loader
def load_user(user_id):
"""User loader function for Flask-Login"""
try:
return User.find_by_id(user_id)
except Exception as e:
logger.error(f"Error loading user with ID {user_id}: {e}")
return None
def login_required(f):
"""
Decorator to require authentication for routes
Enhanced version that handles AJAX requests properly
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
# Handle AJAX requests
if request.is_json or request.headers.get('Content-Type') == 'application/json':
from flask import jsonify
return jsonify({
'success': False,
'error': 'Authentication required',
'redirect': url_for('login')
}), 401
# Handle regular requests
flash('Please log in to access this page.', 'info')
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
def get_current_user():
"""Get currently logged in user"""
try:
if current_user.is_authenticated:
return current_user
return None
except Exception as e:
logger.error(f"Error getting current user: {e}")
return None
def validate_email_format(email: str) -> tuple[bool, str]:
"""
Validate email format using email-validator
Returns (is_valid, error_message)
"""
try:
if not email or not email.strip():
return False, "Email is required"
email = email.strip()
# Use email-validator library for comprehensive validation
validate_email(email, check_deliverability=False)
return True, ""
except EmailNotValidError as e:
return False, "Please enter a valid email address"
except Exception as e:
logger.error(f"Error validating email {email}: {e}")
return False, "Email validation error"
def validate_password_format(password: str) -> tuple[bool, str]:
"""
Validate password strength requirements
Returns (is_valid, error_message)
"""
try:
if not password:
return False, "Password is required"
# Check minimum length (8 characters as per requirements)
if len(password) < 8:
return False, "Password must be at least 8 characters long"
# Additional strength checks can be added here if needed
# For now, just checking minimum length as per requirements
return True, ""
except Exception as e:
logger.error(f"Error validating password: {e}")
return False, "Password validation error"
def validate_registration_data(email: str, password: str, confirm_password: str) -> tuple[bool, list]:
"""
Validate complete registration data
Returns (is_valid, list_of_errors)
"""
errors = []
try:
# Validate email
email_valid, email_error = validate_email_format(email)
if not email_valid:
errors.append(email_error)
# Validate password
password_valid, password_error = validate_password_format(password)
if not password_valid:
errors.append(password_error)
# Check password confirmation
if password != confirm_password:
errors.append("Passwords do not match")
# Check if email already exists (if email format is valid)
if email_valid and User.find_by_email(email.strip()):
errors.append("Email already registered")
return len(errors) == 0, errors
except Exception as e:
logger.error(f"Error validating registration data: {e}")
return False, ["Registration validation error"]
def validate_login_data(email: str, password: str) -> tuple[bool, list]:
"""
Validate login data
Returns (is_valid, list_of_errors)
"""
errors = []
try:
# Basic validation
if not email or not email.strip():
errors.append("Email is required")
if not password:
errors.append("Password is required")
# If basic validation passes, check format
if not errors:
email_valid, email_error = validate_email_format(email)
if not email_valid:
errors.append("Please enter a valid email address")
return len(errors) == 0, errors
except Exception as e:
logger.error(f"Error validating login data: {e}")
return False, ["Login validation error"]
def authenticate_user(email: str, password: str) -> tuple[bool, User, str]:
"""
Authenticate user with email and password
Returns (is_authenticated, user_object, error_message)
"""
try:
# Validate input data
data_valid, errors = validate_login_data(email, password)
if not data_valid:
return False, None, errors[0] if errors else "Please check your email and password"
# Find user by email
user = User.find_by_email(email.strip())
if not user:
logger.warning(f"Login attempt with non-existent email: {email}")
return False, None, "Invalid email or password"
# Check if user is active
if not user.is_active:
logger.warning(f"Login attempt for disabled account: {email}")
return False, None, "Your account has been disabled. Please contact support."
# Verify password
if not user.check_password(password):
logger.warning(f"Failed login attempt for user: {email}")
return False, None, "Invalid email or password"
logger.info(f"User authenticated successfully: {email}")
return True, user, ""
except Exception as e:
logger.error(f"Error authenticating user {email}: {e}")
return False, None, "We're experiencing technical difficulties. Please try again later."
def create_user_account(email: str, password: str, confirm_password: str) -> tuple[bool, User, list]:
"""
Create new user account with validation
Returns (is_created, user_object, list_of_errors)
"""
try:
# Validate registration data
data_valid, errors = validate_registration_data(email, password, confirm_password)
if not data_valid:
return False, None, errors
# Create user
user = User.create_user(email.strip(), password)
if not user:
logger.error(f"Failed to create user account for {email}")
return False, None, ["We couldn't create your account. Please try again later."]
logger.info(f"User account created successfully: {email}")
return True, user, []
except Exception as e:
logger.error(f"Error creating user account for {email}: {e}")
return False, None, ["We're experiencing technical difficulties. Please try again later."]
def is_safe_url(target):
"""
Check if a URL is safe for redirects
Prevents open redirect vulnerabilities
"""
try:
from urllib.parse import urlparse, urljoin
from flask import request
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
except Exception as e:
logger.error(f"Error checking URL safety for {target}: {e}")
return False
def get_redirect_target():
"""
Get safe redirect target from request
"""
try:
for target in request.values.get('next'), request.referrer:
if not target:
continue
if is_safe_url(target):
return target
return None
except Exception as e:
logger.error(f"Error getting redirect target: {e}")
return None
def redirect_back(endpoint, **values):
"""
Redirect back to a safe URL or fallback to endpoint
"""
try:
target = get_redirect_target()
if target:
return redirect(target)
return redirect(url_for(endpoint, **values))
except Exception as e:
logger.error(f"Error in redirect_back: {e}")
return redirect(url_for(endpoint, **values))