Commit Graph

289 Commits

Author SHA1 Message Date
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
Fringg 6da61d7951 fix: убрать WITHDRAWAL из автонегации, добавить abs() в агрегации, исправить all_time_stats
- Убран WITHDRAWAL из автонегации в create_transaction (ломал profit,
  expenses и display flip в admin_users)
- Добавлен func.abs() в by_type агрегацию (transaction.py)
- Добавлен func.abs() в total_spent user.py (_build_spending_stats_select)
- Исправлен all_time_stats в боте и webapi: передаём явный диапазон дат
  вместо дефолтного текущего месяца
2026-03-05 09:05:02 +03:00
Fringg 93a55df4c0 fix: гарантировать положительный доход от подписок и исправить общий доход
- Добавлен abs() на уровне API-ответов для subscription_income (защита от
  негативных значений при несогласованных знаках SUBSCRIPTION_PAYMENT)
- Нормализация знаков в create_transaction: SUBSCRIPTION_PAYMENT и WITHDRAWAL
  всегда сохраняются как отрицательные (дебет)
- Исправлен income_total в дашборде: показывал месячный доход вместо общего
  (теперь используется отдельный запрос all_time_stats)
2026-03-05 08:58:49 +03:00
Fringg a7a18dd0d1 fix: устранение race condition при покупке устройств через re-lock после коммита
subtract_user_balance() делает внутренний коммит, что освобождает
SELECT FOR UPDATE блокировки. Добавлен паттерн re-lock + re-validate +
refund после вызова subtract_user_balance во всех 8 путях мутации
device_limit:

- cabinet: /devices, /devices/purchase, /devices/reduce
- miniapp: /subscription/devices
- bot handlers: execute_change_devices, confirm_add_devices
- auto-purchase: _auto_add_devices
- CRUD: add_subscription_devices

Также добавлен populate_existing=True ко всем SELECT FOR UPDATE запросам
для корректного обновления SQLAlchemy identity map.
2026-03-05 08:15:00 +03:00
Fringg 1cfede28b7 fix: prevent concurrent device purchases exceeding max device limit
Add SELECT FOR UPDATE row lock on subscription before checking device
limit in all 3 device purchase endpoints (cabinet new, cabinet legacy,
miniapp). Without the lock, two concurrent requests both read the old
device_limit, both pass validation, and both increment — resulting in
device count exceeding max_device_limit (e.g., 5 devices when limit is 3).

Also moved max-devices check before balance check in legacy endpoint
to fail fast under lock.
2026-03-05 07:33:09 +03:00
Fringg c8ef808539 fix: consume promo offer in tariff_purchase.py, fix negative transaction amount
- Add consume_promo_offer to 5 call sites in tariff_purchase.py where
  _get_user_period_discount() stacks promo_offer into blended discount
  (lines 823, 1134, 1706, 2238, 2971)
- Fix negative amount_kopeks in miniapp.py:5351 transaction record
  (was -final_total, all other SUBSCRIPTION_PAYMENT use positive)
- Replace duplicate _get_user_promo_offer_discount_percent in
  monitoring_service with shared get_user_active_promo_discount_percent
2026-03-05 07:25:27 +03:00
Fringg b8857e789e fix: consume promo offer in miniapp tariff-mode renewal path
The tariff-mode renewal in miniapp applied promo_offer_discount_percent
to final_total but never passed consume_promo_offer to subtract_user_balance,
allowing infinite reuse of first-purchase-only promo discounts via miniapp.
2026-03-05 07:15:41 +03:00
Fringg e4a6aad621 fix: centralize has_had_paid_subscription into subtract_user_balance
Add mark_as_paid_subscription parameter to subtract_user_balance() that
atomically sets has_had_paid_subscription=True within the same FOR UPDATE
transaction as the balance deduction. This closes ALL purchase paths:

- Cabinet renew: add SELECT FOR UPDATE row lock (fix race condition),
  set has_had_paid_subscription atomically, remove standalone call
- Cabinet purchase_tariff: pass consume_promo_offer to subtract_user_balance
  (fix: inline clearing was wiped by db.refresh), remove standalone call
