332 Commits

Author SHA1 Message Date
Fringg
972614511f fix: address remaining review issues in device limit patch
- Migrate get_device_reduction_info to get_user_devices_all (was raw _make_request)
- Migrate admin_users and miniapp callers to get_user_devices_all
- Migrate reset_user_devices internal call to paginated version
- Fix tariff_max_devices falsy-zero in handlers (use explicit is not None and > 0)
- Fix device deletion sort: dateless devices now sort last (candidates for removal)
2026-03-29 07:26:35 +03:00
Fringg
23d1830644 feat(api): expose email field in UserResponse
Cherry-picked from PR #2821 (without version bump artifacts)
2026-03-29 05:00:44 +03:00
Fringg
6d167d2922 fix: harden node info display against injection and type errors
- HTML-escape all external strings (cpuModel, node name, address,
  last_status_message, provider_uuid, versions) in Telegram HTML messages
- Add _safe_int() helper for defensive xrayUptime parsing
- Remove redundant int() casts in route serializers
2026-03-28 22:34:54 +03:00
Fringg
173cc374bb refactor: update remnawave API integration for v2.7.0
- Node: replace flat fields (cpuCount, cpuModel, totalRam, xrayVersion,
  nodeVersion) with nested versions and system dicts, add activePluginUuid
- Node: xrayUptime changed from string to int (seconds), usersOnline
  now non-nullable
- User: remove subLastOpenedAt and subLastUserAgent (dropped in v2.7.0)
- System stats: remove cpu.physicalCores, memory.available/active
- Update all layers: dataclass, service, Pydantic schemas, route
  serializers, Telegram admin handlers
- Fix variable scoping in show_node_statistics error fallback
2026-03-28 22:32:15 +03:00
Fringg
aa36549bb3 fix: fix MiniApp renewal options 500 error for legacy subscriptions
The early-return response for blocked legacy users used wrong field
name 'balance_currency' instead of 'currency' (a required field in
MiniAppSubscriptionRenewalOptionsResponse), causing Pydantic
ValidationError / 500 Internal Server Error.

Fixed to use correct field names and added status_message explaining
why renewal is blocked, plus balance_label and sales_mode fields.
2026-03-28 19:32:21 +03:00
Fringg
78209c8623 fix: block legacy subscription renewal bypass in tariff mode
When switching from configurator to tariff mode, users with old
subscriptions (tariff_id=NULL) could still renew them through
unguarded paths, bypassing tariff pricing entirely.

Vulnerable paths fixed:
- MiniApp POST /subscription/renewal/options: returns empty list
  for classic subscriptions in tariff mode
- MiniApp POST /subscription/renewal: raises 400 with
  classic_subscription_blocked error code
- Bot confirm_extend_subscription: blocks stale extend_period_
  callbacks with tariff mode check
- Monitoring _process_autopayments: skips classic subscriptions
  (tariff_id=NULL) in autopay loop when tariff mode active

Already protected (no changes needed):
- Cabinet GET/POST renewal endpoints (renewal.py:51,117)
- Auto-purchase service (_prepare_auto_extend_context:244)
- Bot handle_extend_subscription menu (purchase.py:1657)
- Tariff extend flow (tariff_purchase.py:2047)
2026-03-28 19:27:08 +03:00
Fringg
cddb8d6332 fix: add tariff identification to remaining notification gaps
- MiniApp renewal success message: add tariff label to _build_renewal_success_message
- Admin buy subscription: add tariff line to user notification
- Promocode subscription days: add tariff label to effect message
- Fix cosmetic double-space in autopay success tariff line
2026-03-28 19:19:18 +03:00
c0mrade
565c08366b feat: add Remnawave panel 2.7.0 API support
- Add MONTH_ROLLING traffic reset strategy (enum, UI, mapping)
- Fix squad remove-users HTTP method POST → DELETE
- Add forceRestart param to restart_all_nodes (API, service, routes)
- Replace removed /bandwidth-stats/nodes/realtime with /system/nodes/metrics
- Fix parse_bytes suffix matching bug (B was catching GB/MB/KB/TB)
- Add torrent_blocker.report webhook event with admin notifications
- Add subpage_config_changed → auto-invalidate app config cache
- Pass webhook meta field to handlers (notConnectedAfterHours)
- Improve CRM/login webhook formatting (providerName, loginAttempt)
- Update bandwidth display: remove /s suffix, show inbound traffic totals
2026-03-28 12:34:15 +03:00
Fringg
5724906517 fix: multi-tariff code review — 13 critical/high bugs fixed across 14 files
CRITICAL fixes:
- promocode_service: NameError (subscription_id not passed), TypeError (dict
  returns), savepoint without commit, dead else branch
