Update files
This commit is contained in:
parent
34fdbe17ba
commit
5dabbcd690
|
|
@ -0,0 +1,2 @@
|
|||
# Middleware package
|
||||
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
Telegram Mini App authentication middleware
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import Request, HTTPException, status
|
||||
from database import users_collection
|
||||
from utils.telegram_initdata import validate_init_data, normalize_telegram_user
|
||||
from config import settings
|
||||
|
||||
|
||||
async def authenticate_telegram_moderation(request: Request) -> Optional[dict]:
|
||||
"""
|
||||
Authenticate user via Telegram Mini App initData
|
||||
|
||||
Returns:
|
||||
User dict if authenticated, None otherwise
|
||||
|
||||
Raises:
|
||||
HTTPException: If authentication fails
|
||||
"""
|
||||
# Get initData from headers
|
||||
auth_header = request.headers.get('authorization', '')
|
||||
init_data_raw = None
|
||||
|
||||
if auth_header.startswith('tma '):
|
||||
init_data_raw = auth_header[4:].strip()
|
||||
|
||||
if not init_data_raw:
|
||||
init_data_raw = request.headers.get('x-telegram-init-data')
|
||||
if init_data_raw:
|
||||
init_data_raw = init_data_raw.strip()
|
||||
|
||||
if not init_data_raw:
|
||||
print('[TelegramAuth] No initData found in headers')
|
||||
return None
|
||||
|
||||
try:
|
||||
# Validate initData
|
||||
payload = validate_init_data(init_data_raw, settings.MODERATION_BOT_TOKEN)
|
||||
|
||||
telegram_user = payload.get('user')
|
||||
if not telegram_user:
|
||||
print('[TelegramAuth] No user data in initData')
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Отсутствуют данные пользователя в initData'
|
||||
)
|
||||
|
||||
# Normalize user data
|
||||
normalized_user = normalize_telegram_user(telegram_user)
|
||||
telegram_id = normalized_user.get('id')
|
||||
|
||||
if not telegram_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Неверный ID пользователя'
|
||||
)
|
||||
|
||||
print(f'[TelegramAuth] Validated initData for telegramId={telegram_id}')
|
||||
|
||||
# Find or create user
|
||||
from bson import ObjectId
|
||||
user = await users_collection().find_one({'telegramId': telegram_id})
|
||||
|
||||
if not user:
|
||||
# Create new user
|
||||
from datetime import datetime
|
||||
user_doc = {
|
||||
'telegramId': telegram_id,
|
||||
'username': normalized_user.get('username') or normalized_user.get('firstName') or 'user',
|
||||
'firstName': normalized_user.get('firstName'),
|
||||
'lastName': normalized_user.get('lastName'),
|
||||
'photoUrl': normalized_user.get('photoUrl'),
|
||||
'role': 'user',
|
||||
'createdAt': datetime.utcnow()
|
||||
}
|
||||
result = await users_collection().insert_one(user_doc)
|
||||
user = await users_collection().find_one({'_id': result.inserted_id})
|
||||
print(f'[TelegramAuth] Created new user: {telegram_id}')
|
||||
else:
|
||||
# Update user data if needed
|
||||
update_fields = {}
|
||||
if normalized_user.get('username') and not user.get('username'):
|
||||
update_fields['username'] = normalized_user.get('username')
|
||||
if normalized_user.get('firstName') and not user.get('firstName'):
|
||||
update_fields['firstName'] = normalized_user.get('firstName')
|
||||
if normalized_user.get('lastName') is not None:
|
||||
update_fields['lastName'] = normalized_user.get('lastName')
|
||||
if normalized_user.get('photoUrl') and not user.get('photoUrl'):
|
||||
update_fields['photoUrl'] = normalized_user.get('photoUrl')
|
||||
|
||||
if update_fields:
|
||||
from datetime import datetime
|
||||
update_fields['lastActiveAt'] = datetime.utcnow()
|
||||
await users_collection().update_one(
|
||||
{'_id': user['_id']},
|
||||
{'$set': update_fields}
|
||||
)
|
||||
user.update(update_fields)
|
||||
|
||||
if user.get('banned'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Пользователь заблокирован'
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
print(f'[TelegramAuth] Validation error: {e}')
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f'{str(e)}. Используйте официальный клиент.'
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'[TelegramAuth] Error: {type(e).__name__}: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Ошибка авторизации. Используйте официальный клиент.'
|
||||
)
|
||||
|
||||
|
|
@ -39,12 +39,12 @@ class LoginRequest(BaseModel):
|
|||
|
||||
class TelegramWidgetAuth(BaseModel):
|
||||
id: int
|
||||
first_name: str
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
photo_url: Optional[str] = None
|
||||
auth_date: int
|
||||
hash: str
|
||||
auth_date: Optional[int] = None
|
||||
hash: Optional[str] = None
|
||||
|
||||
|
||||
# User models
|
||||
|
|
|
|||
|
|
@ -315,23 +315,31 @@ async def login(request: LoginRequest, response: Response):
|
|||
@router.post("/telegram-widget")
|
||||
async def telegram_widget_auth(request: TelegramWidgetAuth, response: Response):
|
||||
"""Authenticate via Telegram Login Widget"""
|
||||
print(f"[ModerationAuth] 🔍 Запрос авторизации через Telegram виджет: id={request.id}, username={request.username}")
|
||||
|
||||
try:
|
||||
# Find user by telegramId
|
||||
print(f"[ModerationAuth] Поиск пользователя с telegramId={request.id}")
|
||||
user = await users_collection().find_one({'telegramId': str(request.id)})
|
||||
|
||||
if not user:
|
||||
print(f"[ModerationAuth] ❌ Пользователь не найден с telegramId={request.id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Пользователь не найден. Сначала зарегистрируйтесь через бота."
|
||||
)
|
||||
|
||||
print(f"[ModerationAuth] ✅ Пользователь найден: username={user.get('username')}, role={user.get('role')}")
|
||||
|
||||
if user.get('role') not in ['moderator', 'admin']:
|
||||
print(f"[ModerationAuth] ❌ Недостаточно прав: role={user.get('role')}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Доступ запрещен. У вас нет прав модератора."
|
||||
)
|
||||
|
||||
if user.get('banned'):
|
||||
print(f"[ModerationAuth] ❌ Аккаунт заблокирован")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Аккаунт заблокирован"
|
||||
|
|
@ -377,10 +385,13 @@ async def telegram_widget_auth(request: TelegramWidgetAuth, response: Response):
|
|||
"accessToken": access_token
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
except HTTPException as e:
|
||||
print(f"[ModerationAuth] ❌ HTTP ошибка при авторизации через виджет: {e.status_code} - {e.detail}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[ModerationAuth] Ошибка авторизации через виджет: {e}")
|
||||
print(f"[ModerationAuth] ❌ Ошибка авторизации через виджет: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Ошибка сервера"
|
||||
|
|
@ -427,7 +438,82 @@ async def get_current_user_info(user: dict = Depends(get_current_user)):
|
|||
|
||||
|
||||
@router.post("/telegram")
|
||||
async def telegram_auth_alias(request: TelegramWidgetAuth, response: Response):
|
||||
"""Alias for /telegram-widget for compatibility with frontend"""
|
||||
return await telegram_widget_auth(request, response)
|
||||
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="Ошибка сервера"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,10 @@ async def send_email_smtp(to: str, subject: str, html: str, text: Optional[str]
|
|||
msg.attach(MIMEText(html, 'html', 'utf-8'))
|
||||
|
||||
# Connect and send
|
||||
if settings.EMAIL_PROVIDER == 'yandex':
|
||||
logger.info(f"[Email] Отправка через Yandex SMTP: {settings.YANDEX_SMTP_HOST}:{settings.YANDEX_SMTP_PORT}")
|
||||
# Поддерживаем 'yandex' и 'smtp' как алиасы
|
||||
email_provider = settings.EMAIL_PROVIDER.lower()
|
||||
if email_provider in ['yandex', 'smtp']:
|
||||
logger.info(f"[Email] Отправка через SMTP ({email_provider}): {settings.YANDEX_SMTP_HOST}:{settings.YANDEX_SMTP_PORT}")
|
||||
|
||||
if not settings.YANDEX_SMTP_USER or not settings.YANDEX_SMTP_PASSWORD:
|
||||
raise ValueError("YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD должны быть установлены в .env")
|
||||
|
|
@ -79,7 +81,7 @@ async def send_email_smtp(to: str, subject: str, html: str, text: Optional[str]
|
|||
logger.info(f"✅ Email отправлен на {to}")
|
||||
return {"success": True, "to": to}
|
||||
else:
|
||||
raise ValueError(f"Email provider '{settings.EMAIL_PROVIDER}' не поддерживается. Используйте 'yandex'")
|
||||
raise ValueError(f"Email provider '{settings.EMAIL_PROVIDER}' не поддерживается. Используйте 'yandex' или 'smtp'")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка отправки email: {e}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
"""
|
||||
Telegram Mini App initData validation
|
||||
"""
|
||||
import hmac
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
from typing import Optional, Dict, Any
|
||||
from config import settings
|
||||
|
||||
MAX_AUTH_AGE_SECONDS = 60 * 60 # 1 час
|
||||
AUTH_AGE_TOLERANCE_SECONDS = 300 # 5 минут допуск
|
||||
|
||||
|
||||
def validate_init_data(init_data_raw: str, bot_token: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate and parse Telegram Mini App initData
|
||||
|
||||
Args:
|
||||
init_data_raw: Raw initData string from Telegram
|
||||
bot_token: Bot token (uses MODERATION_BOT_TOKEN if not provided)
|
||||
|
||||
Returns:
|
||||
Parsed initData payload with user data
|
||||
|
||||
Raises:
|
||||
ValueError: If validation fails
|
||||
"""
|
||||
token_to_use = bot_token or settings.MODERATION_BOT_TOKEN
|
||||
|
||||
if not token_to_use or not isinstance(token_to_use, str) or not token_to_use.strip():
|
||||
error_msg = (
|
||||
'Bot token модерации не настроен (MODERATION_BOT_TOKEN)'
|
||||
if bot_token
|
||||
else 'Bot token не настроен (TELEGRAM_BOT_TOKEN)'
|
||||
)
|
||||
print(f'[Telegram] Token error: {error_msg}')
|
||||
raise ValueError(error_msg)
|
||||
|
||||
if not init_data_raw or not isinstance(init_data_raw, str):
|
||||
raise ValueError('initData не передан')
|
||||
|
||||
print(f'[Telegram] validate_init_data called: hasInitData={bool(init_data_raw)}, length={len(init_data_raw)}')
|
||||
|
||||
# Parse query string
|
||||
params = urllib.parse.parse_qs(init_data_raw)
|
||||
|
||||
# Get hash
|
||||
hash_value = params.get('hash', [None])[0]
|
||||
if not hash_value:
|
||||
raise ValueError('Отсутствует hash в initData')
|
||||
|
||||
# Remove hash from params
|
||||
params_without_hash = {k: v[0] for k, v in params.items() if k != 'hash'}
|
||||
|
||||
# Create data check string (sorted keys)
|
||||
data_check_arr = sorted(params_without_hash.items())
|
||||
data_check_string = '\n'.join(f'{key}={value}' for key, value in data_check_arr)
|
||||
|
||||
# Create secret key
|
||||
secret_key = hmac.new(
|
||||
'WebAppData'.encode(),
|
||||
token_to_use.encode(),
|
||||
hashlib.sha256
|
||||
).digest()
|
||||
|
||||
# Create signature
|
||||
signature = hmac.new(
|
||||
secret_key,
|
||||
data_check_string.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Compare signatures
|
||||
if signature != hash_value:
|
||||
print(f'[Telegram] Hash mismatch: calculated={signature[:20]}..., received={hash_value[:20]}...')
|
||||
raise ValueError('Неверная подпись initData')
|
||||
|
||||
# Check auth_date if present
|
||||
auth_date_str = params_without_hash.get('auth_date')
|
||||
if auth_date_str:
|
||||
try:
|
||||
auth_date = int(auth_date_str)
|
||||
import time
|
||||
now = int(time.time())
|
||||
age = abs(now - auth_date)
|
||||
|
||||
if age > MAX_AUTH_AGE_SECONDS:
|
||||
# Allow small tolerance in development
|
||||
if settings.IS_DEVELOPMENT and age <= MAX_AUTH_AGE_SECONDS + AUTH_AGE_TOLERANCE_SECONDS:
|
||||
print(f'⚠️ InitData expired ({age}s), but allowing due to tolerance in development.')
|
||||
else:
|
||||
raise ValueError(f'Данные авторизации устарели (возраст: {age}с, макс: {MAX_AUTH_AGE_SECONDS}с)')
|
||||
except ValueError:
|
||||
pass # Invalid auth_date, skip check
|
||||
|
||||
# Parse user data if present
|
||||
user_str = params_without_hash.get('user')
|
||||
user_data = None
|
||||
if user_str:
|
||||
import json
|
||||
try:
|
||||
user_data = json.loads(user_str)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError('Неверный формат user данных в initData')
|
||||
|
||||
print('[Telegram] initData validation complete')
|
||||
|
||||
return {
|
||||
'user': user_data,
|
||||
'auth_date': auth_date_str,
|
||||
'hash': hash_value,
|
||||
'raw': params_without_hash
|
||||
}
|
||||
|
||||
|
||||
def normalize_telegram_user(user_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Normalize Telegram user data (handle both camelCase and snake_case)"""
|
||||
if not user_data:
|
||||
return {}
|
||||
|
||||
normalized = {}
|
||||
|
||||
# ID
|
||||
normalized['id'] = str(user_data.get('id') or user_data.get('id'))
|
||||
|
||||
# Username
|
||||
normalized['username'] = user_data.get('username') or user_data.get('username')
|
||||
|
||||
# First name
|
||||
normalized['firstName'] = user_data.get('first_name') or user_data.get('firstName')
|
||||
|
||||
# Last name
|
||||
normalized['lastName'] = user_data.get('last_name') or user_data.get('lastName')
|
||||
|
||||
# Photo URL
|
||||
normalized['photoUrl'] = user_data.get('photo_url') or user_data.get('photoUrl')
|
||||
|
||||
return normalized
|
||||
|
||||
Loading…
Reference in New Issue