feat: Complete chat system implementation and password reset enhancement

- Add comprehensive chat system with modern UI design
- Implement admin-based password reset system
- Fix template syntax errors and 500 server errors
- Add chat routes, API endpoints, and database models
- Enhance user interface with Tailwind CSS card-based design
- Implement community guidelines and quick action features
- Add responsive design for mobile and desktop compatibility
- Create support chat functionality with admin integration
- Fix JavaScript inheritance in base template
- Add database migration for chat system tables

Features:
 Modern chat interface with room management
 Admin-based password reset workflow
 Real-time chat with mobile app support
 Professional UI with gradient cards and hover effects
 Community guidelines and safety features
 Responsive design for all devices
 Error-free template rendering
This commit is contained in:
ske087
2025-08-09 20:44:25 +03:00
parent d1e2b95678
commit 1661f5f588
14 changed files with 2742 additions and 34 deletions

View File

@@ -6,6 +6,7 @@ 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
@@ -103,25 +104,61 @@ def logout():
@auth.route('/forgot-password', methods=['GET', 'POST'])
def forgot_password():
"""Forgot password page"""
"""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()
if user:
token = generate_reset_token(user.email)
reset_url = url_for('auth.reset_password', token=token, _external=True)
msg = Message(
subject="Password Reset Request",
recipients=[user.email],
body=f"Hello {user.nickname},\n\nTo reset your password, click the link below:\n{reset_url}\n\nIf you did not request this, please ignore this email."
# 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
)
try:
mail.send(msg)
except Exception as e:
flash(f"Failed to send reset email: {e}", "danger")
flash('If an account with that email exists, we\'ve sent password reset instructions.', 'info')
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)
# Password reset route
@@ -145,6 +182,50 @@ def reset_password(token):
return redirect(url_for('auth.login'))
return render_template('auth/reset_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:

125
app/routes/chat.py Normal file
View File

@@ -0,0 +1,125 @@
"""
Chat web interface routes
Provides HTML templates and endpoints for web-based chat
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from sqlalchemy import desc, and_
from app.extensions import db
from app.models import ChatRoom, ChatMessage, ChatParticipant, Post
# Create blueprint
chat = Blueprint('chat', __name__)
@chat.route('/')
@login_required
def index():
"""Chat main page with room list and rules"""
# Get user's recent chat rooms
user_rooms = db.session.query(ChatRoom).join(ChatParticipant).filter(
and_(
ChatParticipant.user_id == current_user.id,
ChatRoom.is_active == True
)
).order_by(desc(ChatRoom.last_activity)).limit(10).all()
# Get public rooms that are active
public_rooms = ChatRoom.query.filter(
ChatRoom.is_active == True,
ChatRoom.is_private == False
).order_by(desc(ChatRoom.last_activity)).limit(10).all()
return render_template('chat/index.html',
user_rooms=user_rooms,
public_rooms=public_rooms)
@chat.route('/room/<int:room_id>')
@login_required
def room(room_id):
"""Chat room interface"""
room = ChatRoom.query.get_or_404(room_id)
# Check access
if room.is_private:
participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if not participant:
flash('You do not have access to this chat room.', 'error')
return redirect(url_for('chat.index'))
# Get or create participant record
participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if not participant and not room.is_private:
# Auto-join public rooms
participant = ChatParticipant(
room_id=room_id,
user_id=current_user.id,
role='member'
)
db.session.add(participant)
db.session.commit()
# Get recent messages
messages = ChatMessage.query.filter_by(
room_id=room_id,
is_deleted=False
).order_by(ChatMessage.created_at).limit(50).all()
# Get participants
participants = ChatParticipant.query.filter_by(room_id=room_id).all()
return render_template('chat/room.html',
room=room,
messages=messages,
participants=participants,
current_participant=participant)
@chat.route('/create')
@login_required
def create_room_form():
"""Show create room form"""
# Get available posts for post discussions
recent_posts = Post.query.filter_by(published=True).order_by(
desc(Post.created_at)
).limit(20).all()
return render_template('chat/create_room.html', posts=recent_posts)
@chat.route('/support')
@login_required
def support():
"""Admin support page"""
# Get user's recent support tickets (rooms they created for support)
recent_tickets = ChatRoom.query.filter(
ChatRoom.room_type == 'admin_support',
ChatRoom.created_by_id == current_user.id
).order_by(desc(ChatRoom.created_at)).limit(5).all()
return render_template('chat/support.html',
recent_tickets=recent_tickets)
@chat.route('/embed/<int:post_id>')
@login_required
def embed_post_chat(post_id):
"""Embedded chat widget for post pages"""
post = Post.query.get_or_404(post_id)
# Find existing discussion room
discussion_room = ChatRoom.query.filter(
and_(
ChatRoom.room_type == 'post_discussion',
ChatRoom.related_post_id == post_id,
ChatRoom.is_active == True
)
).first()
return render_template('chat/embed.html',
post=post,
discussion_room=discussion_room)