- cabinet status/autopay/renewal: resolve_subscription() instead of
  user.subscription fallback in multi-tariff mode
- cabinet devices: MultipleResultsFound crash on 3 POST endpoints
- webhook service: IDOR returning cross-user subscription
- monitoring_service: real expiring notification keyboard with se:{sub_id}

HIGH fixes:
- subscription_purchase_service: FOR UPDATE on both branches of submit_purchase
- miniapp: 8 endpoints now pass subscription_id to _ensure_paid_subscription
- inline.py: se:{subscription_id} callback for expiring keyboard
- tariff_purchase: TransactionType.FAILED_REFUND + _persist_failed_refund()
- account_merge_service: panel sync after subscription transfer
- webhook service: .limit(1) on fallback queries to prevent MultipleResultsFound
2026-03-26 07:26:53 +03:00
c0mrade
a1623d94b1 fix: web API routes use multi-subscription resolution for operations
miniapp.py: get_subscription_details, get_tariffs, purchase_tariff,
preview_tariff_switch all resolve subscription from subscriptions list
instead of user.subscription property.
subscriptions.py + users.py: replace_existing and deactivation use
smart selection with len==1 guard.
2026-03-25 11:47:17 +03:00
c0mrade
d87fb47e88 fix: multi-subscription UUID resolution and ownership validation
- Add _resolve_panel_uuid helper for per-subscription UUID in multi-tariff mode
- Add user ownership validation (user_id check) to all subscription queries
- Add unique partial index on (user_id, tariff_id) for active subscriptions
- Generate remnawave_short_id for new subscriptions in all creation paths
- Fix trial endpoints to check all user subscriptions, not just first
- Fix channel member handler to enable/disable per-subscription UUIDs
- Fix channel checker middleware for multi-subscription iteration
- Fix tariff switch, traffic, and device endpoints to use correct panel UUID
- Fix monitoring, auto-purchase, renewal services for multi-subscription
- Fix user_service, miniapp, subscriptions and users webapi routes
2026-03-23 18:45:46 +03:00
c0mrade
d071269b8c fix: comprehensive multi-subscription audit fixes across routes, handlers, and services
- Fix UUID resolution in monitoring and webhook services for multi-tariff mode
- Update cabinet routes to properly resolve per-subscription UUIDs
- Fix account merge service for multi-tariff subscription transfers
- Update admin handlers (users, promo_offers, servers) for multi-subscription
- Fix traffic, devices, servers, daily subscription modules
- Update payment handlers (stars, yookassa) and purchase services
- Fix broadcast, promocode, and subscription auto-purchase services
- Add multi-subscription support to keyboards and localization
- Fix CRUD operations for subscription queries
- Resolve code quality issues (ruff linting)
2026-03-23 18:40:23 +03:00
c0mrade
06feb3fff5 fix: disable redirect_slashes globally to prevent HTTP 307 on subscription endpoints 2026-03-23 18:40:23 +03:00
c0mrade
335be66980 feat: multi-subscription support (1 user = N subscriptions)
- Add MULTI_TARIFF_ENABLED feature flag for gradual rollout
- Migration 0041: remove unique constraint on subscriptions.user_id
- Migration 0042: add remnawave_short_id (NOT NULL, UNIQUE) to subscriptions
- Each subscription gets its own Remnawave user (user_{tg_id}_{short_id})
- Add _resolve_subscription() to all 30+ bot handlers for per-subscription routing
- Add my_subscriptions.py with list/detail views and delegation handlers
- Refactor cabinet subscription.py (4687 lines -> 10 focused modules)
- Add subscription_id parameter to all cabinet API endpoints
- Adapt all services: autopay, wheel, contests, campaign, guest purchase,
  monitoring, blocked users, account merge, promo codes, payments
