from flask import Blueprint, request, jsonify, session, render_template from functools import wraps from bson import ObjectId from datetime import datetime import logging from datetime import datetime, timedelta from utils.extensions import ext from utils.email import send_email admin_bp = Blueprint('admin', __name__) def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return jsonify({'error': 'Vui lòng đăng nhập'}), 401 try: user = ext.db.users.find_one({'_id': ObjectId(session['user_id'])}) if not user or not user.get('is_admin'): return jsonify({'error': 'Quyền truy cập bị từ chối. Chỉ admin được phép.'}), 403 return f(*args, **kwargs) except Exception as e: logging.error(f"Error in admin_required: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 return decorated_function @admin_bp.route('/admin/dashboard') @admin_required def admin_dashboard(): try: users = ext.db.users.find({'is_active': True}).sort('created_at', -1) user_name = session.get('username') # Get statistics total_users = ext.db.users.count_documents({}) active_users = ext.db.users.count_documents({'is_active': True}) pending_users = ext.db.users.count_documents({'is_active': False}) admin_users = ext.db.users.count_documents({'is_admin': True}) # Get recent activity recent_conversations = ext.db.conversations.find().sort('timestamp', -1).limit(10) users_list = [] for user in users: users_list.append({ 'id': str(user['_id']), 'username': user['username'], 'email': user['email'], 'phone': user['phone'], 'is_active': user.get('is_active', False), 'is_admin': user.get('is_admin', False), 'account_type': user.get('account_type', 'limited'), 'query_limit': user.get('query_limit', None), 'query_count': user.get('query_count', 0), 'created_at': user.get('created_at', datetime.utcnow()).isoformat(), 'last_reset': user.get('last_reset', datetime.utcnow()).isoformat() if user.get('last_reset') else None, 'last_login': user.get('last_login', None) }) recent_conversations_list = [] for conv in recent_conversations: user = ext.db.users.find_one({'_id': ObjectId(conv['user_id'])}) recent_conversations_list.append({ 'id': str(conv['_id']), 'title': conv.get('title', 'Không có tiêu đề'), 'user': user['username'] if user else 'Unknown', 'timestamp': conv['timestamp'].isoformat() if 'timestamp' in conv else None, 'message_count': ext.db.messages.count_documents({'conversation_id': str(conv['_id'])}) }) return render_template('admin_dashboard.html', users=users_list, user_name=user_name, statistics={ 'total_users': total_users, 'active_users': active_users, 'pending_users': pending_users, 'admin_users': admin_users }, recent_conversations=recent_conversations_list) except Exception as e: logging.error(f"Error in admin_dashboard: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/pending_users', methods=['GET']) @admin_required def get_pending_users(): try: pending_users = ext.db.users.find({'is_active': False}).sort('created_at', -1) users_list = [] for user in pending_users: users_list.append({ 'id': str(user['_id']), 'username': user['username'], 'email': user['email'], 'phone': user['phone'], 'created_at': user['created_at'].isoformat() if 'created_at' in user else None, 'account_type': user.get('account_type', 'limited'), 'days_pending': (datetime.utcnow() - user['created_at']).days if 'created_at' in user else 0 }) return jsonify({ 'pending_users': users_list, 'count': len(users_list) }), 200 except Exception as e: logging.error(f"Error in get_pending_users: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/user//verify', methods=['POST']) @admin_required def verify_user(user_id): data = request.get_json(silent=True) or {} action = data.get('action') # 'approve' or 'reject' if action not in ['approve', 'reject']: return jsonify({'error': 'Hành động không hợp lệ'}), 400 try: if not ObjectId.is_valid(user_id): return jsonify({'error': 'ID người dùng không hợp lệ'}), 400 user = ext.db.users.find_one({'_id': ObjectId(user_id)}) if not user: return jsonify({'error': 'Người dùng không tồn tại'}), 404 if user.get('is_active') != False: return jsonify({'error': 'Tài khoản không ở trạng thái chờ xác thực'}), 400 if action == 'approve': ext.db.users.update_one( {'_id': ObjectId(user_id)}, {'$set': { 'is_active': True, 'otp': None, 'verified_at': datetime.utcnow(), 'verified_by': session.get('user_id') }} ) send_email( user['email'], 'Tài khoản đã được xác thực', f'Tài khoản của bạn ({user["username"]}) đã được admin xác thực thành công. Bạn có thể đăng nhập.' ) logging.info(f"Admin {session.get('user_id')} approved user {user_id}") return jsonify({ 'message': 'Xác thực tài khoản thành công', 'user': { 'id': user_id, 'username': user['username'], 'email': user['email'] } }), 200 else: # reject # Store reason for rejection if provided reason = data.get('reason', 'Không có lý do cụ thể') ext.db.users.delete_one({'_id': ObjectId(user_id)}) send_email( user['email'], 'Tài khoản bị từ chối', f'''Tài khoản của bạn ({user["username"]}) đã bị từ chối bởi admin. Lý do: {reason} Vui lòng liên hệ hỗ trợ nếu cần thêm thông tin.''' ) logging.info(f"Admin {session.get('user_id')} rejected user {user_id}") return jsonify({ 'message': 'Từ chối tài khoản thành công', 'user': { 'id': user_id, 'username': user['username'], 'email': user['email'] } }), 200 except Exception as e: logging.error(f"Error in verify_user: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/users', methods=['GET']) @admin_required def get_all_users(): try: # Get pagination parameters page = request.args.get('page', 1, type=int) limit = request.args.get('limit', 20, type=int) skip = (page - 1) * limit # Get filter parameters search = request.args.get('search', '') account_type = request.args.get('account_type', '') is_active = request.args.get('is_active', '') # Build query query = {} if search: query['$or'] = [ {'username': {'$regex': search, '$options': 'i'}}, {'email': {'$regex': search, '$options': 'i'}}, {'phone': {'$regex': search, '$options': 'i'}} ] if account_type: query['account_type'] = account_type if is_active.lower() == 'true': query['is_active'] = True elif is_active.lower() == 'false': query['is_active'] = False # Get total count total_users = ext.db.users.count_documents(query) # Get users with pagination users_cursor = ext.db.users.find(query) \ .sort('created_at', -1) \ .skip(skip) \ .limit(limit) users_list = [] for user in users_cursor: # Get user statistics conversation_count = ext.db.conversations.count_documents({'user_id': str(user['_id'])}) message_count = ext.db.messages.count_documents({'conversation_id': {'$in': [ str(conv['_id']) for conv in ext.db.conversations.find({'user_id': str(user['_id'])}) ]}}) users_list.append({ 'id': str(user['_id']), 'username': user['username'], 'email': user['email'], 'phone': user['phone'], 'is_active': user.get('is_active', False), 'is_admin': user.get('is_admin', False), 'account_type': user.get('account_type', 'limited'), 'query_limit': user.get('query_limit', None), 'query_count': user.get('query_count', 0), 'created_at': user.get('created_at', datetime.utcnow()).isoformat(), 'last_reset': user.get('last_reset', datetime.utcnow()).isoformat() if user.get('last_reset') else None, 'last_login': user.get('last_login', None), 'statistics': { 'conversation_count': conversation_count, 'message_count': message_count } }) return jsonify({ 'users': users_list, 'pagination': { 'page': page, 'limit': limit, 'total': total_users, 'pages': (total_users + limit - 1) // limit }, 'filters': { 'search': search, 'account_type': account_type, 'is_active': is_active } }), 200 except Exception as e: logging.error(f"Error in get_all_users: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/user/', methods=['DELETE']) @admin_required def delete_user(user_id): try: if not ObjectId.is_valid(user_id): return jsonify({ 'error': 'ID người dùng không hợp lệ', 'error_code': 'INVALID_USER_ID' }), 400 user = ext.db.users.find_one({'_id': ObjectId(user_id)}) if not user: return jsonify({ 'error': 'Người dùng không tồn tại', 'error_code': 'USER_NOT_FOUND' }), 404 # Prevent deleting own admin account if str(user['_id']) == session.get('user_id'): return jsonify({ 'error': 'Không thể xóa tài khoản của chính bạn', 'error_code': 'SELF_DELETION' }), 403 if user.get('is_admin', False): # Check if this is the last admin admin_count = ext.db.users.count_documents({'is_admin': True}) if admin_count <= 1: return jsonify({ 'error': 'Không thể xóa admin cuối cùng', 'error_code': 'LAST_ADMIN' }), 403 # Delete user conversations and messages conversations = ext.db.conversations.find({'user_id': user_id}) for conv in conversations: ext.db.messages.delete_many({'conversation_id': str(conv['_id'])}) ext.db.conversations.delete_many({'user_id': user_id}) # Delete user result = ext.db.users.delete_one({'_id': ObjectId(user_id)}) if result.deleted_count > 0: # Send notification email send_email( user['email'], 'Tài khoản của bạn đã bị xóa', f'''Tài khoản của bạn ({user["username"]}) đã bị admin xóa. Nếu bạn cho rằng đây là nhầm lẫn, vui lòng liên hệ hỗ trợ để được giải đáp.''' ) logging.info(f"Admin {session.get('user_id')} deleted user {user_id}") return jsonify({ 'message': 'Xóa tài khoản thành công', 'deleted_user': { 'id': user_id, 'username': user['username'], 'email': user['email'] } }), 200 else: return jsonify({ 'error': 'Không thể xóa người dùng', 'error_code': 'DELETE_FAILED' }), 500 except Exception as e: logging.error(f"Error deleting user {user_id}: {e}") return jsonify({ 'error': 'Lỗi khi xóa tài khoản', 'error_code': 'SERVER_ERROR' }), 500 @admin_bp.route('/admin/user/', methods=['PUT']) @admin_required def update_user(user_id): data = request.get_json(silent=True) or {} if not ObjectId.is_valid(user_id): return jsonify({'error': 'ID người dùng không hợp lệ'}), 400 try: user = ext.db.users.find_one({'_id': ObjectId(user_id)}) if not user: return jsonify({'error': 'Người dùng không tồn tại'}), 404 updates = {} # Update account type if 'account_type' in data and data['account_type'] in ['limited', 'unlimited']: updates['account_type'] = data['account_type'] updates['query_limit'] = 10 if data['account_type'] == 'limited' else None updates['query_count'] = 0 updates['last_reset'] = datetime.utcnow() # Update admin status if 'is_admin' in data and isinstance(data['is_admin'], bool): # Prevent removing last admin if user.get('is_admin') and not data['is_admin']: admin_count = ext.db.users.count_documents({'is_admin': True}) if admin_count <= 1: return jsonify({'error': 'Không thể xóa quyền admin của admin cuối cùng'}), 403 updates['is_admin'] = data['is_admin'] # Update query limit for limited accounts if 'query_limit' in data and isinstance(data['query_limit'], int): if data['query_limit'] < 1: return jsonify({'error': 'Giới hạn truy vấn phải lớn hơn 0'}), 400 updates['query_limit'] = data['query_limit'] # Update username if 'username' in data and data['username'].strip(): new_username = data['username'].strip() if new_username != user.get('username'): # Check if username is already taken existing = ext.db.users.find_one({ 'username': new_username, '_id': {'$ne': ObjectId(user_id)} }) if existing: return jsonify({'error': 'Tên người dùng đã tồn tại'}), 400 updates['username'] = new_username # Update email if 'email' in data and data['email'].strip(): new_email = data['email'].strip() if new_email != user.get('email'): # Check if email is already taken existing = ext.db.users.find_one({ 'email': new_email, '_id': {'$ne': ObjectId(user_id)} }) if existing: return jsonify({'error': 'Email đã tồn tại'}), 400 updates['email'] = new_email # Update phone if 'phone' in data and data['phone'].strip(): new_phone = data['phone'].strip() if new_phone != user.get('phone'): # Check if phone is already taken existing = ext.db.users.find_one({ 'phone': new_phone, '_id': {'$ne': ObjectId(user_id)} }) if existing: return jsonify({'error': 'Số điện thoại đã tồn tại'}), 400 updates['phone'] = new_phone if not updates: return jsonify({'error': 'Không có thông tin cập nhật hợp lệ'}), 400 updates['updated_at'] = datetime.utcnow() updates['updated_by'] = session.get('user_id') result = ext.db.users.update_one( {'_id': ObjectId(user_id)}, {'$set': updates} ) if result.modified_count == 0: return jsonify({'error': 'Không tìm thấy người dùng hoặc không có thay đổi'}), 404 logging.info(f"Admin updated user {user_id}: {updates}") # Broadcast updated query count to the user if user_id in ext.connected_clients: user = ext.db.users.find_one({'_id': ObjectId(user_id)}) ext.socketio.emit('query_update', { 'query_count': user.get('query_count', 0), 'query_limit': user.get('query_limit', 10) }, room=ext.connected_clients[user_id]) logging.info(f"Broadcasted query update to user {user_id} after admin update") return jsonify({ 'message': 'Cập nhật người dùng thành công', 'updates': updates }), 200 except Exception as e: logging.error(f"Error updating user: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/user//reset_query', methods=['POST']) @admin_required def reset_user_query_count(user_id): try: if not ObjectId.is_valid(user_id): return jsonify({'error': 'ID người dùng không hợp lệ'}), 400 user = ext.db.users.find_one({'_id': ObjectId(user_id)}) if not user: return jsonify({'error': 'Người dùng không tồn tại'}), 404 if user.get('account_type') == 'unlimited': return jsonify({'error': 'Tài khoản không giới hạn không cần reset!'}), 400 ext.db.users.update_one( {'_id': ObjectId(user_id)}, {'$set': { 'query_count': 0, 'last_reset': datetime.utcnow(), 'reset_by': session.get('user_id'), 'reset_at': datetime.utcnow() }} ) logging.info(f"Admin {session.get('user_id')} reset query count for user {user_id}") # Broadcast updated query count to the user if user_id in ext.connected_clients: user = ext.db.users.find_one({'_id': ObjectId(user_id)}) ext.socketio.emit('query_update', { 'query_count': 0, 'query_limit': user.get('query_limit', 10) }, room=ext.connected_clients[user_id]) logging.info(f"Broadcasted query update to user {user_id} after reset") return jsonify({ 'message': 'Reset lượt hỏi đáp thành công', 'user': { 'id': user_id, 'username': user['username'] } }), 200 except Exception as e: logging.error(f"Error resetting user query count: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/user//toggle_active', methods=['POST']) @admin_required def toggle_user_active(user_id): try: if not ObjectId.is_valid(user_id): return jsonify({'error': 'ID người dùng không hợp lệ'}), 400 user = ext.db.users.find_one({'_id': ObjectId(user_id)}) if not user: return jsonify({'error': 'Người dùng không tồn tại'}), 404 # Prevent deactivating own account if str(user['_id']) == session.get('user_id'): return jsonify({'error': 'Không thể vô hiệu hóa tài khoản của chính bạn'}), 403 new_status = not user.get('is_active', False) ext.db.users.update_one( {'_id': ObjectId(user_id)}, {'$set': { 'is_active': new_status, 'status_changed_at': datetime.utcnow(), 'status_changed_by': session.get('user_id') }} ) # Send notification email status_text = "kích hoạt" if new_status else "vô hiệu hóa" send_email( user['email'], f'Tài khoản đã được {status_text}', f'''Tài khoản của bạn ({user["username"]}) đã được admin {status_text}. Trạng thái hiện tại: {"Đang hoạt động" if new_status else "Đã vô hiệu hóa"}''' ) logging.info(f"Admin {session.get('user_id')} set user {user_id} active status to {new_status}") return jsonify({ 'message': f'Đã {status_text} tài khoản thành công', 'is_active': new_status, 'user': { 'id': user_id, 'username': user['username'] } }), 200 except Exception as e: logging.error(f"Error toggling user active status: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500 @admin_bp.route('/admin/statistics', methods=['GET']) @admin_required def get_statistics(): try: # User statistics total_users = ext.db.users.count_documents({}) active_users = ext.db.users.count_documents({'is_active': True}) new_users_today = ext.db.users.count_documents({ 'created_at': {'$gte': datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)} }) # Query statistics total_queries = 0 users = ext.db.users.find({}) for user in users: total_queries += user.get('query_count', 0) # Conversation statistics total_conversations = ext.db.conversations.count_documents({}) conversations_today = ext.db.conversations.count_documents({ 'timestamp': {'$gte': datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)} }) # Message statistics total_messages = ext.db.messages.count_documents({}) messages_today = ext.db.messages.count_documents({ 'timestamp': {'$gte': datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)} }) # Account type distribution limited_users = ext.db.users.count_documents({'account_type': 'limited'}) unlimited_users = ext.db.users.count_documents({'account_type': 'unlimited'}) # Daily activity (last 7 days) daily_activity = [] for i in range(6, -1, -1): date = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) start_date = date - timedelta(days=i) end_date = start_date + timedelta(days=1) daily_users = ext.db.users.count_documents({ 'created_at': {'$gte': start_date, '$lt': end_date} }) daily_conversations = ext.db.conversations.count_documents({ 'timestamp': {'$gte': start_date, '$lt': end_date} }) daily_activity.append({ 'date': start_date.strftime('%Y-%m-%d'), 'new_users': daily_users, 'new_conversations': daily_conversations }) return jsonify({ 'statistics': { 'users': { 'total': total_users, 'active': active_users, 'new_today': new_users_today, 'limited': limited_users, 'unlimited': unlimited_users }, 'queries': { 'total': total_queries, 'average_per_user': total_queries / total_users if total_users > 0 else 0 }, 'conversations': { 'total': total_conversations, 'today': conversations_today }, 'messages': { 'total': total_messages, 'today': messages_today } }, 'daily_activity': daily_activity, 'timestamp': datetime.utcnow().isoformat() }), 200 except Exception as e: logging.error(f"Error getting statistics: {e}") return jsonify({'error': 'Lỗi hệ thống'}), 500