Spaces:
Sleeping
Sleeping
| 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): | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 |