- Cabinet switch_tariff: add mark_as_paid_subscription=True
- Auto-extend: add mark_as_paid_subscription, remove standalone call
- Auto-purchase tariff: add consume_promo_offer + mark_as_paid_subscription
- Auto-purchase daily: add mark_as_paid_subscription
- Bot purchase/extend/trial handlers: add mark_as_paid_subscription
- All 8 tariff_purchase.py handlers: add mark_as_paid_subscription
- Both simple_subscription.py handlers: add mark_as_paid_subscription
- Menu smart activation: add mark_as_paid_subscription
- Monitoring autopay: add mark_as_paid_subscription
- Renewal service finalize: add mark_as_paid_subscription
- MiniApp purchase service: add mark_as_paid_subscription, remove standalone
- MiniApp renewal/tariff/switch: add mark_as_paid_subscription
2026-03-05 06:29:34 +03:00
Fringg dce9eaa597 fix: reset traffic purchases on expired subscription renewal + pricing fixes
- Reset TrafficPurchase records and purchased_traffic_gb when renewing
  expired subscriptions (was incorrectly preserving stale purchases,
  inflating traffic limit e.g. 100GB+20GB=120GB instead of fresh 100GB)
- Fix in extend_subscription() CRUD, cabinet /renew, bot handlers,
  simple_subscription handlers
- Add RemnaWave sync to cabinet /renew endpoint after subscription changes
- Fix device_price_kopeks=0 falsy-zero bug (11+ instances, or → is not None)
- Fix double-increment of purchased_traffic_gb in cabinet traffic purchase
- Fix orphaned TrafficPurchase records in 5 locations (replace_subscription,
  extend_subscription fixed_with_topup, classic mode, switch_tariff,
  purchase.py is_traffic_fixed)
- Fix admin_users.py UnboundLocalError from TrafficPurchase inline import
  shadowing module-level import
- Standardize pricing order: base + devices → promo_group → promo_offer
  across all 5+ pricing paths (cabinet, miniapp, autopay, auto-purchase,
  monitoring)
- Fix exception handlers in calculate_renewal_price (raise instead of
  returning fallback 0)
- Fix monitoring_service double-discount (promo_offer applied twice)
- Fix auto-purchase _get_tariff_price_for_period return type to tuple
  (base_price, discount_percent) so callers add devices before discount
- Pass traffic_limit_gb/device_limit to extend_subscription in
  simple_subscription.py instead of manual overwrites
2026-03-04 04:46:29 +03:00
Fringg 69a9899d40 fix: use direct is_trial access, add missing error codes to promo APIs
- Use subscription.is_trial instead of getattr for reliable check
- Fix structlog key typo: format_user_log → _format_user_log
- Add missing error codes (active_discount_exists, not_first_purchase,
  daily_limit) to miniapp and cabinet promo code endpoints
2026-03-01 23:43:04 +03:00
Fringg e32e2f779d fix: reject promo codes for days when user has no subscription or trial
SUBSCRIPTION_DAYS promo codes now require an active or expired non-trial
subscription. Users without any subscription or with a trial subscription
get a clear error message instead of silently creating/extending.
2026-03-01 23:39:02 +03:00
Fringg d708365aca fix: sync traffic reset across all tariff switch code paths
- cabinet admin change_tariff: add full reset logic (traffic_used_gb,
  purchased_traffic_gb, TrafficPurchase deletion, RemnaWave sync)
- cabinet switch_tariff: add local traffic_used_gb reset
- miniapp switch_tariff: add local traffic_used_gb reset + TrafficPurchase deletion
- auto_purchase_service: fix or→if/else branching for reset_traffic logic
2026-02-27 10:10:21 +03:00
Fringg 4eaedd33bf feat: add RESET_TRAFFIC_ON_TARIFF_SWITCH admin setting
New boolean setting (default: True) controls whether user traffic
is reset when switching between tariff plans.

Changes:
- config.py: add RESET_TRAFFIC_ON_TARIFF_SWITCH setting
- system_settings_service.py: category override (TRAFFIC) + hints
- pricing.py: admin bot handler toggle entry
- cabinet/routes/subscription.py: pass reset_traffic to RemnaWave on switch
- webapi/routes/miniapp.py: same for miniapp tariff switch
- tariff_purchase.py: use setting in 3 switch handlers (was hardcoded)
- subscription_auto_purchase_service.py: separate tariff switch vs payment logic
- crud/subscription.py: conditional traffic_used_gb reset on tariff change
2026-02-27 09:57:37 +03:00
Fringg 59fb08c3ea style: format 5 files with ruff 2026-02-24 05:59:08 +03:00
Fringg 295d2e877e refactor: remove legacy app-config.json system
Replace dual-configuration architecture (Remnawave API + local file fallback)
with Remnawave-only approach. When config is unavailable, show explicit
"not configured" message instead of silent file fallback.

