Telegram Desktop/iOS cache initData with stale auth_date (tdesktop#28303).
Increase max_age_seconds from 24h to 30 days for all cabinet login and
account linking endpoints. HMAC signature still validates authenticity,
JWT tokens handle session expiration. Add structured logging for stale
initData acceptance monitoring.
lock_user_for_pricing with populate_existing=True was overwriting the
pending is_daily_paused mutation before db.commit(), silently discarding
the pause toggle. Fix moves lock before state reads, uses commit=False
for subtract_user_balance and create_transaction to ensure single atomic
commit, and re-applies is_daily_paused after any populate_existing reload.
Inactive tariffs with is_trial_available=True can now be used for trial
activation across bot, miniapp, and cabinet. This enables dedicated trial
tariffs with custom limits (traffic, devices, servers) without exposing
them in the regular purchase flow. Paid trial paths now properly resolve
trial tariff parameters instead of using global settings defaults.
Email addresses with dots (e.g., john.doe@gmail.com) caused RemnaWave
API validation failure. Now sanitizes email prefix early in the
identifier construction, not just in the final result. Also sanitizes
the fallback username path for defense in depth.
With 20+ promo groups, the full details per group exceeded Telegram's
4096 char limit. Simplified list to one compact line per group with
name and member count. Full details remain in the group detail view.
- Smart end_date check: subscriptions with future end_date are preserved (not expired) and panel user is re-created automatically
- Recreation loop guard: in-memory 120s cooldown prevents unbounded recreate→delete→recreate cycles with stale entry eviction
- Race condition protection: guard timestamp stamped before any await point so concurrent coroutines are serialized
- Admin deletion: force_panel_delete=True ensures panel user is always removed, preventing orphaned subscriptions
- Add custom buttons support: admins can add up to 10 custom buttons
with callback_data or URL action types to broadcast messages
- CustomBroadcastButton Pydantic model with validation:
callback_data checked in UTF-8 bytes (Telegram 64-byte limit),
URLs restricted to https:// and tg:// schemes only
- Fix home button: removed from CABINET_MINIAPP_BUTTON_KEYS so it
uses back_to_menu callback instead of opening cabinet WebApp
- Both legacy and combined broadcast endpoints pass custom_buttons
- Add telegram_id-based self-referral protection in all 3 Telegram auth endpoints
(user doesn't exist yet at referral resolution, so telegram_id is used instead of user.id)
- Add SELECT FOR UPDATE + db.refresh in _process_referral_code to prevent TOCTOU race
on concurrent referral assignment (matches _process_campaign_bonus pattern)
- Fix _process_referral_code to handle two cases: referred_by_id already set by
create_user() → fire registration event; not set → resolve code, set, fire event
- Fix deleted user re-registration losing referral: keep status=DELETED in preparation
block so complete_registration enters the DELETED branch (not "already active")
- Remove unused referral_code from DeepLinkPollRequest (deep link = existing users only)
- Fix OIDC exception handling inconsistency (ValueError/LookupError → Exception)
- Fix bare except clauses in start.py → except Exception
- Pass is_new_user to _finalize_oauth_login (only new user path passes True)
RemnaWave API only accepts ACTIVE/DISABLED for user status updates —
EXPIRED and LIMITED are managed internally. The bot was sending EXPIRED
status and past expireAt dates, causing 400 validation errors.
- Change UserStatus.EXPIRED → UserStatus.DISABLED in all 5 call sites
- Add 1-minute buffer to expire_at for inactive subscriptions to avoid
"expiration date in the past" rejections (matches _safe_expire_at_for_panel)
- Include TRIAL status in is_actually_active checks (consistent with
remnawave_service.py sync_users_to_panel)
- Create NaloGO receipt when code-only gifts (no recipient) are paid via
any gateway provider, not just directed gifts
- Add receipt_uuid and receipt_created_at columns to guest_purchases for
persistent DB-level dedup (covers PENDING_ACTIVATION and code-only paths
where no Transaction exists at receipt time)
- Use SELECT ... FOR UPDATE in try_fulfill_guest_purchase to prevent
concurrent webhook double-processing race condition
- Expand idempotency guard to include code-only gifts already in PAID status
- Add db.refresh after PENDING_ACTIVATION nalogo call to guard against
inner rollback expiring the ORM object
Cabinet API and WebAPI created admin balance transactions with
payment_method=NULL instead of 'manual', making them invisible
to sales statistics filters.
Changes:
- Add payment_method=PaymentMethod.MANUAL to Cabinet and WebAPI
balance update endpoints
- Add func.abs() to all transaction amount aggregations missing it
across sales stats, dashboard stats, and reporting queries
- Remove redundant Python abs() on addon_revenue (SQL func.abs
already applied)
- Add data migration 0044 to fix historical NULL payment_method
records for admin top-ups
Landing page (guest) payments were completely skipping nalogo receipt
generation because the guest purchase flow returned early in payment
webhook handlers before reaching the nalogo code.
Added _create_nalogo_receipt_for_purchase() helper with:
- payment_id null-check (Redis dedup requires it)
- amount validation (skip zero/negative)
- transaction.receipt_uuid duplicate guard
- inner try/except with db.rollback() for receipt_uuid persistence
- sanitize_proxy_error for credential-safe error logging
- privacy: no telegram_user_id in receipt description sent to tax authority
Called in both DELIVERED and PENDING_ACTIVATION paths.
Added db.refresh(purchase) after nalogo call to handle potential
session expiry from rollback inside the helper.
payload column in cryptobot_payments contains plain strings like
"balance_2_10000" alongside JSON objects. CAST(payload AS json) fails
on these rows during CREATE INDEX CONCURRENTLY.
- Add AND payload LIKE '{%' to partial index WHERE clause in migration 0042
- Add .payload.like('{%') filter to guest_purchase_service query
- Apply sanitize_proxy_error() to all 8 error handlers in nalogo_service
- Remove exc_info=True from error paths that could expose proxy creds
- Fix regex backreference to preserve original SOCKS scheme
- Consolidate proxy utility imports to module level
- Add source indicator (NALOGO_PROXY_URL vs fallback) to startup log
Route all nalog.ru API traffic through SOCKS proxy. Uses NALOGO_PROXY_URL
env var (falls back to PROXY_URL if not set). Adds httpx[socks] dependency.
- Thread proxy_url through Client → AuthProviderImpl + AsyncHTTPClient
- Extract mask_proxy_url() and sanitize_proxy_error() utilities
- Add socks5h:// scheme support for remote DNS resolution
- Sanitize proxy credentials in error messages
- Log masked proxy URL at startup and service init
When a tariff has a stale external_squad_uuid that no longer exists in
the Remnawave panel, PATCH/POST /api/users fails with A039 (P2003 FK
constraint violation). This caused subscriptions to not sync with the
panel even though balance was already charged.
Now both update_user() and create_user() catch A039 errors and
automatically retry without externalSquadUuid, logging a warning about
the stale UUID. The subscription sync succeeds without the external
squad assignment rather than failing entirely.
- Replace broad `except Exception` with `except TelegramAPIError` in
balance.py and wheel.py Stars invoice creation (prevents masking
programming errors)
- Fix session leak in gift.py telegram_stars path: wrap PaymentService
usage in try/finally to ensure bot.session.close() is called
Replace all ~45 direct Bot() calls across the codebase with a centralized
create_bot() factory function that automatically configures SOCKS5 proxy
session when PROXY_URL is set. This ensures proxy support applies uniformly
to all Telegram API traffic.
Key changes:
- Add app/bot_factory.py with create_bot() factory
- Replace direct Bot() instantiation in 33 files
- Fix session leaks in cloudpayments.py and auth.py (async with)
- Replace 2 direct httpx calls to api.telegram.org with
bot.create_invoice_link() (balance.py, wheel.py)
- Remove now-unused imports (Bot, DefaultBotProperties, ParseMode, httpx)
Route bot traffic through SOCKS5 proxy when PROXY_URL env var is set.
Validates scheme to reject HTTP proxies (would expose bot token).
Credentials are masked in logs.
EmailService singleton cached SMTP settings in __init__ at import time.
is_configured() read live from settings, but self.from_email stayed None
when SMTP was unconfigured at startup → AttributeError on .split('@').
Replace cached attributes with @property accessors, snapshot from_email
once per send_email call with validation guard.
- RioPay: use create_transaction(commit=False) to keep FOR UPDATE lock,
replace update_riopay_payment_status with inline assignment + flush,
add emit_transaction_side_effects after commit
- SeverPay: add db.flush() before _finalize, remove self-assignment,
add paid_at to both webhook and status-check paths
- Freekassa/KassaAI: add is_paid and paid_at to webhook and status-check
inline sections (regression from CRUD→inline migration)
- MulenPay: add is_paid and paid_at to webhook inline section
Apply the same FOR UPDATE locking pattern across 8 providers:
- RioPay: added FOR UPDATE lock (had none at all)
- CryptoBot: moved lock before status check, removed redundant lock
- WATA: moved lock before is_paid commit, removed redundant lock
- Freekassa: moved lock before is_paid commit, removed redundant lock
- KassaAI: moved lock before is_paid commit, removed redundant lock
- MulenPay: moved lock before is_paid commit, removed redundant lock
- Pal24: moved lock before is_paid commit, removed redundant lock
- SeverPay: moved lock before is_paid check, removed redundant lock
Pattern applied to all: acquire FOR UPDATE with populate_existing=True
immediately after finding the payment, replace intermediate commits with
inline assignments + flush(), re-check is_paid from locked row.
- acquire FOR UPDATE lock immediately after payment lookup, before is_paid check
- use populate_existing=True to prevent SQLAlchemy identity map stale reads
- replace intermediate update_platega_payment(commit) with inline assignments + flush
- re-check locked.is_paid after lock in get_platega_payment_status
- guard _finalize_platega_payment: only called when lock held and is_paid=False
- suppress "message is not modified" TelegramBadRequest in message_patch
- bootstrap _assign_if_missing no longer reactivates revoked UserRole rows
- revoke_role uses SELECT FOR UPDATE + pg_advisory_xact_lock to prevent TOCTOU race on last-superadmin check
- block self-revocation of superadmin role
- block is_active/level changes on system roles
- block expires_at on superadmin role assignments
- single SUPERADMIN_LEVEL constant in crud/rbac.py, imported everywhere
- get_superadmin_count excludes expired assignments
- removed dead UserRoleCRUD.revoke_role method
- warn when revoking RBAC role from a legacy ADMIN_IDS user
- added migration 0043: indexes on user_roles.role_id, access_policies.role_id, lower(users.email)