from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash from app.models import User, db, PasswordResetToken from app.routes.reset_password import RequestResetForm, ResetPasswordForm from flask_mail import Message from app.routes.mail import mail from app.utils.token import generate_reset_token, verify_reset_token from datetime import datetime import re from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Email, EqualTo, Length auth = Blueprint('auth', __name__) class LoginForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) remember_me = BooleanField('Remember Me') submit = SubmitField('Sign In') class RegisterForm(FlaskForm): nickname = StringField('Nickname', validators=[DataRequired(), Length(min=3, max=32)]) email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired(), Length(min=8)]) password2 = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Register') class ForgotPasswordForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Email()]) submit = SubmitField('Request Password Reset') @auth.route('/login', methods=['GET', 'POST']) def login(): """User login page""" if current_user.is_authenticated: return redirect(url_for('main.index')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user and user.check_password(form.password.data): login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') if not next_page or not next_page.startswith('/'): next_page = url_for('community.index') flash(f'Welcome back, {user.nickname}!', 'success') return redirect(next_page) else: flash('Invalid email or password.', 'error') return render_template('auth/login.html', form=form) @auth.route('/register', methods=['GET', 'POST']) def register(): """User registration page""" if current_user.is_authenticated: return redirect(url_for('main.index')) form = RegisterForm() if form.validate_on_submit(): # Check if user already exists if User.query.filter_by(email=form.email.data).first(): flash('Email address already registered.', 'error') return render_template('auth/register.html', form=form) if User.query.filter_by(nickname=form.nickname.data).first(): flash('Nickname already taken.', 'error') return render_template('auth/register.html', form=form) # Validate password strength if not is_valid_password(form.password.data): flash('Password must be at least 8 characters long and contain at least one letter and one number.', 'error') return render_template('auth/register.html', form=form) # Create new user user = User( nickname=form.nickname.data, email=form.email.data ) user.set_password(form.password.data) try: db.session.add(user) db.session.commit() login_user(user) flash('Registration successful! Welcome to the community!', 'success') return redirect(url_for('community.index')) except Exception as e: db.session.rollback() flash('An error occurred during registration. Please try again.', 'error') return render_template('auth/register.html', form=form) @auth.route('/logout') @login_required def logout(): """User logout""" logout_user() flash('You have been logged out.', 'info') return redirect(url_for('main.index')) @auth.route('/forgot-password', methods=['GET', 'POST']) def forgot_password(): """Forgot password page - sends message to admin instead of email""" if current_user.is_authenticated: return redirect(url_for('main.index')) form = RequestResetForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() # Create password reset user if it doesn't exist reset_user = User.query.filter_by(email='reset_password@motoadventure.local').first() if not reset_user: reset_user = User( nickname='PasswordReset', email='reset_password@motoadventure.local', is_active=False # This is a system user ) reset_user.set_password('temp_password') # Won't be used db.session.add(reset_user) db.session.commit() # Find admin support room from app.models import ChatRoom, ChatMessage admin_room = ChatRoom.query.filter_by(room_type='support').first() if not admin_room: # Create admin support room if it doesn't exist system_user = User.query.filter_by(email='system@motoadventure.local').first() admin_room = ChatRoom( name='Technical Support', description='Administrative support and password resets', room_type='support', is_private=False, is_active=True, created_by_id=system_user.id if system_user else 1 ) db.session.add(admin_room) db.session.commit() # Create the password reset message if user: message_content = f"A user with email '{user.email}' (nickname: {user.nickname}) needs their password to be changed. Please assist with password reset." else: message_content = f"Someone with email '{form.email.data}' requested a password reset, but no account exists with this email. Please check if this user needs assistance creating an account." reset_message = ChatMessage( content=message_content, room_id=admin_room.id, sender_id=reset_user.id, is_system_message=True ) db.session.add(reset_message) # Update room activity admin_room.last_activity = datetime.utcnow() db.session.commit() flash('Your password reset request has been sent to administrators. They will contact you soon to help reset your password.', 'info') return redirect(url_for('auth.login')) return render_template('auth/forgot_password.html', form=form) @auth.route('/change-password', methods=['POST']) @login_required def change_password(): """Change user password""" current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') # Validate inputs if not all([current_password, new_password, confirm_password]): flash('All password fields are required.', 'error') return redirect(url_for('community.profile')) # Check current password if not current_user.check_password(current_password): flash('Current password is incorrect.', 'error') return redirect(url_for('community.profile')) # Validate new password if len(new_password) < 6: flash('New password must be at least 6 characters long.', 'error') return redirect(url_for('community.profile')) # Check password confirmation if new_password != confirm_password: flash('New password and confirmation do not match.', 'error') return redirect(url_for('community.profile')) # Check if new password is different from current if current_user.check_password(new_password): flash('New password must be different from your current password.', 'error') return redirect(url_for('community.profile')) try: # Update password current_user.set_password(new_password) db.session.commit() flash('Password updated successfully!', 'success') except Exception as e: db.session.rollback() flash('An error occurred while updating your password. Please try again.', 'error') return redirect(url_for('community.profile')) def is_valid_password(password): """Validate password strength""" if len(password) < 8: return False if not re.search(r'[A-Za-z]', password): return False if not re.search(r'\d', password): return False return True class ResetPasswordWithTokenForm(FlaskForm): password = PasswordField('New Password', validators=[DataRequired(), Length(min=8)]) password2 = PasswordField('Confirm New Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Reset Password') @auth.route('/reset-password/', methods=['GET', 'POST']) def reset_password_with_token(token): """Reset password using admin-generated token""" # Find the token in database reset_token = PasswordResetToken.query.filter_by(token=token).first() if not reset_token: flash('Invalid or expired reset link.', 'error') return redirect(url_for('auth.login')) # Check if token is expired if reset_token.is_expired: flash('This reset link has expired. Please request a new one.', 'error') return redirect(url_for('auth.login')) # Check if token is already used if reset_token.is_used: flash('This reset link has already been used.', 'error') return redirect(url_for('auth.login')) form = ResetPasswordWithTokenForm() if form.validate_on_submit(): user = reset_token.user # Validate password strength if not is_valid_password(form.password.data): flash('Password must be at least 8 characters long and contain both letters and numbers.', 'error') return render_template('auth/reset_password_with_token.html', form=form, token=token) # Update password user.set_password(form.password.data) # Mark token as used reset_token.used_at = datetime.utcnow() reset_token.user_ip = request.environ.get('REMOTE_ADDR') # Update request status if reset_token.request: reset_token.request.status = 'completed' reset_token.request.updated_at = datetime.utcnow() db.session.commit() flash('Your password has been reset successfully! You can now log in with your new password.', 'success') return redirect(url_for('auth.login')) return render_template('auth/reset_password_with_token.html', form=form, token=token, user=reset_token.user)