nakama/moderation/backend-py/routes/moderation_auth.py

520 lines
19 KiB
Python
Raw Normal View History

2025-12-14 23:45:41 +00:00
"""
Moderation authentication routes
"""
import secrets
from datetime import datetime, timedelta
from typing import Optional
2025-12-14 23:56:36 +00:00
from fastapi import APIRouter, HTTPException, status, Response, Cookie, Depends, Request
2025-12-14 23:45:41 +00:00
from fastapi.responses import JSONResponse
from bson import ObjectId
from models import (
SendCodeRequest, RegisterRequest, LoginRequest, TelegramWidgetAuth,
UserResponse
)
from database import (
users_collection, email_verification_codes_collection,
moderation_admins_collection
)
from utils.auth import (
hash_password, verify_password,
create_access_token, create_refresh_token,
get_current_user, normalize_username, is_moderation_admin
)
from utils.email_service import send_verification_code
from config import settings
router = APIRouter()
def set_auth_cookies(response: Response, access_token: str, refresh_token: str):
"""Set authentication cookies"""
# Access token cookie (short-lived)
response.set_cookie(
key=settings.JWT_ACCESS_COOKIE_NAME,
value=access_token,
max_age=settings.JWT_ACCESS_EXPIRES_IN,
httponly=True,
secure=settings.IS_PRODUCTION,
samesite='lax'
)
# Refresh token cookie (long-lived)
response.set_cookie(
key=settings.JWT_REFRESH_COOKIE_NAME,
value=refresh_token,
max_age=settings.JWT_REFRESH_EXPIRES_IN,
httponly=True,
secure=settings.IS_PRODUCTION,
samesite='lax'
)
def clear_auth_cookies(response: Response):
"""Clear authentication cookies"""
response.delete_cookie(settings.JWT_ACCESS_COOKIE_NAME)
response.delete_cookie(settings.JWT_REFRESH_COOKIE_NAME)
@router.post("/send-code")
2025-12-14 23:56:36 +00:00
async def send_code(request: SendCodeRequest, http_request: Request = None):
2025-12-14 23:45:41 +00:00
"""Send verification code to email"""
try:
email_lower = request.email.lower().strip()
# Check if user exists with moderator/admin role
existing_user = await users_collection().find_one({
'email': email_lower,
'role': {'$in': ['moderator', 'admin']}
})
print(f"[ModerationAuth] Проверка пользователя для email {email_lower}: "
f"{{found: {existing_user is not None}, "
f"hasPassword: {bool(existing_user.get('passwordHash')) if existing_user else False}, "
f"role: {existing_user.get('role') if existing_user else None}}}")
# Allow sending code if user exists
if existing_user:
print(f"[ModerationAuth] Пользователь найден, отправка кода разрешена")
else:
# Check if user exists but without proper role
user_by_email = await users_collection().find_one({'email': email_lower})
if user_by_email:
print(f"[ModerationAuth] Пользователь найден, но роль не moderator/admin: {user_by_email.get('role')}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Регистрация недоступна. Обратитесь к администратору для получения доступа."
)
# No user found - allow for debugging
print(f"[ModerationAuth] Пользователь не найден, но отправка кода разрешена для {email_lower}")
# Generate 6-digit code
code = str(secrets.randbelow(900000) + 100000)
# Delete old codes
await email_verification_codes_collection().delete_many({
'email': email_lower,
'purpose': 'registration'
})
# Save new code (valid for 15 minutes)
await email_verification_codes_collection().insert_one({
'email': email_lower,
'code': code,
'purpose': 'registration',
'verified': False,
'expiresAt': datetime.utcnow() + timedelta(minutes=15),
'createdAt': datetime.utcnow()
})
# Send code via email
try:
await send_verification_code(email_lower, code)
return {"success": True, "message": "Код подтверждения отправлен на email"}
except ValueError as email_error:
# Delete code if email failed
await email_verification_codes_collection().delete_many({
'email': email_lower,
'code': code
})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(email_error)
)
except Exception as email_error:
await email_verification_codes_collection().delete_many({
'email': email_lower,
'code': code
})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Не удалось отправить код на email: {str(email_error)}"
)
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка отправки кода: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/register")
async def register(request: RegisterRequest, response: Response):
"""Register with email verification code"""
try:
email_lower = request.email.lower().strip()
# Find verification code
verification_code = await email_verification_codes_collection().find_one({
'email': email_lower,
'code': request.code,
'purpose': 'registration',
'verified': False
})
if not verification_code:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный или истекший код"
)
# Check expiration
if datetime.utcnow() > verification_code['expiresAt']:
await email_verification_codes_collection().delete_one({'_id': verification_code['_id']})
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Код истек. Запросите новый."
)
# Find user (must be created by administrator)
user = await users_collection().find_one({
'email': email_lower,
'role': {'$in': ['moderator', 'admin']}
})
if not user:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Регистрация недоступна. Обратитесь к администратору."
)
# Check if user already has password
if user.get('passwordHash'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Аккаунт уже зарегистрирован. Используйте вход по паролю."
)
# Check password strength
if len(request.password) < 6:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пароль должен содержать минимум 6 символов"
)
# Hash password
password_hash = hash_password(request.password)
# Update user
await users_collection().update_one(
{'_id': user['_id']},
{
'$set': {
'passwordHash': password_hash,
'emailVerified': True,
'username': request.username or user.get('username')
}
}
)
# Mark code as verified
await email_verification_codes_collection().update_one(
{'_id': verification_code['_id']},
{'$set': {'verified': True}}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
return {
"success": True,
"user": {
"id": user_id_str,
"username": request.username or user.get('username'),
"role": user.get('role')
},
"accessToken": access_token
}
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка регистрации: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/login")
async def login(request: LoginRequest, response: Response):
"""Login with email and password"""
try:
email_lower = request.email.lower().strip()
# Find user with password
user = await users_collection().find_one({
'email': email_lower,
'passwordHash': {'$exists': True, '$ne': None},
'role': {'$in': ['moderator', 'admin']}
})
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный email или пароль"
)
if user.get('banned'):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Аккаунт заблокирован"
)
# Verify password
if not verify_password(request.password, user['passwordHash']):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный email или пароль"
)
# Update last active
await users_collection().update_one(
{'_id': user['_id']},
{'$set': {'lastActiveAt': datetime.utcnow()}}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
return {
"success": True,
"user": {
"id": user_id_str,
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
},
"accessToken": access_token
}
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] Ошибка авторизации: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/telegram-widget")
async def telegram_widget_auth(request: TelegramWidgetAuth, response: Response):
"""Authenticate via Telegram Login Widget"""
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] 🔍 Запрос авторизации через Telegram виджет: id={request.id}, username={request.username}")
2025-12-14 23:45:41 +00:00
try:
# Find user by telegramId
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] Поиск пользователя с telegramId={request.id}")
2025-12-14 23:45:41 +00:00
user = await users_collection().find_one({'telegramId': str(request.id)})
if not user:
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] ❌ Пользователь не найден с telegramId={request.id}")
2025-12-14 23:45:41 +00:00
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден. Сначала зарегистрируйтесь через бота."
)
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] ✅ Пользователь найден: username={user.get('username')}, role={user.get('role')}")
2025-12-14 23:45:41 +00:00
if user.get('role') not in ['moderator', 'admin']:
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] ❌ Недостаточно прав: role={user.get('role')}")
2025-12-14 23:45:41 +00:00
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен. У вас нет прав модератора."
)
if user.get('banned'):
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] ❌ Аккаунт заблокирован")
2025-12-14 23:45:41 +00:00
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Аккаунт заблокирован"
)
# Update user data from widget
update_fields = {}
if request.username and not user.get('username'):
update_fields['username'] = request.username
if request.first_name and not user.get('firstName'):
update_fields['firstName'] = request.first_name
if request.last_name and not user.get('lastName'):
update_fields['lastName'] = request.last_name
if request.photo_url and not user.get('photoUrl'):
update_fields['photoUrl'] = request.photo_url
update_fields['lastActiveAt'] = datetime.utcnow()
if update_fields:
await users_collection().update_one(
{'_id': user['_id']},
{'$set': update_fields}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
print(f"[ModerationAuth] Успешная авторизация через виджет: {user.get('username')}")
return {
"success": True,
"user": {
"id": user_id_str,
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
},
"accessToken": access_token
}
2025-12-15 00:04:03 +00:00
except HTTPException as e:
print(f"[ModerationAuth] ❌ HTTP ошибка при авторизации через виджет: {e.status_code} - {e.detail}")
2025-12-14 23:45:41 +00:00
raise
except Exception as e:
2025-12-15 00:04:03 +00:00
print(f"[ModerationAuth] ❌ Ошибка авторизации через виджет: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
2025-12-14 23:45:41 +00:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
@router.post("/logout")
async def logout(response: Response):
"""Logout and clear cookies"""
clear_auth_cookies(response)
return {"success": True}
@router.get("/config")
async def get_config():
"""Get configuration for frontend"""
bot_username = settings.MODERATION_BOT_USERNAME
# If not set, try to get from Bot API (simplified - can add full implementation)
if not bot_username:
bot_username = "moderation_bot"
return {"botUsername": bot_username}
@router.get("/me")
async def get_current_user_info(user: dict = Depends(get_current_user)):
"""Get current authenticated user info"""
if user.get('role') not in ['moderator', 'admin']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен"
)
return {
"success": True,
"user": {
"id": str(user['_id']),
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
}
}
2025-12-14 23:56:36 +00:00
@router.post("/telegram")
2025-12-15 00:04:03 +00:00
async def telegram_miniapp_auth(request: Request, response: Response):
"""Authenticate via Telegram Mini App initData"""
print(f"[ModerationAuth] 🔍 Запрос авторизации через Telegram Mini App (initData)")
from middleware.telegram_auth import authenticate_telegram_moderation
from utils.auth import require_moderator, require_owner
try:
# Authenticate via initData
user = await authenticate_telegram_moderation(request)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Отсутствует initData. Используйте официальный клиент."
)
print(f"[ModerationAuth] ✅ Пользователь найден: username={user.get('username')}, role={user.get('role')}")
# Check moderation access
username = user.get('username', '').lower()
is_owner = username in settings.OWNER_USERNAMES_LIST
is_admin_by_role = user.get('role') in ['moderator', 'admin']
# Check if user is moderation admin in DB
from database import moderation_admins_collection
is_admin_by_db = await moderation_admins_collection().find_one({
'$or': [
{'telegramId': user.get('telegramId')},
{'username': username}
]
}) is not None
if not is_owner and not is_admin_by_role and not is_admin_by_db:
print(f"[ModerationAuth] ❌ Недостаточно прав: role={user.get('role')}, isOwner={is_owner}, isAdminByDB={is_admin_by_db}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступ запрещен. У вас нет прав модератора. Обратитесь к администратору."
)
# Update last active
from datetime import datetime
await users_collection().update_one(
{'_id': user['_id']},
{'$set': {'lastActiveAt': datetime.utcnow()}}
)
# Generate tokens
user_id_str = str(user['_id'])
access_token = create_access_token(user_id_str)
refresh_token = create_refresh_token(user_id_str)
# Set cookies
set_auth_cookies(response, access_token, refresh_token)
print(f"[ModerationAuth] ✅ Успешная авторизация через Mini App: {user.get('username')}")
return {
"success": True,
"user": {
"id": user_id_str,
"username": user.get('username'),
"role": user.get('role'),
"telegramId": user.get('telegramId')
},
"accessToken": access_token
}
except HTTPException:
raise
except Exception as e:
print(f"[ModerationAuth] ❌ Ошибка авторизации через Mini App: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ошибка сервера"
)
2025-12-14 23:56:36 +00:00