Features Added: 🔥 Modern Chat System: - Real-time messaging with modern Tailwind CSS design - Post-linked discussions for adventure sharing - Chat categories (general, technical-support, adventure-planning) - Mobile-responsive interface with gradient backgrounds - JavaScript polling for live message updates 🎯 Comprehensive Admin Panel: - Chat room management with merge capabilities - Password reset system with email templates - User management with admin controls - Chat statistics and analytics dashboard - Room binding to posts and categorization �� Mobile API Integration: - RESTful API endpoints at /api/v1/chat - Session-based authentication for mobile apps - Comprehensive endpoints for rooms, messages, users - Mobile app compatibility (React Native, Flutter) 🛠️ Technical Improvements: - Enhanced database models with ChatRoom categories - Password reset token system with email verification - Template synchronization fixes for Docker deployment - Migration scripts for database schema updates - Improved error handling and validation 🎨 UI/UX Enhancements: - Modern card-based layouts matching app design - Consistent styling across chat and admin interfaces - Mobile-optimized touch interactions - Professional gradient designs and glass morphism effects 📚 Documentation: - Updated README with comprehensive API documentation - Added deployment instructions for Docker (port 8100) - Configuration guide for production environments - Mobile integration examples and endpoints This update transforms the platform into a comprehensive motorcycle adventure community with modern chat capabilities and professional admin management tools.
274 lines
11 KiB
Python
274 lines
11 KiB
Python
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/<token>', 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)
|