560
app/routes/chat_api.py Normal file
View File

@@ -0,0 +1,560 @@
"""
Chat API routes for mobile app compatibility
Provides RESTful endpoints for chat functionality
"""
from flask import Blueprint, request, jsonify, current_app
from flask_login import login_required, current_user
from sqlalchemy import desc, and_, or_
from datetime import datetime, timedelta
import re
from app.extensions import db
from app.models import ChatRoom, ChatMessage, ChatParticipant, Post, User
# Create blueprint
chat_api = Blueprint('chat_api', __name__)
# Chat rules and guidelines
CHAT_RULES = [
"Be respectful and courteous to all community members",
"No offensive language, harassment, or personal attacks",
"Stay on topic - use post-specific chats for discussions about routes",
"No spam or promotional content without permission",
"Share useful tips and experiences about motorcycle adventures",
"Help newcomers and answer questions when you can",
"Report inappropriate behavior to administrators",
"Keep conversations constructive and helpful"
]
# Profanity filter (basic implementation)
BLOCKED_WORDS = [
'spam', 'scam', 'fake', 'stupid', 'idiot', 'hate'
# Add more words as needed
]
def contains_blocked_content(text):
"""Check if text contains blocked words"""
text_lower = text.lower()
return any(word in text_lower for word in BLOCKED_WORDS)
@chat_api.route('/rules', methods=['GET'])
def get_chat_rules():
"""Get chat rules and guidelines"""
return jsonify({
'success': True,
'rules': CHAT_RULES
})
@chat_api.route('/rooms', methods=['GET'])
@login_required
def get_chat_rooms():
"""Get list of available chat rooms for the user"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
room_type = request.args.get('type', None)
# Base query - only active rooms the user can access
query = ChatRoom.query.filter(ChatRoom.is_active == True)
# Filter by type if specified
if room_type:
query = query.filter(ChatRoom.room_type == room_type)
# Order by last activity
query = query.order_by(desc(ChatRoom.last_activity))
# Paginate
rooms = query.paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'success': True,
'rooms': [room.to_dict() for room in rooms.items],
'pagination': {
'page': page,
'pages': rooms.pages,
'per_page': per_page,
'total': rooms.total,
'has_next': rooms.has_next,
'has_prev': rooms.has_prev
}
})
@chat_api.route('/rooms', methods=['POST'])
@login_required
def create_chat_room():
"""Create a new chat room"""
data = request.get_json()
if not data or not data.get('name'):
return jsonify({
'success': False,
'error': 'Room name is required'
}), 400
# Validate input
name = data.get('name', '').strip()
description = data.get('description', '').strip()
room_type = data.get('room_type', 'general')
related_post_id = data.get('related_post_id')
if len(name) < 3 or len(name) > 100:
return jsonify({
'success': False,
'error': 'Room name must be between 3 and 100 characters'
}), 400
# Check if room already exists
existing_room = ChatRoom.query.filter_by(name=name).first()
if existing_room:
return jsonify({
'success': False,
'error': 'A room with this name already exists'
}), 400
# Validate related post if specified
if related_post_id:
post = Post.query.get(related_post_id)
if not post:
return jsonify({
'success': False,
'error': 'Related post not found'
}), 404
try:
# Create room
room = ChatRoom(
name=name,
description=description,
room_type=room_type,
created_by_id=current_user.id,
related_post_id=related_post_id
)
db.session.add(room)
db.session.flush() # Get the room ID
# Add creator as participant with admin role
participant = ChatParticipant(
room_id=room.id,
user_id=current_user.id,
role='admin'
)
db.session.add(participant)
# Add system message
system_message = ChatMessage(
room_id=room.id,
user_id=current_user.id,
content=f"Welcome to {name}! This chat room was created for motorcycle adventure discussions.",
message_type='system'
)
db.session.add(system_message)
db.session.commit()
return jsonify({
'success': True,
'room': room.to_dict()
}), 201
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating chat room: {str(e)}")
return jsonify({
'success': False,
'error': 'Failed to create chat room'
}), 500
@chat_api.route('/rooms/<int:room_id>', methods=['GET'])
@login_required
def get_chat_room(room_id):
"""Get chat room details"""
room = ChatRoom.query.get_or_404(room_id)
# Check if user has access
if room.is_private:
participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if not participant:
return jsonify({
'success': False,
'error': 'Access denied'
}), 403
return jsonify({
'success': True,
'room': room.to_dict()
})
@chat_api.route('/rooms/<int:room_id>/join', methods=['POST'])
@login_required
def join_chat_room(room_id):
"""Join a chat room"""
room = ChatRoom.query.get_or_404(room_id)
# Check if already a participant
existing_participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if existing_participant:
return jsonify({
'success': True,
'message': 'Already a member of this room'
})
try:
# Add user as participant
participant = ChatParticipant(
room_id=room_id,
user_id=current_user.id,
role='member'
)
db.session.add(participant)
# Add system message
system_message = ChatMessage(
room_id=room_id,
user_id=current_user.id,
content=f"{current_user.nickname} joined the chat",
message_type='system'
)
db.session.add(system_message)
# Update room activity
room.last_activity = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'message': 'Successfully joined the room'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error joining chat room: {str(e)}")
return jsonify({
'success': False,
'error': 'Failed to join room'
}), 500
@chat_api.route('/rooms/<int:room_id>/messages', methods=['GET'])
@login_required
def get_chat_messages(room_id):
"""Get messages from a chat room"""
room = ChatRoom.query.get_or_404(room_id)
# Check access
if room.is_private:
participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if not participant:
return jsonify({
'success': False,
'error': 'Access denied'
}), 403
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
# Get messages (newest first for mobile scrolling)
messages = ChatMessage.query.filter_by(
room_id=room_id,
is_deleted=False
).order_by(desc(ChatMessage.created_at)).paginate(
page=page, per_page=per_page, error_out=False
)
# Update user's last seen
participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if participant:
participant.last_seen = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'messages': [msg.to_dict() for msg in reversed(messages.items)],
'pagination': {
'page': page,
'pages': messages.pages,
'per_page': per_page,
'total': messages.total,
'has_next': messages.has_next,
'has_prev': messages.has_prev
}
})
@chat_api.route('/rooms/<int:room_id>/messages', methods=['POST'])
@login_required
def send_message(room_id):
"""Send a message to a chat room"""
room = ChatRoom.query.get_or_404(room_id)
data = request.get_json()
if not data or not data.get('content'):
return jsonify({
'success': False,
'error': 'Message content is required'
}), 400
content = data.get('content', '').strip()
message_type = data.get('message_type', 'text')
reply_to_id = data.get('reply_to_id')
# Validate content
if len(content) < 1 or len(content) > 2000:
return jsonify({
'success': False,
'error': 'Message must be between 1 and 2000 characters'
}), 400
# Check for blocked content
if contains_blocked_content(content):
return jsonify({
'success': False,
'error': 'Message contains inappropriate content'
}), 400
# Check if user is participant
participant = ChatParticipant.query.filter_by(
room_id=room_id,
user_id=current_user.id
).first()
if not participant:
return jsonify({
'success': False,
'error': 'You must join the room first'
}), 403
if participant.is_muted:
return jsonify({
'success': False,
'error': 'You are muted in this room'
}), 403
try:
# Create message
message = ChatMessage(
room_id=room_id,
user_id=current_user.id,
content=content,
message_type=message_type,
reply_to_id=reply_to_id
)
db.session.add(message)
# Update room activity
room.last_activity = datetime.utcnow()
# Update participant last seen
participant.last_seen = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'message': message.to_dict()
}), 201
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error sending message: {str(e)}")
return jsonify({
'success': False,
'error': 'Failed to send message'
}), 500
@chat_api.route('/rooms/<int:room_id>/participants', methods=['GET'])
@login_required
def get_room_participants(room_id):
"""Get participants of a chat room"""
room = ChatRoom.query.get_or_404(room_id)
participants = ChatParticipant.query.filter_by(room_id=room_id).all()
return jsonify({
'success': True,
'participants': [p.to_dict() for p in participants]
})
@chat_api.route('/admin-support', methods=['POST'])
@login_required
def create_admin_support_chat():
"""Create a chat room for admin support (e.g., password reset)"""
data = request.get_json()
reason = data.get('reason', 'general_support')
description = data.get('description', '')
# Check if user already has an active admin support chat
existing_room = ChatRoom.query.filter(
and_(
ChatRoom.room_type == 'admin_support',
ChatRoom.created_by_id == current_user.id,
ChatRoom.is_active == True
)
).first()
if existing_room:
return jsonify({
'success': True,
'room': existing_room.to_dict(),
'message': 'Using existing support chat'
})
try:
# Create admin support room
room_name = f"Support - {current_user.nickname} - {reason}"
room = ChatRoom(
name=room_name,
description=f"Admin support chat for {current_user.nickname}. Reason: {reason}",
room_type='admin_support',
is_private=True,
created_by_id=current_user.id
)
db.session.add(room)
db.session.flush()
# Add user as participant
participant = ChatParticipant(
room_id=room.id,
user_id=current_user.id,
role='member'
)
db.session.add(participant)
# Add all admins as participants
admins = User.query.filter_by(is_admin=True).all()
for admin in admins:
if admin.id != current_user.id:
admin_participant = ChatParticipant(
room_id=room.id,
user_id=admin.id,
role='admin'
)
db.session.add(admin_participant)
# Add initial message
initial_message = ChatMessage(
room_id=room.id,
user_id=current_user.id,
content=f"Hello! I need help with: {reason}. {description}",
message_type='text'
)
db.session.add(initial_message)
db.session.commit()
return jsonify({
'success': True,
'room': room.to_dict()
}), 201
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating admin support chat: {str(e)}")
return jsonify({
'success': False,
'error': 'Failed to create support chat'
}), 500
@chat_api.route('/post/<int:post_id>/discussion', methods=['POST'])
@login_required
def create_post_discussion(post_id):
"""Create or get discussion chat for a specific post"""
post = Post.query.get_or_404(post_id)
# Check if discussion already exists
existing_room = ChatRoom.query.filter(
and_(
ChatRoom.room_type == 'post_discussion',
ChatRoom.related_post_id == post_id,
ChatRoom.is_active == True
)
).first()
if existing_room:
# Join the existing room if not already a participant
participant = ChatParticipant.query.filter_by(
room_id=existing_room.id,
user_id=current_user.id
).first()
if not participant:
participant = ChatParticipant(
room_id=existing_room.id,
user_id=current_user.id,
role='member'
)
db.session.add(participant)
db.session.commit()
return jsonify({
'success': True,
'room': existing_room.to_dict(),
'message': 'Joined existing discussion'
})
try:
# Create new discussion room
room_name = f"Discussion: {post.title}"
room = ChatRoom(
name=room_name,
description=f"Discussion about the post: {post.title}",
room_type='post_discussion',
created_by_id=current_user.id,
related_post_id=post_id
)
db.session.add(room)
db.session.flush()
# Add creator as participant
participant = ChatParticipant(
room_id=room.id,
user_id=current_user.id,
role='moderator'
)
db.session.add(participant)
# Add post author as participant if different
if post.author_id != current_user.id:
author_participant = ChatParticipant(
room_id=room.id,
user_id=post.author_id,
role='moderator'
)
db.session.add(author_participant)
# Add initial message
initial_message = ChatMessage(
room_id=room.id,
user_id=current_user.id,
content=f"Started discussion about: {post.title}",
message_type='system'
)
db.session.add(initial_message)
db.session.commit()
return jsonify({
'success': True,
'room': room.to_dict()
}), 201
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating post discussion: {str(e)}")
return jsonify({
'success': False,
'error': 'Failed to create discussion'
}), 500