Files
LT_QR_Codes_Management/routes/auth.py
T

305 lines
12 KiB
Python

"""
routes/auth.py
==============
Authentication and user-profile routes.
Routes: /, /register, /login, /logout, /profile
"""
from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for
from datetime import datetime
from urllib.parse import urlparse, urljoin
import json
from extensions import db, logger_handler
from models.user import User
from logger_handler import log_user_activity, log_database_operations
from utils.helpers import admin_required, login_required, staff_or_admin_required
from turnstile_utils import turnstile_utils
bp = Blueprint('auth', __name__)
@bp.route('/', endpoint='index')
def index():
"""Home page - redirect to login if not authenticated"""
if 'user_id' in session:
return redirect(url_for('dashboard.dashboard'))
return redirect(url_for('auth.login'))
@bp.route('/register', methods=['GET', 'POST'], endpoint='register')
@login_required
@admin_required
@log_user_activity('user_registration')
def register():
"""User registration endpoint"""
if request.method == 'POST':
try:
full_name = request.form['full_name']
email = request.form['email']
username = request.form['username']
password = request.form['password']
# Check if user already exists
if User.query.filter_by(username=username).first():
flash('Username already exists.', 'error')
return render_template('register.html')
if User.query.filter_by(email=email).first():
flash('Email already registered.', 'error')
return render_template('register.html')
# Create new user (default role: staff)
new_user = User(
full_name=full_name,
email=email,
username=username,
role='staff'
)
new_user.set_password(password)
db.session.add(new_user)
db.session.commit()
# Log successful user registration
logger_handler.logger.info(f"New user registered: {username} ({email})")
flash('Registration successful! Please log in.', 'success')
return redirect(url_for('auth.login'))
except Exception as e:
db.session.rollback()
logger_handler.log_database_error('user_registration', e)
flash('Registration failed. Please try again.', 'error')
return render_template('register.html')
@bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def login():
"""Enhanced user authentication with Turnstile and comprehensive logging"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
turnstile_response = request.form.get('cf-turnstile-response', '')
if not username or not password:
flash('Please enter both username and password.', 'error')
return render_template('login.html')
# Rate-limit check — blocks IPs with 5+ failed attempts in 15 minutes
from flask import current_app
sec_mgr = getattr(current_app, 'security_manager', None)
if sec_mgr and sec_mgr.is_auth_rate_limited():
logger_handler.log_security_event(
event_type="login_rate_limited",
description=f"Login blocked by rate limiter for username: {username}",
severity="HIGH"
)
flash('Too many failed attempts. Please wait 15 minutes before trying again.', 'error')
return render_template('login.html')
# Verify Turnstile if enabled
if turnstile_utils.is_enabled():
if not turnstile_utils.verify_turnstile(turnstile_response):
# Log failed Turnstile attempt
logger_handler.log_security_event(
event_type="turnstile_verification_failed",
description=f"Failed Turnstile verification for username: {username}",
severity="HIGH"
)
flash('Please complete the security verification.', 'error')
return render_template('login.html')
try:
# Find user (case-insensitive username)
user = User.query.filter(
User.username.like(username),
User.active_status == True
).first()
if user and user.check_password(password):
# Check if "Remember Me" is checked
remember_me = request.form.get('remember_me') == 'on'
# Invalidate the pre-login session to prevent session fixation attacks,
# then re-apply the remember_me permanence flag on the fresh session.
session.clear()
# Set session as permanent if "Remember Me" is checked
if remember_me:
session.permanent = True
session['remember_me'] = True
else:
session.permanent = False
session['remember_me'] = False
# Successful login
session['user_id'] = user.id
session['username'] = user.username
session['role'] = user.role
session['full_name'] = user.full_name
session['login_time'] = datetime.now().isoformat()
# Start of the 10-hour limit for logins without Remember Me
# (enforced in app.py adjust_session_lifetime)
session['login_epoch'] = datetime.now().timestamp()
# Create a secure session token (also clears failed attempts for this IP)
if sec_mgr:
sec_mgr.create_secure_session(user.id)
# Update last login date
user.last_login_date = datetime.utcnow()
db.session.commit()
# Log successful login with Turnstile info
logger_handler.log_user_login(
user_id=user.id,
username=user.username,
success=True
)
# Log successful Turnstile verification
if turnstile_utils.is_enabled():
logger_handler.log_security_event(
event_type="turnstile_verification_success",
description=f"Successful Turnstile verification for user: {user.username}",
severity="INFO"
)
flash(f'Welcome back, {user.full_name}!', 'success')
logger_handler.logger.info(f"User {user.username} (ID: {user.id}) logged in successfully")
# Redirect to intended page or dashboard.
# Validate next is a relative path on this host to prevent open-redirect attacks.
def _is_safe_url(target):
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)
next_page = request.args.get('next')
if next_page and _is_safe_url(next_page):
return redirect(next_page)
return redirect(url_for('attendance.attendance_report'))
else:
# Invalid credentials — record failed attempt for rate limiting
if sec_mgr:
sec_mgr.record_failed_attempt(username)
user_id = user.id if user else None
logger_handler.log_user_login(
user_id=user_id,
username=username,
success=False,
failure_reason="Invalid credentials"
)
flash('Invalid username or password.', 'error')
logger_handler.logger.warning(f"Failed login attempt for username: {username}")
except Exception as e:
db.session.rollback()
logger_handler.log_database_error('user_login', e)
logger_handler.logger.error(f"Login error for username '{username}': {e}")
flash('Login error. Please try again.', 'error')
return render_template('login.html')
@bp.route('/logout', endpoint='logout')
def logout():
"""User logout endpoint with session duration logging"""
user_id = session.get('user_id')
username = session.get('username')
login_time_str = session.get('login_time')
# Calculate session duration
session_duration = None
if login_time_str:
try:
login_time = datetime.fromisoformat(login_time_str)
session_duration = (datetime.now() - login_time).total_seconds() / 60 # minutes
except Exception as e:
logger_handler.logger.debug(f"Could not parse login_time for session duration: {e}")
# Log user logout
if user_id and username:
logger_handler.log_user_logout(
user_id=user_id,
username=username,
session_duration=session_duration
)
session.clear()
flash('You have been logged out.', 'info')
return redirect(url_for('auth.login'))
@bp.route('/profile', methods=['GET', 'POST'], endpoint='profile')
@login_required
@log_user_activity('profile_update')
def profile():
"""User profile management with logging"""
try:
user = db.session.get(User, session['user_id'])
if request.method == 'POST':
form_type = request.form.get('form_type')
if form_type == 'profile':
# Track changes for logging
old_name = user.full_name
old_email = user.email
# Update profile information
user.full_name = request.form['full_name']
user.email = request.form['email']
# Check for changes
changes = {}
if old_name != user.full_name:
changes['full_name'] = {'old': old_name, 'new': user.full_name}
if old_email != user.email:
changes['email'] = {'old': old_email, 'new': user.email}
db.session.commit()
# Log profile update if there were changes
if changes:
logger_handler.logger.info(f"User profile updated: {user.username} - Changes: {json.dumps(changes)}")
flash('Profile updated successfully!', 'success')
elif form_type == 'password':
# Update password
current_password = request.form['current_password']
new_password = request.form['new_password']
if user.check_password(current_password):
user.set_password(new_password)
db.session.commit()
# Log password change
logger_handler.log_security_event(
event_type="password_change",
description=f"User {user.username} changed password",
severity="MEDIUM"
)
flash('Password updated successfully!', 'success')
else:
# Log failed password change attempt
logger_handler.log_security_event(
event_type="password_change_failed",
description=f"Failed password change attempt for user {user.username}",
severity="HIGH"
)
flash('Current password is incorrect.', 'error')
return render_template('profile.html', user=user)
except Exception as e:
db.session.rollback()
logger_handler.log_database_error('profile_update', e)
flash('Profile update failed. Please try again.', 'error')
return redirect(url_for('dashboard.dashboard'))