- Delete app-config.json and admin_apps.py CRUD module (~1260 lines)
- Remove sync loaders, legacy step format handlers, device_mapping dicts
- Remove miniapp /app-config.json endpoint and filesystem search
- Remove backup service app-config.json snapshot/restore
- Remove APP_CONFIG_PATH setting, env var, docker volume mount
- Remove hardcoded 6-device keyboard fallback
- Remove legacy step-based keyboard rendering (installationStep etc.)
- Add "config not configured" message when Remnawave config is missing
- Update admin UI: "clear config" disables guide mode instead of reverting
2026-02-24 05:58:25 +03:00
Fringg 8375d7ecc5 feat: add multi-channel mandatory subscription system
- Multi-channel subscription enforcement via middleware, events, and cabinet API
- 3-layer cache architecture: Redis -> PostgreSQL -> rate-limited Telegram API
- ChatMemberUpdated event-driven tracking with automatic VPN access control
- Admin management via bot FSM handler and REST API with full CRUD
- Channel ID normalization: @username resolved to numeric ID at creation time
- Fail-closed error handling: API errors deny access (security-first)
- Background reconciliation with keyset pagination (100 per batch)
- Per-user rate limiting on subscription check button (5s cooldown)
- Redis connection pooling via cache singleton (no per-request connections)
- Database: channel_id index, multi-row upsert optimization
- Localization: en, ru, zh, fa, ua translations for all new strings
- Frontend blocking UI with channel list and subscription status
- Admin channel management page with toggle, delete, and create
2026-02-24 02:50:31 +03:00
Fringg 1b6bbc7131 fix: protect active paid subscriptions from being disabled in RemnaWave
Add is_active_paid_subscription() helper that checks if subscription is
non-trial, active, and not expired. Use it across all disable_remnawave_user
call sites to prevent disabling VPN access for users with paid subscriptions.

Protected paths: block_user, delete_user_account, broadcast cleanup,
channel unsubscribe, admin deactivation, webapi endpoints, cabinet
reset-trial, reset-subscription, and disable-user endpoints.
2026-02-23 16:49:31 +03:00
Fringg 10e231e52e feat: blocked user detection during broadcasts, filter blocked from all notifications
- Broadcast tri-state return: 'sent'/'blocked'/'failed' with blocked_count tracking
- Background cleanup: mark blocked users + disable their subscriptions + Remnawave
- blocked_count in BroadcastHistory model, schemas, API responses, admin UI
- Filter User.status==ACTIVE in subscription queries: get_expiring_subscriptions,
  get_expired_subscriptions, get_subscriptions_for_autopay,
  get_daily_subscriptions_for_charge, get_disabled_daily_subscriptions_for_resume
- Guard in notification_delivery_service.send_notification for BLOCKED/DELETED users
- Fix subscription tariff switch: preserve remaining days with total_seconds()
- Fix redundant local UTC imports across 16 files
- Fix test mocks: add **kwargs, correct assertion, remove dead expression
2026-02-17 18:37:25 +03:00
Fringg 094609005a fix: add naive datetime guards to parsers and fix test datetime literals 2026-02-17 05:00:13 +03:00
Fringg eb18994b7d fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
2026-02-17 04:45:40 +03:00
Fringg 25e8c9f8fc fix: use sync context manager for structlog bound_contextvars
bound_contextvars() returns a sync _GeneratorContextManager, not async.
Using `async with` caused TypeError crashing all web API requests.
2026-02-16 09:23:22 +03:00
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00
Fringg 871ceb866c fix: replace deprecated Query(regex=) with pattern= 2026-02-16 07:11:58 +03:00
Fringg 57dc1ff47f fix: resolve deadlock on server_squads counter updates and add webhook notification toggles
- Fix deadlock: enforce sorted lock ordering in add_user_to_servers/remove_user_from_servers
- Fix cross-call deadlock: add update_server_user_counts() for atomic add+remove in one sorted pass
- Fix deadlock in squad migration: use sorted dict iteration for counter updates
- Fix broken "Buy traffic" button: subscription_add_traffic → buy_traffic callback_data
- Add 12 webhook notification toggle settings (WEBHOOK_NOTIFY_*) with master toggle
- Add admin UI category "Уведомления от вебхуков" with hints in BotConfigurationService
- Add toggle check in _notify_user() respecting master and per-event settings
2026-02-12 06:47:26 +03:00
Fringg 3dac332a9f chore: ruff format 7 files 2026-02-11 21:50:49 +03:00
Fringg ee2e79db31 refactor: remove modem functionality from classic subscriptions
Remove all modem purchase/management code:
- Delete modem handler, service, and tests
- Remove modem button from keyboards and admin panel
- Remove modem pricing from calculations
- Remove modem REST API endpoint and schemas
- Remove modem decorator, config settings, and notification formatting
- Keep DB column and migration for backwards compatibility
2026-02-11 21:14:08 +03:00
Fringg bee4aa4284 fix: protect server counter callers and fix tariff change detection
- Wrap unprotected add/remove_user_to/from_servers calls in try/except
  in miniapp.py and cabinet subscription.py to prevent 500 errors