- Replace all user.subscription (singular) with user.subscriptions iteration
- IDOR protection via get_subscription_by_id_for_user on all endpoints
- Full backward compatibility: MULTI_TARIFF_ENABLED=False = legacy behavior
2026-03-23 18:37:17 +03:00
Fringg
1642be8bd6 chore: ruff format miniapp.py 2026-03-23 16:49:17 +03:00
Fringg
8175bc8bfe fix: comprehensive security hardening across payment and API layers
- CloudPayments: require webhook signature when secret configured (all 4 handlers)
- Platega: timing-safe HMAC comparison via hmac.compare_digest
- CryptoBot/Heleket: return False when API token unconfigured
- Tribute: return 503 when API key not configured
- Freekassa: use request.client.host instead of X-Forwarded-For
- Pal24: verify webhook amount matches stored payment amount
- YooKassa: reject test-mode payments in production; add YOOKASSA_TEST_MODE config
- CloudPayments: reject test-mode payments in production
- WebAPI: add upper bounds to duration_days, traffic_limit_gb, device_limit schemas
- WebAPI: bound balance update amount to ±100M kopeks
- WebAPI: sign-dispatch for balance updates (negative → subtract_user_balance)
- WebAPI miniapp: add blocked/deleted user checks, restriction_topup/subscription guards
- Admin handlers: add @admin_required and @error_handler to moderator panel
- add_user_balance: guard against negative amounts (use subtract_user_balance instead)
2026-03-23 16:44:38 +03:00
Fringg
2f19c76357 fix: add user ID to payment descriptions for all providers and fix tuple bug
- 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 ("Чек уже попыток")
2026-03-23 14:48:12 +03:00
Fringg
89341baa62 fix: restore connected_squads and admin notification on daily subscription resume
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)
2026-03-23 09:31:34 +03:00
Fringg
d3c994083e fix: daily subscription pause not persisting in cabinet and miniapp
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.
2026-03-22 06:54:39 +03:00
Fringg
cce3b0c13b feat: allow inactive tariffs for trial subscription activation
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.
2026-03-22 06:01:34 +03:00
Fringg
ab43e74ab7 fix: manual admin top-ups missing from sales statistics
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
2026-03-21 07:01:22 +03:00
Fringg
0a53b85b8a refactor: centralize Bot instantiation via create_bot() factory
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)
2026-03-21 02:49:37 +03:00
Fringg
67da390371 feat: show both bot and cabinet referral links everywhere
Previously the bot showed only one referral link (cabinet when CABINET_URL
is set, bot otherwise). Users who received the cabinet link were confused —
they opened a web registration form instead of being directed to the bot.

Now the bot, cabinet API, and miniapp API all return both links:
- Bot link (t.me deep link) — always shown
- Cabinet link (web registration) — shown when CABINET_URL is configured

Changes:
- Add get_bot_referral_link() and get_cabinet_referral_link() to config
- Refactor config methods to eliminate code duplication
- Update bot referral handler to display both links
- Fix switch_inline_query 256-char limit with auto-truncation
- Add html_escape() to all user-controlled strings in HTML messages
- Add translations for 5 new keys in all 5 locale files (ru/en/ua/zh/fa)
- Simplify cabinet route to use new methods instead of inline URL construction
- Add bot_referral_link to MiniApp API schema and response
2026-03-20 23:12:22 +03:00
Fringg
641da949a9 fix: enforce promo group authorization on country/server selection
Previously, users could retain access to servers removed from their
promo group by re-submitting already-connected UUIDs in country
selection requests. The validation allowed any UUID present in
current connected_squads, bypassing promo group checks.

