Commit Graph

7557 Commits

Author SHA1 Message Date
Fringg 165d25ef5f fix: media upload security hardening from 6-agent review
- 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)
2026-03-23 12:05:37 +03:00
Fringg 5ed3780f83 fix: create uploads subdirectories in Dockerfile for correct permissions 2026-03-23 12:02:05 +03:00
Fringg a0d40ad432 feat: add media upload/delete API for news articles
Local filesystem storage with Docker volume mount, magic byte validation,
PIL image resize/thumbnail generation, atomic writes, path traversal guards.
2026-03-23 11:58:07 +03:00
Fringg 3e69efe589 fix: replace asyncio.gather with sequential queries on shared session
AsyncSession does not support concurrent operations on the same
connection. Running gather caused InvalidRequestError on news list.
2026-03-23 11:34:25 +03:00
Fringg 015c2da297 fix: simplify 0046 migration downgrade to just drop_table
drop_table automatically removes all indexes, fixing downgrade failure
when indexes were added after initial migration was applied
2026-03-23 11:29:31 +03:00
Fringg 2b91808b0c fix: news module security hardening, perf optimizations, bug fixes
- 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)
2026-03-23 11:09:45 +03:00
Fringg b93240393f feat: add news articles module with admin CRUD and public API
- NewsArticle model with composite index, Alembic migration
- Admin routes: list, create, update, delete, toggle publish/featured
- Public routes: paginated list with category filter, article detail with view counter
- Pydantic schemas with strict hex color validation, slug auto-generation
- IntegrityError handling for slug race conditions
2026-03-23 10:51: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 cbe630cab0 refactor: simplify referral invite text to single template
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).
2026-03-23 08:25:19 +03:00
Fringg 9de34900a2 fix: comprehensive html.escape() for all user/admin data in Telegram HTML messages
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
2026-03-23 08:06:15 +03:00
Fringg aec04f0085 fix: correctly price unlimited traffic (0 GB) in classic subscription mode
_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.
2026-03-23 06:42:57 +03:00
Fringg 0fe3c217f7 fix: suppress harmless TelegramBadRequest errors and fix discount promo display
- 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)
2026-03-23 06:16:28 +03:00
Fringg 958ec489a2 fix: respect per-channel disable_on_leave settings in monitoring service
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
2026-03-23 05:54:26 +03:00
Fringg 0335f40b47 chore: ruff format rbac_bootstrap_service.py 2026-03-22 10:51:26 +03:00
Fringg 8ac1183670 chore: ruff format admin_referral_network.py 2026-03-22 10:47:01 +03:00
Fringg bcc761f9d3 fix: add missing total_subscription_revenue_kopeks in scoped graph early return
Prevents Pydantic ValidationError (500) when scoped_user_ids is empty
but campaign_ids is present.
2026-03-22 10:46:08 +03:00
Fringg 1eb4e18c17 fix: add abs() to all remaining subscription payment sum queries
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.
2026-03-22 10:39:20 +03:00
Fringg 056c13bc23 fix: use abs() for subscription payment amounts in referral network
SUBSCRIPTION_PAYMENT transactions are stored as negative values,
causing negative totals in stats panel and user detail card.
2026-03-22 10:36:13 +03:00
Fringg 2bdb7643f8 feat: add total subscription revenue to referral network stats
Expose total_subscription_revenue_kopeks in NetworkGraphResponse,
computed from the existing personal_spent data (sum of all
SUBSCRIPTION_PAYMENT transactions by scoped users).
2026-03-22 10:31:18 +03:00
Fringg 8b8f1b91f3 refactor: extract _compute_subscription_status shared helper
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.
2026-03-22 10:08:50 +03:00
Fringg 5ed2f0c958 fix: treat expired and limited subscription statuses as inactive in referral network graph
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.
2026-03-22 09:54:59 +03:00
Fringg 454dc9321b fix: consider subscription status field in network graph
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.
2026-03-22 09:51:59 +03:00
Fringg de91d3282f feat: add subscription status to referral network graph nodes
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.
2026-03-22 09:07:29 +03:00
Fringg e0bedc8e78 fix: superadmin role managed exclusively via env config
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).
2026-03-22 08:41:36 +03:00
Fringg fe847e35f0 chore: ruff format oauth, auth schemas, webhook service 2026-03-22 07:32:04 +03:00
Fringg 4c2cb63cf9 fix: accept stale Telegram initData to prevent MiniApp auth failures
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.
2026-03-22 07:24:27 +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 6c208581d9 fix: sanitize email dots in RemnaWave username generation
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.
2026-03-22 04:59:30 +03:00
Fringg c307278231 fix: prevent MESSAGE_TOO_LONG in promo groups list
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.
2026-03-22 04:17:32 +03:00
Fringg 9eab802000 fix: handle spurious user.deleted webhooks — preserve active subscriptions and prevent orphaned panel users
- 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
2026-03-22 03:20:50 +03:00
Fringg 13ea3768b5 feat: custom broadcast buttons and fix home button to use bot menu
- 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
2026-03-22 01:54:05 +03:00
Fringg ed5a92ab96 fix: referral system — self-referral protection, race condition fix, deleted user re-registration
- 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)
2026-03-21 15:06:10 +03:00
Fringg 48265f1cd4 chore: remove redundant comments from DISABLED status fix 2026-03-21 09:07:43 +03:00
Fringg 3b9568fcc1 style: format long lines in monitoring and subscription services 2026-03-21 09:06:01 +03:00
Fringg 79cfcbcece fix: send DISABLED instead of EXPIRED status to RemnaWave API
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)
2026-03-21 09:00:59 +03:00
Egor 59cd74d307 Merge pull request #2791 from BEDOLAGA-DEV/main
w
2026-03-21 07:38:22 +03:00
Fringg 90209ebef1 feat: add NaloGO fiscal receipts for code-only gift purchases
- 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
2026-03-21 07:37:03 +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 4244962337 fix: add NaloGO fiscal receipt creation for landing page purchases
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.
2026-03-21 06:36:21 +03:00
Fringg ba79d03e38 fix: skip non-JSON payload rows in cryptobot payment index and query
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
2026-03-21 05:43:22 +03:00
Egor 8e4e2ddd1a Update README.md 2026-03-21 05:09:00 +03:00
Egor 38853cdd5a Merge pull request #2790 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.38.0
v3.38.0
2026-03-21 04:32:38 +03:00
github-actions[bot] f837c0c244 chore(main): release 3.38.0 2026-03-21 01:32:11 +00:00
Egor 8a7b9cc651 Merge pull request #2789 from BEDOLAGA-DEV/dev
Dev
2026-03-21 04:31:51 +03:00
Fringg 3bf31055e7 fix: sanitize proxy credentials in all nalogo error paths
- 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
2026-03-21 04:27:15 +03:00
Fringg 3c5bf4fa22 feat: add SOCKS proxy support for nalogo (tax service) module
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
2026-03-21 04:21:11 +03:00
Fringg 4990ddf9e4 fix: add diagnostic payload logging in create_user error path
Consistent with update_user — log full payload before re-raising
non-A039 errors to aid debugging.
2026-03-21 04:08:18 +03:00
Fringg de00612965 fix: retry Remnawave API calls without externalSquadUuid on A039 FK violation
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.
2026-03-21 03:58:21 +03:00
Egor 43f5629c8c Merge pull request #2788 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.37.0
v3.37.0
2026-03-21 03:18:30 +03:00