""" Moderation authentication routes """ import secrets from datetime import datetime, timedelta from typing import Optional from fastapi import APIRouter, HTTPException, status, Response, Cookie, Depends from fastapi.responses import JSONResponse from slowapi import Limiter from slowapi.util import get_remote_address 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() limiter = Limiter(key_func=get_remote_address) # Rate limiters AUTH_LIMITER = "5/15minutes" # 5 requests per 15 minutes CODE_LIMITER = "1/minute" # 1 request per minute 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") @limiter.limit(CODE_LIMITER) async def send_code(request: SendCodeRequest): """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") @limiter.limit(AUTH_LIMITER) 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") @limiter.limit(AUTH_LIMITER) 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") @limiter.limit(AUTH_LIMITER) async def telegram_widget_auth(request: TelegramWidgetAuth, response: Response): """Authenticate via Telegram Login Widget""" try: # Find user by telegramId user = await users_collection().find_one({'telegramId': str(request.id)}) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден. Сначала зарегистрируйтесь через бота." ) if user.get('role') not in ['moderator', 'admin']: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещен. У вас нет прав модератора." ) if user.get('banned'): 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 } except HTTPException: raise except Exception as e: print(f"[ModerationAuth] Ошибка авторизации через виджет: {e}") 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') } }