Now all selected server UUIDs must be in the user's allowed promo
group set. Unauthorized servers are rejected (cabinet/bot) or
filtered out (miniapp). Fixes authorization bypass across all 3
surfaces: cabinet, Telegram bot, and miniapp.
2026-03-17 22:30:57 +03:00
Fringg
c34fdd10a0 fix: sync squads to Remnawave panel on tariff purchase/switch
When sync_squads parameter was introduced (4aaf0ddd) to prevent FK
violations from stale squad UUIDs, all update_remnawave_user calls
defaulted to sync_squads=False. This broke squad synchronization for
purchase/tariff-change flows where squads are freshly assigned and
must be sent to the panel.

Adds sync_squads=True to all purchase, tariff switch, and country
selection call sites across cabinet, bot handlers, miniapp, and
auto-purchase service.
2026-03-17 22:17:35 +03:00
Fringg
8d3cd50098 refactor: централизация всех расчётов цен в PricingEngine
- Мигрирован confirm_purchase() на calculate_classic_new_subscription_price()
- Мигрирован compute_simple_subscription_price на делегацию в PricingEngine
- Мигрирован handle_custom_confirm на calculate_tariff_purchase_price()
- Мигрированы daily confirm handlers (confirm_daily_tariff_purchase,
  confirm_daily_tariff_switch, confirm_instant_switch daily path)
