""" Moderation app routes - main moderation functionality """ import secrets from datetime import datetime, timedelta from typing import Optional, List from fastapi import APIRouter, HTTPException, status, Depends, Query from bson import ObjectId from models import ( UserResponse, PostResponse, ReportResponse, AdminResponse, UpdatePostRequest, UpdateReportRequest, BanUserRequest ) from database import ( users_collection, posts_collection, reports_collection, moderation_admins_collection ) from utils.auth import require_moderator, require_owner, normalize_username router = APIRouter() @router.get("/users") async def get_users( filter: str = Query('active', description="Filter: active, banned, all"), page: int = Query(1, ge=1), limit: int = Query(50, ge=1, le=100), user: dict = Depends(require_moderator) ): """Get users list with filtering""" try: query = {} if filter == 'active': # Активные пользователи: не забанены И активны за последние 7 дней seven_days_ago = datetime.utcnow() - timedelta(days=7) query['banned'] = {'$ne': True} query['lastActiveAt'] = {'$gte': seven_days_ago, '$exists': True, '$ne': None} elif filter == 'inactive': # Неактивные пользователи: не забанены И не активны более 7 дней ИЛИ нет lastActiveAt seven_days_ago = datetime.utcnow() - timedelta(days=7) query['banned'] = {'$ne': True} query['$or'] = [ {'lastActiveAt': {'$lt': seven_days_ago}}, {'lastActiveAt': {'$exists': False}}, {'lastActiveAt': None} ] elif filter == 'banned': query['banned'] = True # 'all' - no filter skip = (page - 1) * limit # Get users - сортируем по дате последнего входа (lastActiveAt) # Для активных и неактивных сортируем по lastActiveAt, для остальных по createdAt if filter in ['active', 'inactive']: # Сортируем по lastActiveAt (сначала самые активные/недавние) # Пользователи без lastActiveAt идут в конец cursor = users_collection().find(query).sort([ ('lastActiveAt', -1), # Сначала по lastActiveAt (убывание) ('createdAt', -1) # Потом по createdAt для пользователей без lastActiveAt ]).skip(skip).limit(limit) else: # Для banned и all сортируем по createdAt cursor = users_collection().find(query).sort('createdAt', -1).skip(skip).limit(limit) users = await cursor.to_list(length=limit) # Get total count total = await users_collection().count_documents(query) # Serialize users serialized_users = [] for u in users: # Проверяем, является ли пользователь админом модерации is_admin = False if u.get('telegramId'): admin = await moderation_admins_collection().find_one({ 'telegramId': str(u.get('telegramId')) }) is_admin = admin is not None # Обработка дат banned_until = None if u.get('bannedUntil'): if isinstance(u.get('bannedUntil'), datetime): banned_until = u.get('bannedUntil').isoformat() else: banned_until = str(u.get('bannedUntil')) created_at = datetime.utcnow().isoformat() if u.get('createdAt'): if isinstance(u.get('createdAt'), datetime): created_at = u.get('createdAt').isoformat() else: created_at = str(u.get('createdAt')) last_active_at = None if u.get('lastActiveAt'): if isinstance(u.get('lastActiveAt'), datetime): last_active_at = u.get('lastActiveAt').isoformat() else: last_active_at = str(u.get('lastActiveAt')) serialized_users.append({ 'id': str(u['_id']), 'telegramId': str(u.get('telegramId')) if u.get('telegramId') else None, 'username': u.get('username', ''), 'firstName': u.get('firstName', ''), 'lastName': u.get('lastName', ''), 'photoUrl': u.get('photoUrl', ''), 'role': u.get('role', 'user'), 'banned': bool(u.get('banned', False)), 'bannedUntil': banned_until, 'createdAt': created_at, 'lastActiveAt': last_active_at, 'referralsCount': int(u.get('referralsCount', 0)), 'isAdmin': is_admin }) return { 'users': serialized_users, 'total': total, 'totalPages': (total + limit - 1) // limit, 'currentPage': page } except Exception as e: print(f"[ModApp] ❌ Ошибка получения пользователей: {type(e).__name__}: {e}") import traceback traceback.print_exc() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Ошибка сервера: {str(e)}" ) @router.put("/users/{user_id}/ban") async def ban_user( user_id: str, request: BanUserRequest, current_user: dict = Depends(require_moderator) ): """Ban or unban user""" try: target_user = await users_collection().find_one({'_id': ObjectId(user_id)}) if not target_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден" ) update_data = {'banned': request.banned} if request.banned and request.days: update_data['bannedUntil'] = datetime.utcnow() + timedelta(days=request.days) elif not request.banned: update_data['bannedUntil'] = None await users_collection().update_one( {'_id': ObjectId(user_id)}, {'$set': update_data} ) return {"success": True} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка бана пользователя: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.get("/posts") async def get_posts( page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100), author: Optional[str] = None, tag: Optional[str] = None, user: dict = Depends(require_moderator) ): """Get posts list with filtering""" try: query = {} if author: query['author'] = ObjectId(author) if tag: query['tags'] = tag skip = (page - 1) * limit # Get posts with populated author pipeline = [ {'$match': query}, {'$sort': {'createdAt': -1}}, {'$skip': skip}, {'$limit': limit}, { '$lookup': { 'from': 'users', 'localField': 'author', 'foreignField': '_id', 'as': 'author' } }, {'$unwind': {'path': '$author', 'preserveNullAndEmptyArrays': True}} ] posts = await posts_collection().aggregate(pipeline).to_list(length=limit) total = await posts_collection().count_documents(query) # Serialize posts serialized_posts = [] for post in posts: author_data = None if post.get('author'): author_data = { 'id': str(post['author']['_id']), 'username': post['author'].get('username'), 'firstName': post['author'].get('firstName'), 'lastName': post['author'].get('lastName'), 'photoUrl': post['author'].get('photoUrl') } serialized_posts.append({ 'id': str(post['_id']), 'author': author_data, 'content': post.get('content'), 'hashtags': post.get('hashtags', []), 'tags': post.get('tags', []), 'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []), 'commentsCount': len(post.get('comments', [])), 'likesCount': len(post.get('likes', [])), 'isNSFW': post.get('isNSFW', False), 'isArt': post.get('isArt', False), 'publishedToChannel': post.get('publishedToChannel', False), 'adminNumber': post.get('adminNumber'), 'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None, 'createdAt': post.get('createdAt', datetime.utcnow()).isoformat() }) return { 'posts': serialized_posts, 'total': total, 'totalPages': (total + limit - 1) // limit, 'currentPage': page } except Exception as e: print(f"[ModApp] Ошибка получения постов: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.get("/posts/{post_id}") async def get_post( post_id: str, user: dict = Depends(require_moderator) ): """Get single post with comments""" try: pipeline = [ {'$match': {'_id': ObjectId(post_id)}}, { '$lookup': { 'from': 'users', 'localField': 'author', 'foreignField': '_id', 'as': 'author' } }, {'$unwind': {'path': '$author', 'preserveNullAndEmptyArrays': True}} ] posts = await posts_collection().aggregate(pipeline).to_list(length=1) if not posts: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пост не найден" ) post = posts[0] # Serialize author_data = None if post.get('author'): author_data = { 'id': str(post['author']['_id']), 'username': post['author'].get('username'), 'firstName': post['author'].get('firstName'), 'lastName': post['author'].get('lastName'), 'photoUrl': post['author'].get('photoUrl') } # Serialize comments comments_serialized = [] for comment in post.get('comments', []): comment_author_id = comment.get('author') if isinstance(comment_author_id, ObjectId): comment_author_id = str(comment_author_id) elif isinstance(comment_author_id, dict): comment_author_id = str(comment_author_id.get('_id', '')) comments_serialized.append({ 'id': str(comment.get('_id', '')), 'author': comment_author_id, 'content': comment.get('content'), 'createdAt': comment.get('createdAt', datetime.utcnow()).isoformat() if comment.get('createdAt') else None, 'editedAt': comment.get('editedAt').isoformat() if comment.get('editedAt') else None }) # Serialize likes (convert ObjectIds to strings) likes_serialized = [] for like in post.get('likes', []): if isinstance(like, ObjectId): likes_serialized.append(str(like)) elif isinstance(like, dict): likes_serialized.append(str(like.get('_id', ''))) else: likes_serialized.append(str(like)) return { 'post': { 'id': str(post['_id']), 'author': author_data, 'content': post.get('content'), 'hashtags': post.get('hashtags', []), 'tags': post.get('tags', []), 'images': post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []), 'comments': comments_serialized, 'likes': likes_serialized, 'commentsCount': len(comments_serialized), 'likesCount': len(likes_serialized), 'isNSFW': post.get('isNSFW', False), 'isHomo': post.get('isHomo', False), 'isArt': post.get('isArt', False), 'publishedToChannel': post.get('publishedToChannel', False), 'adminNumber': post.get('adminNumber'), 'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None, 'createdAt': post.get('createdAt', datetime.utcnow()).isoformat() } } except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка получения поста: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.put("/posts/{post_id}") async def update_post( post_id: str, request: UpdatePostRequest, user: dict = Depends(require_moderator) ): """Update post""" try: post = await posts_collection().find_one({'_id': ObjectId(post_id)}) if not post: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пост не найден" ) # Модераторы и админы могут редактировать любой пост через панель модерации # Доступ к панели модерации уже проверен через require_moderator update_data = {} if request.content is not None: update_data['content'] = request.content # Обновить хэштеги из контента, если hashtags не переданы явно if request.hashtags is not None: update_data['hashtags'] = [tag.replace('#', '').lower() if tag.startswith('#') else tag.lower() for tag in request.hashtags] else: # Извлечь хэштеги из контента (поддержка кириллицы) import re hashtags = re.findall(r'#([\wа-яА-ЯёЁ]+)', request.content) update_data['hashtags'] = [tag.lower() for tag in hashtags] if request.tags is not None: update_data['tags'] = [t.lower() for t in request.tags] if request.isNSFW is not None: update_data['isNSFW'] = request.isNSFW if request.isHomo is not None: update_data['isHomo'] = request.isHomo if request.isArt is not None: update_data['isArt'] = request.isArt if update_data: update_data['editedAt'] = datetime.utcnow() await posts_collection().update_one( {'_id': ObjectId(post_id)}, {'$set': update_data} ) # Get updated post with author updated_post = await posts_collection().find_one({'_id': ObjectId(post_id)}) author = None if updated_post.get('author'): author_doc = await users_collection().find_one({'_id': updated_post['author']}) if author_doc: author = { 'id': str(author_doc['_id']), 'username': author_doc.get('username'), 'firstName': author_doc.get('firstName'), 'lastName': author_doc.get('lastName') } return { "post": { "id": str(updated_post['_id']), "author": author, "content": updated_post.get('content'), "hashtags": updated_post.get('hashtags', []), "tags": updated_post.get('tags', []), "images": updated_post.get('images', []) or ([updated_post.get('imageUrl')] if updated_post.get('imageUrl') else []), "isNSFW": updated_post.get('isNSFW', False), "isArt": updated_post.get('isArt', False), "publishedToChannel": updated_post.get('publishedToChannel', False), "adminNumber": updated_post.get('adminNumber'), "editedAt": updated_post.get('editedAt').isoformat() if updated_post.get('editedAt') else None, "createdAt": updated_post.get('createdAt', datetime.utcnow()).isoformat() } } except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка обновления поста: {e}") import traceback traceback.print_exc() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.delete("/posts/{post_id}/comments/{comment_id}") async def delete_comment( post_id: str, comment_id: str, user: dict = Depends(require_moderator) ): """Delete comment from post""" try: post = await posts_collection().find_one({'_id': ObjectId(post_id)}) if not post: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пост не найден" ) # Find and remove comment comments = post.get('comments', []) comment_found = False for i, comment in enumerate(comments): if str(comment.get('_id')) == comment_id: comments.pop(i) comment_found = True break if not comment_found: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Комментарий не найден" ) # Update post await posts_collection().update_one( {'_id': ObjectId(post_id)}, {'$set': {'comments': comments}} ) # Get updated comments with populated authors updated_post = await posts_collection().find_one({'_id': ObjectId(post_id)}) comments_serialized = [] for comment in updated_post.get('comments', []): comment_author_id = comment.get('author') if isinstance(comment_author_id, ObjectId): # Populate author author = await users_collection().find_one({'_id': comment_author_id}) if author: comments_serialized.append({ 'id': str(comment.get('_id', '')), 'author': { 'id': str(author['_id']), 'username': author.get('username'), 'firstName': author.get('firstName'), 'lastName': author.get('lastName'), 'photoUrl': author.get('photoUrl') }, 'content': comment.get('content'), 'createdAt': comment.get('createdAt', datetime.utcnow()).isoformat() if comment.get('createdAt') else None }) return {"comments": comments_serialized} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка удаления комментария: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.delete("/posts/{post_id}/images/{index}") async def delete_post_image( post_id: str, index: int, user: dict = Depends(require_moderator) ): """Delete specific image from post""" try: post = await posts_collection().find_one({'_id': ObjectId(post_id)}) if not post: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пост не найден" ) images = post.get('images', []) or [] if index < 0 or index >= len(images): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Неверный индекс изображения" ) # Remove image removed_image = images.pop(index) # Update post update_data = {'images': images} if images: update_data['imageUrl'] = images[0] # Set first image as main else: update_data['imageUrl'] = None await posts_collection().update_one( {'_id': ObjectId(post_id)}, {'$set': update_data} ) # Delete from MinIO if removed_image and 'nakama-media/' in removed_image: from utils.minio_client import delete_file try: path_match = removed_image.split('nakama-media/') if len(path_match) > 1: file_path = path_match[-1] await delete_file(file_path) print(f"✅ Удалено изображение из MinIO: {file_path}") except Exception as e: print(f"❌ Ошибка удаления изображения из MinIO: {e}") return {"images": images} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка удаления изображения: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.post("/posts/{post_id}/ban") async def ban_post_author( post_id: str, request: dict, user: dict = Depends(require_moderator) ): """Ban post author""" try: days = request.get('days', 7) duration_days = max(int(days) if days else 7, 1) post = await posts_collection().find_one({'_id': ObjectId(post_id)}) if not post: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пост не найден" ) author_id = post.get('author') if not author_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Автор поста не найден" ) # Ban author await users_collection().update_one( {'_id': author_id}, { '$set': { 'banned': True, 'bannedUntil': datetime.utcnow() + timedelta(days=duration_days) } } ) # Get updated user banned_user = await users_collection().find_one({'_id': author_id}) return { "user": { "id": str(banned_user['_id']), "username": banned_user.get('username'), "firstName": banned_user.get('firstName'), "lastName": banned_user.get('lastName'), "banned": banned_user.get('banned', False), "bannedUntil": banned_user.get('bannedUntil').isoformat() if banned_user.get('bannedUntil') else None } } except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка бана автора: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.delete("/posts/{post_id}") async def delete_post( post_id: str, user: dict = Depends(require_moderator) ): """Delete post""" try: post = await posts_collection().find_one({'_id': ObjectId(post_id)}) if not post: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пост не найден" ) # Delete images from MinIO if needed from utils.minio_client import delete_file images = post.get('images', []) or ([post.get('imageUrl')] if post.get('imageUrl') else []) for image_url in images: if image_url and 'nakama-media/' in image_url: try: # Extract path from URL path_match = image_url.split('nakama-media/') if len(path_match) > 1: file_path = path_match[-1] await delete_file(file_path) print(f"✅ Удалено изображение из MinIO: {file_path}") except Exception as e: print(f"❌ Ошибка удаления изображения из MinIO: {e}") # Delete post await posts_collection().delete_one({'_id': ObjectId(post_id)}) return {"success": True, "message": "Пост удален"} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка удаления поста: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.get("/reports") async def get_reports( status_filter: str = Query('pending', alias='status'), page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100), user: dict = Depends(require_moderator) ): """Get reports list""" try: query = {} if status_filter != 'all': query['status'] = status_filter skip = (page - 1) * limit # Get reports with populated fields pipeline = [ {'$match': query}, {'$sort': {'createdAt': -1}}, {'$skip': skip}, {'$limit': limit}, { '$lookup': { 'from': 'users', 'localField': 'reporter', 'foreignField': '_id', 'as': 'reporter' } }, {'$unwind': {'path': '$reporter', 'preserveNullAndEmptyArrays': True}}, { '$lookup': { 'from': 'posts', 'localField': 'post', 'foreignField': '_id', 'as': 'post' } }, {'$unwind': {'path': '$post', 'preserveNullAndEmptyArrays': True}} ] reports = await reports_collection().aggregate(pipeline).to_list(length=limit) total = await reports_collection().count_documents(query) # Serialize serialized_reports = [] for report in reports: reporter_data = None if report.get('reporter'): reporter_data = { 'id': str(report['reporter']['_id']), 'username': report['reporter'].get('username'), 'firstName': report['reporter'].get('firstName') } post_data = None if report.get('post'): post_data = { 'id': str(report['post']['_id']), 'content': report['post'].get('content'), 'images': report['post'].get('images', []) } serialized_reports.append({ 'id': str(report['_id']), 'reporter': reporter_data, 'post': post_data, 'reason': report.get('reason'), 'status': report.get('status'), 'createdAt': report.get('createdAt', datetime.utcnow()).isoformat() }) return { 'reports': serialized_reports, 'total': total, 'totalPages': (total + limit - 1) // limit, 'currentPage': page } except Exception as e: print(f"[ModApp] Ошибка получения репортов: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.put("/reports/{report_id}") async def update_report( report_id: str, request: UpdateReportRequest, user: dict = Depends(require_moderator) ): """Update report status""" try: report = await reports_collection().find_one({'_id': ObjectId(report_id)}) if not report: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Репорт не найден" ) await reports_collection().update_one( {'_id': ObjectId(report_id)}, { '$set': { 'status': request.status, 'reviewedBy': user['_id'] } } ) return {"success": True} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка обновления репорта: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" ) @router.get("/admins") async def get_admins(user: dict = Depends(require_moderator)): """Get list of moderation admins""" try: admins = await moderation_admins_collection().find().sort('adminNumber', 1).to_list(length=100) serialized_admins = [] for admin in admins: serialized_admins.append({ 'id': str(admin['_id']), 'telegramId': admin.get('telegramId'), 'username': admin.get('username'), 'firstName': admin.get('firstName'), 'lastName': admin.get('lastName'), 'adminNumber': admin.get('adminNumber'), 'addedBy': admin.get('addedBy'), 'createdAt': admin.get('createdAt', datetime.utcnow()).isoformat() }) return {'admins': serialized_admins} except Exception as e: print(f"[ModApp] Ошибка получения админов: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка получения списка админов" ) @router.post("/admins/initiate-add") async def initiate_add_admin( request: dict, user: dict = Depends(require_owner) ): """Initiate adding admin (owner only)""" try: user_id = request.get('userId') admin_number = request.get('adminNumber') if not user_id or not admin_number: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Не указан ID пользователя или номер админа" ) if admin_number < 1 or admin_number > 10: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Номер админа должен быть от 1 до 10" ) # Check if number is taken existing_admin = await moderation_admins_collection().find_one({'adminNumber': admin_number}) if existing_admin: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Номер админа уже занят" ) # Check if user exists target_user = await users_collection().find_one({'_id': ObjectId(user_id)}) if not target_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден" ) # Check if already admin is_already_admin = await moderation_admins_collection().find_one({ 'telegramId': target_user.get('telegramId') }) if is_already_admin: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь уже является админом" ) # Generate code code = str(secrets.randbelow(900000) + 100000) # Save confirmation from database import admin_confirmations_collection await admin_confirmations_collection().insert_one({ 'userId': target_user.get('telegramId'), 'code': code, 'adminNumber': admin_number, 'action': 'add', 'expiresAt': datetime.utcnow() + timedelta(minutes=5), 'createdAt': datetime.utcnow() }) # Send code to owner email from utils.email_service import send_admin_confirmation_code try: await send_admin_confirmation_code( code, 'add', { 'username': target_user.get('username'), 'firstName': target_user.get('firstName'), 'adminNumber': admin_number } ) except Exception as email_error: print(f'[ModApp] Ошибка отправки кода на email: {email_error}') # Continue anyway - code is saved return { "success": True, "message": "Код подтверждения отправлен на email владельца", "username": target_user.get('username') } except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка инициирования добавления админа: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка отправки кода подтверждения" ) @router.post("/admins/confirm-add") async def confirm_add_admin( request: dict, user: dict = Depends(require_owner) ): """Confirm adding admin (owner only)""" try: user_id = request.get('userId') code = request.get('code') if not user_id or not code: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Не указан ID пользователя или код" ) target_user = await users_collection().find_one({'_id': ObjectId(user_id)}) if not target_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден" ) # Find confirmation from database import admin_confirmations_collection confirmation = await admin_confirmations_collection().find_one({ 'userId': target_user.get('telegramId'), 'code': code, 'action': 'add' }) if not confirmation: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Неверный код подтверждения" ) # Check expiration if datetime.utcnow() > confirmation['expiresAt']: await admin_confirmations_collection().delete_one({'_id': confirmation['_id']}) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Код истек" ) # Create admin await moderation_admins_collection().insert_one({ 'telegramId': target_user.get('telegramId'), 'username': target_user.get('username'), 'firstName': target_user.get('firstName'), 'lastName': target_user.get('lastName'), 'adminNumber': confirmation['adminNumber'], 'addedBy': str(user['_id']), 'createdAt': datetime.utcnow() }) # Delete confirmation await admin_confirmations_collection().delete_one({'_id': confirmation['_id']}) return {"success": True} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка подтверждения добавления админа: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка добавления админа" ) @router.post("/admins/initiate-remove") async def initiate_remove_admin( request: dict, user: dict = Depends(require_owner) ): """Initiate removing admin (owner only)""" try: admin_id = request.get('adminId') if not admin_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Не указан ID админа" ) admin = await moderation_admins_collection().find_one({'_id': ObjectId(admin_id)}) if not admin: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Администратор не найден" ) # Generate code code = str(secrets.randbelow(900000) + 100000) # Save confirmation from database import admin_confirmations_collection await admin_confirmations_collection().insert_one({ 'userId': admin.get('telegramId'), 'code': code, 'adminNumber': admin.get('adminNumber'), 'action': 'remove', 'expiresAt': datetime.utcnow() + timedelta(minutes=5), 'createdAt': datetime.utcnow() }) # Send code to owner email from utils.email_service import send_admin_confirmation_code try: await send_admin_confirmation_code( code, 'remove', { 'username': admin.get('username'), 'firstName': admin.get('firstName'), 'adminNumber': admin.get('adminNumber') } ) except Exception as email_error: print(f'[ModApp] Ошибка отправки кода на email: {email_error}') return { "success": True, "message": "Код подтверждения отправлен на email владельца", "username": admin.get('username') } except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка инициирования удаления админа: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка отправки кода подтверждения" ) @router.post("/admins/confirm-remove") async def confirm_remove_admin( request: dict, user: dict = Depends(require_owner) ): """Confirm removing admin (owner only)""" try: admin_id = request.get('adminId') code = request.get('code') if not admin_id or not code: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Не указан ID админа или код" ) admin = await moderation_admins_collection().find_one({'_id': ObjectId(admin_id)}) if not admin: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Администратор не найден" ) # Find confirmation from database import admin_confirmations_collection confirmation = await admin_confirmations_collection().find_one({ 'userId': admin.get('telegramId'), 'code': code, 'action': 'remove' }) if not confirmation: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Неверный код подтверждения" ) # Delete admin await moderation_admins_collection().delete_one({'_id': ObjectId(admin_id)}) # Delete confirmation await admin_confirmations_collection().delete_one({'_id': confirmation['_id']}) return {"success": True} except HTTPException: raise except Exception as e: print(f"[ModApp] Ошибка подтверждения удаления админа: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка удаления админа" ) @router.post("/auth/verify") async def verify_auth(user: dict = Depends(require_moderator)): """Verify authentication and get admin list""" try: # Get admin list admins = await moderation_admins_collection().find().sort('adminNumber', 1).to_list(length=100) admin_list = [] for admin in admins: admin_list.append({ 'adminNumber': admin.get('adminNumber'), 'username': admin.get('username'), 'firstName': admin.get('firstName'), 'telegramId': admin.get('telegramId') }) return { 'success': True, 'admins': admin_list, 'user': { 'id': str(user['_id']), 'username': user.get('username'), 'role': user.get('role') } } except Exception as e: print(f"[ModApp] Ошибка верификации: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Ошибка сервера" )