fix: review findings — exception chaining, redundant unquote, validator tightening

- Add `from exc` to IntegrityError raise for consistent exception chaining
- Remove redundant unquote() in validate_telegram_init_data (parse_qsl already decodes)
- Tighten model_validator to require all 3 widget fields (id, auth_date, hash) together
- Extract _MAX_CLOCK_SKEW_SECONDS constant replacing magic number -300
- Use logger.exception() instead of logger.error(exc_info=True) in 2 places
This commit is contained in:
Fringg
2026-03-04 17:21:14 +03:00
parent da40d5662d
commit 467dea1315
2 changed files with 14 additions and 8 deletions

View File

@@ -5,11 +5,15 @@ import hmac
import json
from datetime import UTC, datetime
from typing import Any
from urllib.parse import parse_qsl, unquote
from urllib.parse import parse_qsl
from app.config import settings
# Maximum allowed clock skew (seconds) for auth_date — tolerates minor drift between Telegram servers and ours.
_MAX_CLOCK_SKEW_SECONDS = 300
def validate_telegram_login_widget(data: dict[str, Any], max_age_seconds: int = 86400) -> bool:
"""
Validate Telegram Login Widget data.
@@ -36,7 +40,7 @@ def validate_telegram_login_widget(data: dict[str, Any], max_age_seconds: int =
try:
auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC)
age = (datetime.now(UTC) - auth_time).total_seconds()
if age > max_age_seconds or age < -300:
if age > max_age_seconds or age < -_MAX_CLOCK_SKEW_SECONDS:
return False
except (ValueError, TypeError, OSError):
return False
@@ -83,7 +87,7 @@ def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) ->
try:
auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC)
age = (datetime.now(UTC) - auth_time).total_seconds()
if age > max_age_seconds or age < -300:
if age > max_age_seconds or age < -_MAX_CLOCK_SKEW_SECONDS:
return None
except (ValueError, TypeError, OSError):
return None
@@ -105,7 +109,7 @@ def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) ->
# Parse user data from the validated data
user_data_str = parsed.get('user')
if user_data_str:
user_data = json.loads(unquote(user_data_str))
user_data = json.loads(user_data_str)
return user_data
return parsed

View File

@@ -113,6 +113,8 @@ class LinkTelegramRequest(BaseModel):
raise ValueError('Provide either init_data or Login Widget fields, not both')
if not has_init and not has_widget:
raise ValueError('Provide either init_data or Login Widget fields (id, auth_date, hash)')
if has_widget and not (self.id is not None and self.auth_date is not None and self.hash is not None):
raise ValueError('Login Widget mode requires id, auth_date, and hash fields')
return self
@@ -498,12 +500,12 @@ async def link_telegram(
try:
await db.commit()
except IntegrityError:
except IntegrityError as exc:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail='This Telegram account was just linked to another user',
)
) from exc
logger.info(
'Telegram linked to account',
@@ -616,7 +618,7 @@ async def execute_merge_endpoint(
except Exception as exc:
await db.rollback()
await restore_merge_token(merge_token, consumed)
logger.error('Merge execution failed', exc_info=True)
logger.exception('Merge execution failed')
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Account merge failed due to an internal error',
@@ -635,7 +637,7 @@ async def execute_merge_endpoint(
auth_response = await _create_auth_response(merged_user, db)
await _store_refresh_token(db, merged_user.id, auth_response.refresh_token, device_info='merge')
except Exception as exc:
logger.error('Failed to create auth tokens after merge', exc_info=True)
logger.exception('Failed to create auth tokens after merge')
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Merge succeeded but failed to create new session',