Files
moto-adv-website/app/routes/admin.py
ske087 30bd4c62ad Major Feature Update: Modern Chat System & Admin Management
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.
2025-08-10 00:22:33 +03:00

896 lines
32 KiB
Python

from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify, current_app
from flask_mail import Message
from flask_login import login_required, current_user
from functools import wraps
from datetime import datetime, timedelta
from sqlalchemy import func, desc
import secrets
import uuid
from app.routes.mail import mail
from app.extensions import db
from app.models import User, Post, PostImage, GPXFile, Comment, Like, PageView, MailSettings, SentEmail, PasswordResetRequest, PasswordResetToken, ChatRoom, ChatMessage
admin = Blueprint('admin', __name__, url_prefix='/admin')
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
flash('Admin access required.', 'error')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
@admin.route('/mail-settings', methods=['GET', 'POST'])
@login_required
@admin_required
def mail_settings():
settings = MailSettings.query.first()
if request.method == 'POST':
enabled = bool(request.form.get('enabled'))
provider = request.form.get('provider')
server = request.form.get('server')
port = int(request.form.get('port') or 0)
use_tls = bool(request.form.get('use_tls'))
username = request.form.get('username')
password = request.form.get('password')
default_sender = request.form.get('default_sender')
if not settings:
settings = MailSettings()
db.session.add(settings)
settings.enabled = enabled
settings.provider = provider
settings.server = server
settings.port = port
settings.use_tls = use_tls
settings.username = username
settings.password = password
settings.default_sender = default_sender
db.session.commit()
flash('Mail settings updated.', 'success')
sent_emails = SentEmail.query.order_by(SentEmail.sent_at.desc()).limit(50).all()
return render_template('admin/mail_settings.html', settings=settings, sent_emails=sent_emails)
## Duplicate imports and Blueprint definitions removed
# Password reset token generator (simple, for demonstration)
def generate_reset_token(user):
# In production, use itsdangerous or Flask-Security for secure tokens
return secrets.token_urlsafe(32)
# Admin: Send password reset email to user
@admin.route('/users/<int:user_id>/reset-password', methods=['POST'])
@login_required
@admin_required
def reset_user_password(user_id):
user = User.query.get_or_404(user_id)
token = generate_reset_token(user)
# In production, save token to DB or cache, and validate on reset
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\nAn admin has requested a password reset for your account. Click the link below to reset your password:\n{reset_url}\n\nIf you did not request this, please ignore this email."
)
try:
mail.send(msg)
flash(f"Password reset email sent to {user.email}.", "success")
except Exception as e:
current_app.logger.error(f"Error sending reset email: {e}")
flash(f"Failed to send password reset email: {e}", "danger")
return redirect(url_for('admin.user_detail', user_id=user.id))
def admin_required(f):
"""Decorator to require admin access"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
flash('Admin access required.', 'error')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
@admin.route('/')
@login_required
@admin_required
def dashboard():
"""Admin dashboard with site statistics"""
# Get basic statistics
total_users = User.query.count()
total_posts = Post.query.count()
published_posts = Post.query.filter_by(published=True).count()
pending_posts = Post.query.filter_by(published=False).count()
total_comments = Comment.query.count()
total_likes = Like.query.count()
# Active users (users who created posts or comments in the last 30 days)
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
active_users = db.session.query(User.id).filter(
db.or_(
User.posts.any(Post.created_at >= thirty_days_ago),
User.comments.any(Comment.created_at >= thirty_days_ago)
)
).distinct().count()
# Recent activity
recent_posts = Post.query.order_by(Post.created_at.desc()).limit(5).all()
recent_users = User.query.order_by(User.created_at.desc()).limit(5).all()
# Page views statistics
today = datetime.utcnow().date()
yesterday = today - timedelta(days=1)
week_ago = today - timedelta(days=7)
views_today = PageView.query.filter(
func.date(PageView.created_at) == today
).count()
views_yesterday = PageView.query.filter(
func.date(PageView.created_at) == yesterday
).count()
views_this_week = PageView.query.filter(
PageView.created_at >= week_ago
).count()
# Most viewed posts
most_viewed_posts = db.session.query(
Post.id, Post.title, func.count(PageView.id).label('view_count')
).join(PageView, Post.id == PageView.post_id)\
.group_by(Post.id, Post.title)\
.order_by(desc('view_count'))\
.limit(10).all()
# Most viewed pages
most_viewed_pages = db.session.query(
PageView.path, func.count(PageView.id).label('view_count')
).group_by(PageView.path)\
.order_by(desc('view_count'))\
.limit(10).all()
# Password reset statistics
pending_password_requests = PasswordResetRequest.query.filter_by(status='pending').count()
active_reset_tokens = PasswordResetToken.query.filter_by(is_used=False).filter(
PasswordResetToken.expires_at > datetime.utcnow()
).count()
# Chat statistics
total_chat_rooms = ChatRoom.query.count()
linked_chat_rooms = ChatRoom.query.filter(ChatRoom.related_post_id.isnot(None)).count()
active_chat_rooms = db.session.query(ChatRoom.id).filter(
ChatRoom.messages.any(ChatMessage.created_at >= thirty_days_ago)
).distinct().count()
recent_chat_messages = ChatMessage.query.filter(
ChatMessage.created_at >= thirty_days_ago
).count()
return render_template('admin/dashboard.html',
total_users=total_users,
total_posts=total_posts,
published_posts=published_posts,
pending_posts=pending_posts,
total_comments=total_comments,
total_likes=total_likes,
active_users=active_users,
recent_posts=recent_posts,
recent_users=recent_users,
views_today=views_today,
views_yesterday=views_yesterday,
views_this_week=views_this_week,
most_viewed_posts=most_viewed_posts,
most_viewed_pages=most_viewed_pages,
pending_password_requests=pending_password_requests,
active_reset_tokens=active_reset_tokens,
total_chat_rooms=total_chat_rooms,
linked_chat_rooms=linked_chat_rooms,
active_chat_rooms=active_chat_rooms,
recent_chat_messages=recent_chat_messages)
@admin.route('/posts')
@login_required
@admin_required
def posts():
"""Admin post management - review posts"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', 'all') # pending, published, all
query = Post.query
if status == 'pending':
query = query.filter_by(published=False)
elif status == 'published':
query = query.filter_by(published=True)
# 'all' shows all posts
posts = query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=20, error_out=False
)
return render_template('admin/posts.html', posts=posts, status=status)
@admin.route('/posts/<int:post_id>')
@login_required
@admin_required
def post_detail(post_id):
"""View post details for review"""
post = Post.query.get_or_404(post_id)
return render_template('admin/post_detail.html', post=post)
@admin.route('/posts/<int:post_id>/publish', methods=['POST'])
@login_required
@admin_required
def publish_post(post_id):
"""Publish a post and create map routes if GPX files exist"""
post = Post.query.get_or_404(post_id)
post.published = True
post.updated_at = datetime.utcnow()
try:
db.session.commit()
# Create map routes for GPX files when post is published
from app.utils.gpx_processor import process_post_approval
success = process_post_approval(post_id)
if success:
flash(f'Post "{post.title}" has been published and map routes created.', 'success')
else:
flash(f'Post "{post.title}" has been published. No GPX files found or error creating map routes.', 'warning')
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error publishing post {post_id}: {str(e)}')
flash(f'Error publishing post: {str(e)}', 'error')
return redirect(url_for('admin.posts'))
@admin.route('/posts/<int:post_id>/unpublish', methods=['POST'])
@login_required
@admin_required
def unpublish_post(post_id):
"""Unpublish a post and remove from map"""
post = Post.query.get_or_404(post_id)
post.published = False
post.updated_at = datetime.utcnow()
try:
# Note: We keep the MapRoute data for potential re-publishing
# Only the API will filter by published status
db.session.commit()
flash(f'Post "{post.title}" has been unpublished.', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error unpublishing post {post_id}: {str(e)}')
flash(f'Error unpublishing post: {str(e)}', 'error')
return redirect(url_for('admin.posts'))
@admin.route('/posts/<int:post_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete_post(post_id):
"""Delete a post"""
current_app.logger.info(f'Admin {current_user.id} attempting to delete post {post_id}')
# Get all posts before deletion for debugging
all_posts_before = Post.query.all()
current_app.logger.info(f'Posts before deletion: {[p.id for p in all_posts_before]}')
post = Post.query.get_or_404(post_id)
title = post.title
current_app.logger.info(f'Found post to delete: ID={post.id}, Title="{title}"')
try:
# Delete associated map route if exists
from app.models import MapRoute
map_route = MapRoute.query.filter_by(post_id=post.id).first()
if map_route:
db.session.delete(map_route)
current_app.logger.info(f'Deleted MapRoute for post {post.id}')
# Delete associated files and records
db.session.delete(post)
db.session.commit()
# Clean up orphaned media folders
from app.utils.clean_orphan_media import clean_orphan_post_media
clean_orphan_post_media()
# Check posts after deletion
all_posts_after = Post.query.all()
current_app.logger.info(f'Posts after deletion: {[p.id for p in all_posts_after]}')
current_app.logger.info(f'Successfully deleted post {post_id}: "{title}"')
flash(f'Post "{title}" has been deleted.', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error deleting post {post_id}: {str(e)}')
flash(f'Error deleting post: {str(e)}', 'error')
return redirect(url_for('admin.posts'))
@admin.route('/users')
@login_required
@admin_required
def users():
"""Admin user management"""
page = request.args.get('page', 1, type=int)
users = User.query.order_by(User.created_at.desc()).paginate(
page=page, per_page=50, error_out=False
)
return render_template('admin/users.html', users=users)
@admin.route('/users/<int:user_id>')
@login_required
@admin_required
def user_detail(user_id):
"""View user details"""
user = User.query.get_or_404(user_id)
user_posts = Post.query.filter_by(author_id=user_id).order_by(Post.created_at.desc()).all()
user_comments = Comment.query.filter_by(author_id=user_id).order_by(Comment.created_at.desc()).all()
return render_template('admin/user_detail.html',
user=user,
user_posts=user_posts,
user_comments=user_comments)
@admin.route('/users/<int:user_id>/toggle-status', methods=['POST'])
@login_required
@admin_required
def toggle_user_status(user_id):
"""Toggle user active/inactive status"""
user = User.query.get_or_404(user_id)
# Prevent self-modification if current user is admin
if user.is_admin and user.id == current_user.id:
return jsonify({'success': False, 'error': 'Cannot deactivate your own admin account'})
# Toggle status
user.is_active = not user.is_active
status = "activated" if user.is_active else "deactivated"
try:
db.session.commit()
return jsonify({
'success': True,
'message': f'User {user.nickname} has been {status}',
'is_active': user.is_active
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': f'Failed to update user status: {str(e)}'})
@admin.route('/users/<int:user_id>/delete', methods=['DELETE', 'POST'])
@login_required
@admin_required
def delete_user(user_id):
"""Delete user and transfer their posts to admin"""
user = User.query.get_or_404(user_id)
# Prevent self-deletion
if user.id == current_user.id:
return jsonify({'success': False, 'error': 'Cannot delete your own account'})
# Prevent deletion of other admins
if user.is_admin:
return jsonify({'success': False, 'error': 'Cannot delete admin accounts'})
try:
# Transfer all user's posts to current admin
user_posts = Post.query.filter_by(author_id=user.id).all()
for post in user_posts:
post.author_id = current_user.id
# Transfer all user's comments to current admin
user_comments = Comment.query.filter_by(author_id=user.id).all()
for comment in user_comments:
comment.author_id = current_user.id
# Delete user's likes (they will be orphaned)
Like.query.filter_by(user_id=user.id).delete()
# Delete user's page views
PageView.query.filter_by(user_id=user.id).delete()
username = user.nickname
user_posts_count = len(user_posts)
# Delete the user
db.session.delete(user)
db.session.commit()
return jsonify({
'success': True,
'message': f'User {username} has been deleted. {user_posts_count} posts transferred to {current_user.nickname}.'
})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': f'Failed to delete user: {str(e)}'})
@admin.route('/analytics')
@login_required
@admin_required
def analytics():
"""Detailed analytics page"""
# Get date range from query params
days = request.args.get('days', 30, type=int)
end_date = datetime.utcnow().date()
start_date = end_date - timedelta(days=days)
# Daily views for the chart
daily_views = db.session.query(
func.date(PageView.created_at).label('date'),
func.count(PageView.id).label('views')
).filter(
func.date(PageView.created_at) >= start_date
).group_by(func.date(PageView.created_at))\
.order_by('date').all()
# Top posts by views
top_posts = db.session.query(
Post.id, Post.title, Post.author, func.count(PageView.id).label('view_count')
).join(PageView, Post.id == PageView.post_id)\
.filter(PageView.created_at >= start_date)\
.group_by(Post.id, Post.title)\
.order_by(desc('view_count'))\
.limit(20).all()
# User activity - separate queries to avoid cartesian product
user_posts = db.session.query(
User.id,
User.nickname,
func.count(Post.id).label('post_count')
).outerjoin(Post, User.id == Post.author_id)\
.group_by(User.id, User.nickname).subquery()
user_comments = db.session.query(
User.id,
func.count(Comment.id).label('comment_count')
).outerjoin(Comment, User.id == Comment.author_id)\
.group_by(User.id).subquery()
user_activity = db.session.query(
user_posts.c.nickname,
user_posts.c.post_count,
func.coalesce(user_comments.c.comment_count, 0).label('comment_count')
).outerjoin(user_comments, user_posts.c.id == user_comments.c.id)\
.order_by(desc(user_posts.c.post_count))\
.limit(20).all()
# Get overall statistics
total_views = PageView.query.count()
unique_visitors = db.session.query(PageView.ip_address).distinct().count()
today_views = PageView.query.filter(
func.date(PageView.created_at) == datetime.utcnow().date()
).count()
week_start = datetime.utcnow().date() - timedelta(days=7)
week_views = PageView.query.filter(
func.date(PageView.created_at) >= week_start
).count()
# Popular pages
popular_pages = db.session.query(
PageView.path,
func.count(PageView.id).label('view_count')
).group_by(PageView.path)\
.order_by(desc('view_count'))\
.limit(10).all()
# Recent activity
recent_views = PageView.query.join(User, PageView.user_id == User.id, isouter=True)\
.order_by(desc(PageView.created_at))\
.limit(20).all()
# Browser statistics (extract from user agent)
browser_stats = db.session.query(
func.substr(PageView.user_agent, 1, 50).label('browser'),
func.count(PageView.id).label('view_count')
).filter(PageView.user_agent.isnot(None))\
.group_by(func.substr(PageView.user_agent, 1, 50))\
.order_by(desc('view_count'))\
.limit(10).all()
return render_template('admin/analytics.html',
total_views=total_views,
unique_visitors=unique_visitors,
today_views=today_views,
week_views=week_views,
popular_pages=popular_pages,
recent_views=recent_views,
browser_stats=browser_stats,
daily_views=daily_views,
top_posts=top_posts,
user_activity=user_activity,
days=days)
# API endpoints for AJAX requests
@admin.route('/api/quick-stats')
@login_required
@admin_required
def api_quick_stats():
"""Quick stats for dashboard widgets"""
pending_count = Post.query.filter_by(published=False).count()
today_views = PageView.query.filter(
func.date(PageView.created_at) == datetime.utcnow().date()
).count()
return jsonify({
'pending_posts': pending_count,
'today_views': today_views
})
# Password Reset Management Routes
@admin.route('/password-reset-requests')
@login_required
@admin_required
def password_reset_requests():
"""View all password reset requests"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', 'all')
query = PasswordResetRequest.query
if status != 'all':
query = query.filter_by(status=status)
requests = query.order_by(PasswordResetRequest.created_at.desc()).paginate(
page=page, per_page=20, error_out=False
)
return render_template('admin/password_reset_requests.html',
requests=requests, status=status)
@admin.route('/password-reset-requests/<int:request_id>')
@login_required
@admin_required
def password_reset_request_detail(request_id):
"""View individual password reset request details"""
reset_request = PasswordResetRequest.query.get_or_404(request_id)
# Get associated tokens
tokens = PasswordResetToken.query.filter_by(request_id=request_id).order_by(
PasswordResetToken.created_at.desc()
).all()
return render_template('admin/password_reset_request_detail.html',
request=reset_request, tokens=tokens)
@admin.route('/password-reset-requests/<int:request_id>/generate-token', methods=['POST'])
@login_required
@admin_required
def generate_password_reset_token(request_id):
"""Generate a new password reset token for a request"""
reset_request = PasswordResetRequest.query.get_or_404(request_id)
# Create token
token = str(uuid.uuid4())
expires_at = datetime.utcnow() + timedelta(hours=24) # 24 hour expiry
reset_token = PasswordResetToken(
token=token,
request_id=request_id,
user_id=reset_request.user.id,
created_by_admin_id=current_user.id,
expires_at=expires_at
)
db.session.add(reset_token)
reset_request.status = 'token_generated'
reset_request.updated_at = datetime.utcnow()
db.session.commit()
flash('Password reset token generated successfully!', 'success')
return redirect(url_for('admin.password_reset_token_template', token_id=reset_token.id))
@admin.route('/password-reset-tokens')
@login_required
@admin_required
def password_reset_tokens():
"""View all password reset tokens"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', 'all')
query = PasswordResetToken.query.join(User).order_by(PasswordResetToken.created_at.desc())
if status == 'active':
query = query.filter_by(is_used=False).filter(PasswordResetToken.expires_at > datetime.utcnow())
elif status == 'used':
query = query.filter_by(is_used=True)
elif status == 'expired':
query = query.filter(PasswordResetToken.expires_at <= datetime.utcnow(), PasswordResetToken.is_used == False)
tokens = query.paginate(page=page, per_page=20, error_out=False)
# Get counts for statistics
active_count = PasswordResetToken.query.filter_by(is_used=False).filter(
PasswordResetToken.expires_at > datetime.utcnow()
).count()
used_count = PasswordResetToken.query.filter_by(is_used=True).count()
expired_count = PasswordResetToken.query.filter(
PasswordResetToken.expires_at <= datetime.utcnow(),
PasswordResetToken.is_used == False
).count()
return render_template('admin/password_reset_tokens.html',
tokens=tokens, status=status,
active_count=active_count, used_count=used_count, expired_count=expired_count)
@admin.route('/manage-chats')
@login_required
@admin_required
def manage_chats():
"""Admin chat room management"""
page = request.args.get('page', 1, type=int)
category = request.args.get('category', '')
status = request.args.get('status', '')
search = request.args.get('search', '')
# Base query with message count
query = db.session.query(
ChatRoom,
func.count(ChatMessage.id).label('message_count'),
func.max(ChatMessage.created_at).label('last_activity')
).outerjoin(ChatMessage).group_by(ChatRoom.id)
# Apply filters
if category:
query = query.filter(ChatRoom.category == category)
if status == 'linked':
query = query.filter(ChatRoom.related_post_id.isnot(None))
elif status == 'unlinked':
query = query.filter(ChatRoom.related_post_id.is_(None))
elif status == 'active':
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
query = query.having(func.max(ChatMessage.created_at) >= thirty_days_ago)
elif status == 'inactive':
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
query = query.having(
db.or_(
func.max(ChatMessage.created_at) < thirty_days_ago,
func.max(ChatMessage.created_at).is_(None)
)
)
if search:
query = query.filter(
db.or_(
ChatRoom.name.contains(search),
ChatRoom.description.contains(search)
)
)
# Order by last activity
query = query.order_by(func.max(ChatMessage.created_at).desc().nullslast())
# Paginate
results = query.paginate(page=page, per_page=20, error_out=False)
# Process results to add message count and last activity to room objects
chat_rooms = []
for room, message_count, last_activity in results.items:
room.message_count = message_count
room.last_activity = last_activity
chat_rooms.append(room)
# Get statistics
total_rooms = ChatRoom.query.count()
linked_rooms = ChatRoom.query.filter(ChatRoom.related_post_id.isnot(None)).count()
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
active_today = db.session.query(ChatRoom.id).filter(
ChatRoom.messages.any(
func.date(ChatMessage.created_at) == datetime.utcnow().date()
)
).distinct().count()
total_messages = ChatMessage.query.count()
# Get available posts for linking
available_posts = Post.query.filter_by(published=True).order_by(Post.title).all()
# Create pagination object with processed rooms
class PaginationWrapper:
def __init__(self, original_pagination, items):
self.page = original_pagination.page
self.per_page = original_pagination.per_page
self.total = original_pagination.total
self.pages = original_pagination.pages
self.has_prev = original_pagination.has_prev
self.prev_num = original_pagination.prev_num
self.has_next = original_pagination.has_next
self.next_num = original_pagination.next_num
self.items = items
def iter_pages(self):
return range(1, self.pages + 1)
pagination = PaginationWrapper(results, chat_rooms)
return render_template('admin/manage_chats.html',
chat_rooms=chat_rooms,
pagination=pagination,
total_rooms=total_rooms,
linked_rooms=linked_rooms,
active_today=active_today,
total_messages=total_messages,
available_posts=available_posts)
@admin.route('/api/chat-rooms')
@login_required
@admin_required
def api_chat_rooms():
"""API endpoint for chat rooms (for AJAX calls)"""
exclude_id = request.args.get('exclude', type=int)
query = ChatRoom.query
if exclude_id:
query = query.filter(ChatRoom.id != exclude_id)
rooms = query.all()
# Get message counts
room_data = []
for room in rooms:
message_count = ChatMessage.query.filter_by(room_id=room.id).count()
room_data.append({
'id': room.id,
'name': room.name,
'description': room.description,
'category': room.category,
'message_count': message_count,
'created_at': room.created_at.isoformat() if room.created_at else None
})
return jsonify({'success': True, 'rooms': room_data})
@admin.route('/api/chat-rooms/<int:room_id>', methods=['PUT'])
@login_required
@admin_required
def api_update_chat_room(room_id):
"""Update chat room details"""
room = ChatRoom.query.get_or_404(room_id)
try:
data = request.get_json()
room.name = data.get('name', room.name)
room.description = data.get('description', room.description)
room.category = data.get('category', room.category)
# Handle post linking
related_post_id = data.get('related_post_id')
if related_post_id:
post = Post.query.get(related_post_id)
if post:
room.related_post_id = related_post_id
else:
return jsonify({'success': False, 'error': 'Post not found'})
else:
room.related_post_id = None
db.session.commit()
return jsonify({'success': True, 'message': 'Room updated successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)})
@admin.route('/api/chat-rooms/<int:room_id>/link-post', methods=['POST'])
@login_required
@admin_required
def api_link_chat_room_to_post(room_id):
"""Link chat room to a post"""
room = ChatRoom.query.get_or_404(room_id)
try:
data = request.get_json()
post_id = data.get('post_id')
if post_id:
post = Post.query.get_or_404(post_id)
room.related_post_id = post_id
else:
room.related_post_id = None
db.session.commit()
return jsonify({'success': True, 'message': 'Room linked to post successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)})
@admin.route('/api/chat-rooms/<int:source_room_id>/merge', methods=['POST'])
@login_required
@admin_required
def api_merge_chat_rooms(source_room_id):
"""Merge source room into target room"""
source_room = ChatRoom.query.get_or_404(source_room_id)
try:
data = request.get_json()
target_room_id = data.get('target_room_id')
target_room = ChatRoom.query.get_or_404(target_room_id)
# Move all messages from source to target room
messages = ChatMessage.query.filter_by(room_id=source_room_id).all()
for message in messages:
message.room_id = target_room_id
# Add system message about the merge
merge_message = ChatMessage(
room_id=target_room_id,
sender_id=current_user.id,
content=f"Room '{source_room.name}' has been merged into this room by admin {current_user.nickname}",
message_type='system',
is_system_message=True
)
db.session.add(merge_message)
# Delete the source room
db.session.delete(source_room)
db.session.commit()
return jsonify({'success': True, 'message': 'Rooms merged successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)})
@admin.route('/api/chat-rooms/<int:room_id>', methods=['DELETE'])
@login_required
@admin_required
def api_delete_chat_room(room_id):
"""Delete chat room and all its messages"""
room = ChatRoom.query.get_or_404(room_id)
try:
# Delete all messages first
ChatMessage.query.filter_by(room_id=room_id).delete()
# Delete the room
db.session.delete(room)
db.session.commit()
return jsonify({'success': True, 'message': 'Room deleted successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': str(e)})
@admin.route('/password-reset-tokens/<int:token_id>/template')
@login_required
@admin_required
def password_reset_token_template(token_id):
"""Display email template for password reset token"""
token = PasswordResetToken.query.get_or_404(token_id)
# Generate the reset URL
reset_url = url_for('auth.reset_password_with_token', token=token.token, _external=True)
return render_template('admin/password_reset_email_template.html',
token=token, reset_url=reset_url)