2025-12-14 23:45:41 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Moderation app routes - main moderation functionality
|
|
|
|
|
|
"""
|
2025-12-15 00:15:53 +00:00
|
|
|
|
import secrets
|
2025-12-14 23:45:41 +00:00
|
|
|
|
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':
|
|
|
|
|
|
query['banned'] = {'$ne': True}
|
|
|
|
|
|
elif filter == 'banned':
|
|
|
|
|
|
query['banned'] = True
|
|
|
|
|
|
# 'all' - no filter
|
|
|
|
|
|
|
|
|
|
|
|
skip = (page - 1) * limit
|
|
|
|
|
|
|
|
|
|
|
|
# Get users
|
|
|
|
|
|
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:
|
|
|
|
|
|
serialized_users.append({
|
|
|
|
|
|
'id': str(u['_id']),
|
|
|
|
|
|
'telegramId': u.get('telegramId'),
|
|
|
|
|
|
'username': u.get('username'),
|
|
|
|
|
|
'firstName': u.get('firstName'),
|
|
|
|
|
|
'lastName': u.get('lastName'),
|
|
|
|
|
|
'photoUrl': u.get('photoUrl'),
|
|
|
|
|
|
'role': u.get('role', 'user'),
|
|
|
|
|
|
'banned': u.get('banned', False),
|
|
|
|
|
|
'bannedUntil': u.get('bannedUntil').isoformat() if u.get('bannedUntil') else None,
|
|
|
|
|
|
'createdAt': u.get('createdAt', datetime.utcnow()).isoformat(),
|
|
|
|
|
|
'lastActiveAt': u.get('lastActiveAt').isoformat() if u.get('lastActiveAt') else None
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'users': serialized_users,
|
|
|
|
|
|
'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("/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')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 00:15:53 +00:00
|
|
|
|
# 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))
|
|
|
|
|
|
|
2025-12-14 23:45:41 +00:00
|
|
|
|
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 []),
|
2025-12-15 00:15:53 +00:00
|
|
|
|
'comments': comments_serialized,
|
|
|
|
|
|
'likes': likes_serialized,
|
|
|
|
|
|
'commentsCount': len(comments_serialized),
|
|
|
|
|
|
'likesCount': len(likes_serialized),
|
2025-12-14 23:45:41 +00:00
|
|
|
|
'isNSFW': post.get('isNSFW', False),
|
2025-12-15 00:15:53 +00:00
|
|
|
|
'isHomo': post.get('isHomo', False),
|
2025-12-14 23:45:41 +00:00
|
|
|
|
'isArt': post.get('isArt', False),
|
|
|
|
|
|
'publishedToChannel': post.get('publishedToChannel', False),
|
|
|
|
|
|
'adminNumber': post.get('adminNumber'),
|
2025-12-15 00:15:53 +00:00
|
|
|
|
'editedAt': post.get('editedAt').isoformat() if post.get('editedAt') else None,
|
2025-12-14 23:45:41 +00:00
|
|
|
|
'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="Пост не найден"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
update_data = {}
|
|
|
|
|
|
|
|
|
|
|
|
if request.content is not None:
|
|
|
|
|
|
update_data['content'] = request.content
|
|
|
|
|
|
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}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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="Ошибка сервера"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 00:15:53 +00:00
|
|
|
|
@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="Ошибка сервера"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-14 23:45:41 +00:00
|
|
|
|
@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="Пост не найден"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-15 00:15:53 +00:00
|
|
|
|
# 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}")
|
|
|
|
|
|
|
2025-12-14 23:45:41 +00:00
|
|
|
|
# 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="Ошибка получения списка админов"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 00:15:53 +00:00
|
|
|
|
@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="Ошибка удаления админа"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-14 23:45:41 +00:00
|
|
|
|
@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="Ошибка сервера"
|
|
|
|
|
|
)
|
|
|
|
|
|
|