mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-06 14:03:07 +00:00
fix: reassign orphaned records on merge, eliminate TOCTOU race
- Reassign SubscriptionConversion, SubscriptionEvent, DiscountOffer from secondary to primary during merge (previously orphaned) - Consume-first pattern: atomically GETDEL merge token before validation, restore on invalid input (eliminates TOCTOU window)
This commit is contained in:
@@ -431,27 +431,7 @@ async def execute_merge_endpoint(
|
||||
db: AsyncSession = Depends(get_cabinet_db),
|
||||
) -> MergeResponse:
|
||||
"""Execute account merge. Consumes the merge token (one-time use)."""
|
||||
# 1. Read token data first (non-destructive) to validate request
|
||||
token_data = await get_merge_token_data(merge_token)
|
||||
if not token_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Merge token is invalid, expired, or already consumed',
|
||||
)
|
||||
|
||||
primary_user_id: int = token_data['primary_user_id']
|
||||
secondary_user_id: int = token_data['secondary_user_id']
|
||||
provider: str = token_data.get('provider', '')
|
||||
provider_id: str = token_data.get('provider_id', '')
|
||||
|
||||
# 2. Validate keep_subscription_from BEFORE consuming token
|
||||
if request.keep_subscription_from not in (primary_user_id, secondary_user_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='keep_subscription_from must be one of the two user IDs being merged',
|
||||
)
|
||||
|
||||
# 3. Consume token atomically (one-time use)
|
||||
# 1. Consume token atomically first (GETDEL — one-time use, no TOCTOU)
|
||||
consumed = await consume_merge_token(merge_token)
|
||||
if not consumed:
|
||||
raise HTTPException(
|
||||
@@ -459,6 +439,19 @@ async def execute_merge_endpoint(
|
||||
detail='Merge token is invalid, expired, or already consumed',
|
||||
)
|
||||
|
||||
primary_user_id: int = consumed['primary_user_id']
|
||||
secondary_user_id: int = consumed['secondary_user_id']
|
||||
provider: str = consumed.get('provider', '')
|
||||
provider_id: str = consumed.get('provider_id', '')
|
||||
|
||||
# 2. Validate keep_subscription_from — restore token if invalid
|
||||
if request.keep_subscription_from not in (primary_user_id, secondary_user_id):
|
||||
await restore_merge_token(merge_token, consumed)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='keep_subscription_from must be one of the two user IDs being merged',
|
||||
)
|
||||
|
||||
# Convert user_id to 'primary'/'secondary' string for execute_merge()
|
||||
keep_from: Literal['primary', 'secondary'] = 'primary' if request.keep_subscription_from == primary_user_id else 'secondary'
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.database.models import (
|
||||
CabinetRefreshToken,
|
||||
CloudPaymentsPayment,
|
||||
CryptoBotPayment,
|
||||
DiscountOffer,
|
||||
FreekassaPayment,
|
||||
HeleketPayment,
|
||||
KassaAiPayment,
|
||||
@@ -22,6 +23,8 @@ from app.database.models import (
|
||||
PlategaPayment,
|
||||
ReferralEarning,
|
||||
Subscription,
|
||||
SubscriptionConversion,
|
||||
SubscriptionEvent,
|
||||
Transaction,
|
||||
User,
|
||||
UserStatus,
|
||||
@@ -456,6 +459,17 @@ async def execute_merge(
|
||||
update(WithdrawalRequest).where(WithdrawalRequest.user_id == secondary.id).values(user_id=primary.id)
|
||||
)
|
||||
|
||||
# 10a. Переназначение subscription_conversions, subscription_events, discount_offers
|
||||
await db.execute(
|
||||
update(SubscriptionConversion).where(SubscriptionConversion.user_id == secondary.id).values(user_id=primary.id)
|
||||
)
|
||||
await db.execute(
|
||||
update(SubscriptionEvent).where(SubscriptionEvent.user_id == secondary.id).values(user_id=primary.id)
|
||||
)
|
||||
await db.execute(
|
||||
update(DiscountOffer).where(DiscountOffer.user_id == secondary.id).values(user_id=primary.id)
|
||||
)
|
||||
|
||||
# 11. Инвалидация refresh-токенов обоих пользователей (после мержа будет создан новый)
|
||||
now = datetime.now(UTC)
|
||||
await db.execute(
|
||||
|
||||
Reference in New Issue
Block a user