- Fix is_tariff_change to include classic-to-tariff transitions
  (subscription.tariff_id=None → new tariff_id) so purchased traffic
  is properly reset when switching modes
2026-02-11 04:44:15 +03:00
Egor cc54a7ad2f Merge pull request #2580 from xenral/main
feat(localization): add Persian (fa) locale support and wire it across app flows
2026-02-09 21:09:43 +03:00
Fringg 1cae7130bc fix: promo code max_uses=0 conversion and trial UX after promo activation
- Convert max_uses=0 to 999999 (unlimited) in cabinet and webapi routes,
  matching bot handler behavior. Fixes miniapp-created promo codes being
  immediately invalid due to is_valid check (current_uses < max_uses).
- Skip trial offer in post-registration keyboard when promo code already
  activated a subscription, showing "back to menu" button instead.
2026-02-09 17:13:11 +03:00
Ali Morshedzadeh 5482e609f8 Add initial Persian locale support and language handling updates 2026-02-09 16:53:50 +03:30
Fringg c4794db1dd feat: add TRIAL_DISABLED_FOR setting to disable trial by user type
New setting allows granular control over trial availability:
- none: trial available for all (default)
- email: trial disabled for email users
- telegram: trial disabled for telegram users
- all: trial disabled for everyone

Enforced in bot handlers, cabinet API, and miniapp routes.
Automatically appears in admin panel as dropdown via CHOICES.
2026-02-07 00:19:25 +03:00
Fringg b9352a5bd5 fix(autopay): exclude daily subscriptions from global autopay
- Skip daily tariff subscriptions in monitoring autopay cycle
- Filter daily subscriptions in get_subscriptions_for_autopay CRUD
- Block autopay menu and toggle for daily tariffs in bot handler
- Reject autopay enable for daily subscriptions in Cabinet API (HTTP 400)
- Reject autopay enable for daily subscriptions in MiniApp API (HTTP 400)
2026-02-05 07:10:52 +03:00
Egor e85a2a58cf Update middleware.py 2026-01-27 14:29:40 +03:00
c0mrade 9a2aea038a chore: add uv package manager and ruff linter configuration
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
2026-01-24 17:45:27 +03:00
Egor c9cbb81054 Add files via upload 2026-01-23 11:29:08 +03:00
Egor 7e16d0edee Add files via upload 2026-01-23 11:28:32 +03:00
Egor f6b795e555 Update miniapp.py 2026-01-20 16:56:59 +03:00
Egor c783884ace Update miniapp.py 2026-01-20 14:27:37 +03:00
Egor c331c33321 Merge pull request #2356 from BEDOLAGA-DEV/dev5
Dev5
2026-01-20 02:20:14 +03:00
Egor b7a05f7cd1 Update miniapp.py 2026-01-20 02:14:42 +03:00
Egor dd396aa16d Update miniapp.py 2026-01-20 02:14:10 +03:00
Egor 4fbe07919c Merge pull request #2355 from BEDOLAGA-DEV/dev5
Dev5
2026-01-20 01:46:57 +03:00
Egor 1752f52197 Update miniapp.py 2026-01-20 01:39:50 +03:00
PEDZEO da9997609b Add custom logging configuration to WebAPIServer to reduce WebSocket spam 2026-01-20 01:30:17 +03:00