Critical security fix: the POST /cabinet/subscription/purchase-tariff
endpoint accepted arbitrary period_days from client without validating
against the tariff's configured periods. The pricing engine returned 0
for unknown periods, allowing free subscription creation.
Changes:
- Add period_days whitelist validation in /purchase-tariff endpoint
- Add zero-price safety guard as defense in depth
- Add period_days validation in _auto_purchase_tariff (saved cart)
- Add period_days validation in _prepare_auto_extend_context (saved cart)
Show each Platega method (SBP, cards, crypto) as a separate button
on the main payment screen like YooKassa, instead of behind a sub-menu.
Controlled by PLATEGA_INLINE_METHODS env var (default: true).
Set to false to keep the old sub-menu behavior.
- Add news_categories and news_tags tables with case-insensitive unique names
- Add category_id/tag_id FK columns to news_articles (ON DELETE SET NULL)
- CRUD endpoints for categories and tags (admin permissions)
- Sync legacy string fields from FK entities on create/update
- Clear legacy fields on category/tag deletion
- Alembic migration 0049 with backfill from existing article data
- Add telegram_user_id and user_db_id to get_balance_payment_description() across
8 cabinet balance providers (heleket, mulenpay, pal24, wata, cloudpayments,
freekassa, kassa_ai, riopay), all miniapp endpoints, and recurrent payments
- For email/OAuth users without telegram_id, fallback to DB ID with (U{id}) format
- Fix pre-existing tuple bug in bot_configuration.py (trailing comma created tuple)
- Fix typo in nalogo_queue_service.py log message ("Чек уже попыток")
- Extract _claim_phantom_user and _merge_phantom_into_active_user from
start.py into app/services/phantom_service.py
- Replace lightweight 3-4 table merge with full execute_merge (30+ tables)
- Add durable AdminAuditLog records for phantom claims and merges
- Use begin_nested() savepoints for audit log writes (session-safe)
- Move Remnawave panel sync to after commit (no HTTP inside locked txn)
- Fix remnawave_uuid transfer in account_merge_service with two-flush
pattern (clear→flush→assign) to prevent unique constraint violations
- Add db.refresh after rollback in Path A to prevent stale object access
- Fix orphaned subscriptions/GuestPurchase when phantom claim fails with
IntegrityError — now merges phantom into existing user across all 3 call sites
- Add explicit db.commit() after merge in both active-user and registration paths
- Fix remnawave_uuid transfer ordering (clear→flush→assign) to prevent unique
constraint violation during flush
- Clear phantom.referral_code on soft-delete to prevent unique constraint issues
- Add status != DELETED filter to find_phantom_user_by_username (defense in depth)
- Add WARNING-level logging on phantom claims for admin audit trail
- Add functional index on lower(username) for phantom lookup performance (migration 0048)
- Add ON DELETE CASCADE to subscription_servers.subscription_id (migration 0047)
- Add admin endpoint POST /users/{id}/assign-referrer with recursive CTE cycle
detection, self-enrichment prevention, and audit logging
- Harden account_merge_service: add SubscriptionServer, RioPayPayment,
SeverPayPayment, SavedPaymentMethod, GuestPurchase, NewsArticle handling
- Fix logger key typo get= → error= in promocode activation
- Add PIL.Image.DecompressionBombError to except clause (inherits from
Exception, not ValueError/OSError — was escaping as unhandled 500)
- Move _MP4_VIDEO_BRANDS to module-level frozenset for consistency
- Add ftyp brand allowlist to reject HEIC/HEIF files misclassified as MP4
- Close UploadFile after read to release resources during processing
- Add exception chaining (from None) on HTTPException raises
- Narrow except to ValueError/OSError (let programming errors propagate)
- Add exc_info=True to thumbnail failure log for debuggability
- Type detect_file_type return as tuple[MediaType, str]
- SavedMedia.media_type now uses Literal['image', 'video'] matching Pydantic schema
- Explicitly close old Image objects after exif_transpose and convert('RGB')
- PIL Image resource leak: wrap in try/finally with img.close()
- Grayscale images: normalize all to RGB for consistent JPEG output
- exif_transpose: defensive None guard
- Thumbnail: explicit close after save
- media_type: use Literal['image', 'video'] in schema
- ensure_upload_dirs: run in asyncio.to_thread (not sync in event loop)
- _SAFE_FILENAME_RE: remove thumb_ prefix (prevent orphaned files)
- Server-side HTML sanitization for article content
- URL scheme validation for featured_image_url (http/https only)
- Slug sanitization on create/update
- MissingGreenlet fix in delete (capture attrs before commit)
- Missing rollback after IntegrityError in CRUD
- nullslast() for published_at ordering
- asyncio.gather for parallel DB queries
- Removed selectinload(author) from list queries
- increment_views with RETURNING (no extra SELECT)
- Migration-model index alignment
- Pre-compiled regex, structlog.exception pattern
- View counter dedup cache (5min TTL)
When a daily subscription is resumed after user deletion from RemnaWave panel
and deactivation sync, connected_squads were cleared but never restored,
causing internal squads to not be assigned. Also, admin notifications were
missing from the Telegram bot handler path.
Fixed across all 5 resume code paths:
- cabinet /pause endpoint
- miniapp /subscription/daily/toggle-pause endpoint
- bot handle_toggle_daily_subscription_pause handler
- DailySubscriptionService._process_single_charge
- try_resume_disabled_daily_after_topup auto-resume
Changes in each path:
- Restore connected_squads from tariff.allowed_squads (fallback: all available servers)
- Branch create/update based on remnawave_uuid presence
- Follow-up PATCH after POST to ensure internal squads are assigned
- Use limit=10000 in get_all_server_squads to avoid silent truncation
- Separate try/except for squad restore vs RemnaWave sync for resilience
- Add admin notification in bot handler (was missing)
Replace 7 fragmented localization keys with one REFERRAL_INVITE_TEXT
template. Remove Share button (switch_inline_query). Wrap invite text
in blockquote+code for visual quote style with tap-to-copy. Update
instruction text in all 5 locales (ru, en, ua, fa, zh).
Bot uses default HTML parse mode — all messages are HTML-parsed by Telegram.
Added html.escape() to all user-controlled and admin-controlled strings
before interpolation into HTML messages to prevent injection and parse errors.
49 files, ~250+ injection points fixed:
- user.full_name, first_name across all handlers and services
- tariff.name/description in purchase flow, admin panel, auto-purchase service
- campaign.name, start_parameter in admin and user-facing handlers
- group.name, promo_group.name across promo management
- contest.title, prize_text, leaderboard names (including public channels)
- transaction.description (contains raw user.full_name from referral service)
- restriction_reason across all balance and subscription handlers
- ticket.title, message_text, poll.title, poll.description
- welcome text template placeholders (first_name, username)
- maintenance reason, admin_name, selected_prize.display_name
New helpers in app/utils/formatting.py:
- safe_html_name() for escaping display names
- user_html_link() replacing 15+ duplicated inline link patterns
_calculate_traffic_price treated unlimited traffic as free because
base_gb=0 triggered the `if base_gb > 0 else 0` guard, skipping
the price lookup. Added early return for total_gb==0 to use the
configured unlimited tier price.
- Reorder middleware: LoggingMiddleware now outermost, GlobalErrorMiddleware inside — prevents full traceback logging for suppressed errors (message not modified, query too old, bot blocked)
- Fix root cause in message_patch.py: _edit_with_photo missing try/except for _original_edit_text when ENABLE_LOGO_MODE=False
- Add "message is not modified" suppression in AuthMiddleware to prevent unnecessary db.rollback() and ERROR-level logging
- Fix discount promo display in admin notifications: subscription_days shown as hours (not days), balance_bonus_kopeks shown as percentage (not price)
The background monitoring service was deactivating trial subscriptions
when users unsubscribed from channels, ignoring per-channel
disable_trial_on_leave and disable_paid_on_leave settings that the
real-time handler and middleware already respected.
Changes:
- Use shared should_disable_subscription() for all 3 deactivation paths
- Add global CHANNEL_DISABLE_TRIAL_ON_UNSUBSCRIBE override in should_disable_subscription
- Add admin skip in monitoring (consistent with handler/middleware)
- Replace inline reactivation with reactivate_subscription() CRUD
- Switch to enable_remnawave_user() instead of heavy update_remnawave_user()
- Add commit=False to deactivate/reactivate/record/clear_notification for batch atomicity
- Include paid subs in monitoring when any channel has disable_paid_on_leave=True
- Use skip_deactivation flag instead of early return to preserve reactivation path
- Commit batch before create_remnawave_user which internally commits
Apply func.abs() consistently to all 5 remaining locations that sum
SUBSCRIPTION_PAYMENT amounts: branch revenue, campaign stats,
user detail branch revenue, campaign detail, and search results.
Expose total_subscription_revenue_kopeks in NetworkGraphResponse,
computed from the existing personal_spent data (sum of all
SUBSCRIPTION_PAYMENT transactions by scoped users).
Eliminates duplicated status mapping logic between _fetch_subscription_info
and get_network_user_detail. Single source of truth for mapping
subscription fields to frontend status labels.
Previously only disabled and pending statuses were forced to show as
expired in the network graph. Subscriptions with status='expired' or
status='limited' but with end_date > now would incorrectly display as
active. Now all four non-active statuses are treated as expired.
The subscription_status computation now checks the Subscription.status
field. Disabled and pending subscriptions are treated as expired
regardless of end_date, preventing incorrect "active" display.
Also added SubscriptionStatus import.
Add subscription_status field (trial_active, paid_active, trial_expired,
paid_expired) to NetworkUserNode and NetworkUserDetail schemas. Backend
computes status from Subscription.is_trial and end_date using window
function to pick latest subscription per user.
Superadmin (level 999) assignments are now the sole domain of
ADMIN_IDS/ADMIN_EMAILS environment variables. On startup, bootstrap
reactivates env-listed users and revokes superadmin from users removed
from env. API assign/revoke endpoints return 403 for superadmin-level
roles. _ensure_role_by_email now requires email_verified (symmetric
with revocation check).
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.