- Мигрирован gift.py на calculate_tariff_purchase_price()
- Мигрированы FSM cache prices (select_period, select_devices, toggle_country)
- Добавлен lock_user_for_pricing в admin_buy_tariff_execute (TOCTOU fix)
- Добавлен lock + recompute в _auto_add_devices и _auto_add_traffic
- Исправлено двойное применение promo-offer в simple_subscription (критический баг)
- Унифицирован daily price display (group+offer) на всех 6 поверхностях
- PricingEngine.get_addon_discount_percent: добавлен promo_group= kwarg
- PricingEngine._calculate_switch_to/from_daily: добавлен promo-offer discount
- Удалён мёртвый код из common.py (_get_addon_discount_percent_for_user)
- Miniapp period_discounts: исправлен доступ через get_discount_percent()
2026-03-16 03:10:22 +03:00
Fringg
c9f2dffabf fix: address 6-agent review findings for PricingEngine
H1: log error when tariff_id set but tariff relationship not loaded
H2: warn on CLASSIC_PERIOD_PRICES→PERIOD_PRICES fallback
M1: fix float division in miniapp tariff purchase (use PricingEngine.apply_discount)
M2: fix format_period Russian pluralization for teen-hundreds (111-119, etc.)
M3: deduplicate _resolve_discount_percent — import from pricing_utils
M4: fix N+1 queries in compute_simple_subscription_price (batch fetch)
M5: add period_days validation tests (negative, zero, float)
M6: add user=None tests for tariff and classic modes
M7: fix float division in calculate_prorated_price (use // instead of /)
L1: add context to _calculate_servers_price error log
L2: add comment clarifying ClassicBreakdown.group_discount_pct type
L3: add test for original_total property
L4: inline _apply_percentage_discount wrapper in subscription_purchase_service
L5: replace global _server_id_counter with itertools.count() in tests
2026-03-13 05:45:46 +03:00
Fringg
e24b911283 refactor: migrate all callers to pricing_engine singleton + fix miniapp discount
- 13 PricingEngine() instantiation sites → import pricing_engine singleton
- miniapp _apply_promo_discount now delegates to PricingEngine.apply_discount
  (fixes float division vs floor division inconsistency)
2026-03-13 05:22:18 +03:00
Fringg
ae99358ae9 fix: pricing audit — display/charge parity, race conditions, balance locks
M-2: tariff_purchase.py — _apply_promo_discount delegates to PricingEngine,
     _get_user_period_discount returns (group_pct, offer_pct, combined),
     all ~15 call sites updated for display/charge price parity

M-4: miniapp switch_tariff — add FOR UPDATE lock on subscription,
     commit=False for atomic balance+transaction, emit_transaction_side_effects

M-6: CryptoBot — defer status commit (commit=False) so webhook retry works
     if fulfillment fails

WARNING: add lock_user_for_update before balance_kopeks mutations in
     contest_attempt_service, wheel_service, admin/referrals,
     account_merge_service, cabinet/routes/contests
2026-03-13 05:11:59 +03:00
Fringg
c9a9816daa refactor: remove dead pricing code and fix miniapp classic mode
- Remove SubscriptionService.calculate_renewal_price (zero callers, replaced by PricingEngine)
- Remove SubscriptionService.calculate_renewal_price_with_months (zero callers)
- Remove _calculate_subscription_renewal_pricing wrapper in miniapp (zero callers)
- Fix miniapp classic mode: pass PricingEngine result directly to finalize() instead of old wrapper
- Fix potential NameError: pricing_snapshot in cryptobot path used undefined 'pricing' variable
- Net: -396 lines of duplicate pricing logic
2026-03-12 23:12:59 +03:00
Fringg
cb43acab31 refactor: migrate miniapp renewal display + execute to PricingEngine 2026-03-12 23:00:58 +03:00
Fringg
f59b215645 style: fix import sorting and formatting after lint
ruff auto-fix for import ordering in cabinet/subscription.py and
formatting adjustments across changed files.
2026-03-12 22:58:35 +03:00
Fringg
8f434525eb feat: add LIMITED subscription status and preserve extra devices on tariff switch
- Add SubscriptionStatus.LIMITED for traffic-exhausted subscriptions
- Webhook user.limited now sets LIMITED directly instead of DISABLED
- Add LIMITED to reactivation, extend, resume, auto-purchase, contest eligibility
- Add traffic_exhausted error response in miniapp API
- Fix device_limit being overwritten on tariff switch in all code paths:
  admin change_tariff, user switch-tariff, miniapp, bot tariff_purchase,
  auto_purchase_service — now preserves extra purchased devices via
  calc_device_limit_on_tariff_switch() helper
- Fix truthiness checks on device_limit (0 is valid, use `is not None`)
2026-03-12 18:35:59 +03:00
Fringg
12ae871653 feat: referral links now point to web cabinet instead of bot
Centralized referral link generation into settings.get_referral_link().
When CABINET_URL is configured, links use {CABINET_URL}?ref={code}.
Falls back to Telegram bot deep link when CABINET_URL is not set.

- URL-encodes referral_code for safety
- Handles CABINET_URL with existing query params (uses & vs ?)
- Guards against None referral_code in all call sites
- QR code caching uses link hash for auto-invalidation
2026-03-12 06:45:17 +03:00
Fringg
8b35428055 fix: reactivate subscription after traffic top-up when status is EXPIRED
When traffic is exhausted, RemnaWave may send user.expired webhook setting
local status to EXPIRED (not just DISABLED). reactivate_subscription() only
handled DISABLED→ACTIVE, silently ignoring EXPIRED subscriptions. After
purchasing additional GB, the subscription stayed expired and VPN remained
blocked despite payment.

Changes:
- reactivate_subscription() now handles both DISABLED and EXPIRED→ACTIVE
  when end_date is still in the future
- Inverted null end_date guard to block reactivation (defense-in-depth)
- Added enable_remnawave_user() call after update in all traffic/device
  top-up paths to ensure panel exits LIMITED state
- Gated enable call on subscription.status == 'active' to prevent
  enabling when reactivation was a no-op
- Fixed all 12 call sites across bot handlers, cabinet routes,
  miniapp, webapi, and auto-purchase service
2026-03-12 03:57:35 +03:00
Fringg
864a4ed700 fix: record transactions for free tariff switches and admin tariff changes
- Add transaction records for free tariff switches (downgrade, upgrade_cost=0) in miniapp and cabinet
- Add atomic transaction records for admin tariff changes in bot handler and cabinet API
- Use commit=False for admin flows to ensure subscription change and transaction are committed together
2026-03-11 02:17:35 +03:00
Fringg
3c96c2affd fix: 3 bugs — notification type, referral with channel sub, BOT_USERNAME
1. Admin notification showed "renewal" instead of "first purchase" for new
   users because has_had_paid_subscription was set before notification.
   All 21 call sites now pass explicit purchase_type.

2. Partner referral not counted when mandatory channel subscription enabled.
   required_sub_channel_check saved campaign_id but not referrer_id from
   campaign.partner_user_id. Also removed duplicate DB query.

3. BOT_USERNAME auto-detection moved before web server start to close
   race window on /cabinet/branding/telegram-widget endpoint.
2026-03-10 21:19:18 +03:00
firewookie
fd3466b75c fix PR problems 2026-03-10 14:29:29 +05:00
Fringg
fcdeff1ee5 fix: migrate pricing to days-based proration, fix promo revenue leaks, fix admin panel bugs
- Migrate all addon pricing (devices, traffic, countries) from months-based to days-based proration
- Remove get_remaining_months() utility, use days_left / 30 consistently
- Fix promo/campaign balance bonuses counted as revenue in reports (add REAL_PAYMENT_METHODS filter)
- Fix partner stats, campaign stats, miniapp stats, referral fraud detection promo deposit leaks
- Fix admin balance history showing deductions with + sign (use -abs for expense types)
- Fix promo offer deactivation returning 400 for non-promocode offers
- Fix daily tariff renewal requesting 30-day renewal instead of 1-day purchase
2026-03-10 03:31:07 +03:00
Fringg
95a32e8574 fix: payment gateway issues — YooKassa polling, PAL24 card 500
- YooKassa: return local_payment_id instead of UUID for frontend polling
  (parseInt on UUID produced wrong ID → eternal spinner)
- PAL24: remove unsupported payment_method param from API call
  (cabinet and miniapp routes — URL selection is client-side)
2026-03-09 21:53:53 +03:00
Fringg
f9f07f360c fix: enforce tariff device_price and max_device_limit across all purchase paths
The miniapp, legacy cabinet endpoint, auto-purchase service, and Telegram bot
handlers were using only global settings (PRICE_PER_DEVICE, MAX_DEVICES_LIMIT)
for device purchases, completely ignoring tariff-level device_price_kopeks and
max_device_limit. This allowed users to buy devices when tariff price was 0
(should be blocked) and exceed the tariff's max device limit.

Fixed in all 4 code paths:
- miniapp _build_subscription_settings + update_subscription_devices_endpoint
- cabinet legacy POST /devices (+ added subscription status check, RemnaWave sync)
- subscription_auto_purchase_service._auto_add_devices
- telegram bot handlers confirm_change_devices, execute_change_devices, confirm_add_devices
2026-03-08 23:08:32 +03:00
Fringg
770b31d3d0 feat: auto-resume disabled daily subscriptions on balance topup
- Add try_resume_disabled_daily_after_topup() for instant resume when balance is topped up
- Fix all 5 resume paths to charge daily fee BEFORE activating subscription
- Remove unsafe inline auto-resume from add_user_balance() that bypassed fee charging
- Add NULL-safe is_daily_paused filter in subscription queries
- Use create_remnawave_user() instead of enable_remnawave_user() for full VPN panel sync
- Add DAILY_SUBSCRIPTION_RESUMED_AFTER_TOPUP localization key (ru, en, fa, zh, ua)
2026-03-08 21:36:17 +03:00
Fringg
1f664a9083 fix: remove is_active_paid_subscription guard from admin deactivation
The guard silently blocked admins from deactivating active paid
subscriptions, returning a generic error with no explanation.
Admin deactivation is intentional (with confirmation step) and
should not be prevented. The guard remains in automated processes
(monitoring, broadcast, user_service) where it makes sense.
2026-03-08 13:05:52 +03:00
Fringg
77456efb75 fix: add X-CSRF-Token and X-Telegram-Init-Data to CORS allow_headers
The security hardening commit changed allow_headers from ['*'] to
['Authorization', 'Content-Type'], but the frontend sends X-CSRF-Token
on all POST/PUT/DELETE/PATCH requests and X-Telegram-Init-Data on all
requests. The missing headers caused preflight OPTIONS requests to fail
with 400 "Disallowed CORS origin".
2026-03-07 04:30:29 +03:00
Fringg
e96fe1ecd8 fix: comprehensive security hardening from 7-agent review
Schema validation:
- Add max_length to init_data (4096), widget fields (first_name 64,
  last_name 64, username 32, photo_url 512, hash 64)
- Add max_length=2048 to all token fields (verify, refresh, reset, auto-login)
- Add max_length=128 to EmailLoginRequest.password (bcrypt DoS prevention)
- Add pattern to OAuthCallbackRequest.referral_code (was missing)
- Add pattern=r'^\d{6}$' to EmailChangeVerifyRequest.code
- Add pattern/max_length to language field (ISO 639-1)

Auth endpoints:
- Add user.status check to auto-login (banned users could authenticate)
- Add exception chaining (from e) to refresh and auto-login endpoints
- Add IP rate limiting to initData, register standalone, verify email,
  forgot password, and reset password endpoints

CORS:
- Add PATCH to allow_methods in both unified_app and webapi (38+ PATCH
  endpoints were blocked for cross-origin requests)

JWKS:
- Fix race condition: move force-refresh cooldown check and cache
  invalidation inside asyncio.Lock via dedicated _force_refresh_jwks()
2026-03-07 03:47:58 +03:00
Fringg
6495384bcf fix: transaction boundary and CORS in webapi
- Revert OIDC flush to commit before _store_refresh_token
  (matches widget/initData pattern, prevents rollback losing user updates)
- Fix CORS wildcard+credentials in webapi/app.py (same as unified_app fix)
2026-03-07 03:33:37 +03:00
Fringg
7fb839aef6 fix: промокоды — конвертация триалов, race condition, savepoints
- trial подписки теперь конвертируются в платные вместо отказа (ошибка ~20 из 300 юзеров)
- extend_subscription: добавлен переход TRIAL→ACTIVE
- UniqueConstraint на PromoCodeUse(user_id, promocode_id) + миграция 0015 с дедупликацией
- create_promocode_use: begin_nested()+flush() вместо commit/rollback (без коррупции сессии)
- race condition: create_promocode_use вызывается ДО _apply_promocode_effects
- cleanup: удаление зарезервированной записи при ValueError от эффектов
- atomic SQL increment для current_uses (защита от lost-update)
- mark_user_as_had_paid_subscription: savepoint вместо commit/rollback
- удалён мёртвый код: use_promocode(), trial_subscription_not_eligible из маппингов
2026-03-06 01:33:18 +03:00
Fringg
b9e17be855 fix: реактивация DISABLED подписок при покупке устройств и в REST API
Добавлен вызов reactivate_subscription перед update_remnawave_user в 4 пропущенных местах:
- handlers/subscription/devices.py (2 пути покупки устройств)
- webapi/routes/subscriptions.py (эндпоинты добавления трафика и устройств)
2026-03-05 11:29:23 +03:00
Fringg
7d28f5516a fix: реактивация DISABLED подписок при покупке трафика для LIMITED пользователей
Когда RemnaWave ставит пользователю статус LIMITED (трафик исчерпан),
webhook бота устанавливает локальный статус подписки в DISABLED. При
покупке дополнительного трафика update_remnawave_user() видел DISABLED
и отправлял status=EXPIRED, что RemnaWave отвергал с ошибкой 400.

Добавлен вызов reactivate_subscription() перед синхронизацией с RemnaWave
во всех 8 потоках покупки/переключения трафика:
- handlers/subscription/traffic.py (add_traffic, execute_switch_traffic)
- cabinet/routes/subscription.py (purchase_traffic)
- cabinet/routes/admin_users.py (admin add_traffic)
- handlers/admin/users.py (_add_subscription_traffic)
- webapi/routes/miniapp.py (purchase_traffic_topup)
- subscription_auto_purchase_service.py (_auto_add_traffic, _auto_add_devices)

Также разрешён статус DISABLED в guard автопокупки трафика и устройств,
чтобы LIMITED пользователи могли автоматически докупать ресурсы.
2026-03-05 10:56:10 +03:00
Fringg
849b3a7034 fix: убрать избыточный минус в amount_kopeks для create_transaction
amount_kopeks=-X → amount_kopeks=X в 10 местах:
- tariff_purchase.py (8 локаций)
- miniapp.py (1 локация)
- admin/users.py (1 локация)

create_transaction автоматически негирует для SUBSCRIPTION_PAYMENT,
поэтому передача положительного значения — правильная конвенция.
2026-03-05 10:15:39 +03:00