Business
Users & support
What it does
/_authed/users is the master customer record: every Telegram user who has interacted with the bot, merged with their VPN accounts, payments, wallet ledgers, referral relationships, and gift/coupon redemptions, plus admin tools to grant/deduct balances, message users, block/unblock, and run bulk actions. It supports ?tg=<telegramId> deep-linking to open a specific user (used from other pages such as a wallet ledger row).
Fields and merged data sources
| Source | Feeds |
|---|---|
| api.users.list() | BotUser[] — the base Telegram user records. |
| api.accounts.list() | VPN accounts, matched to users. |
| api.payments.list('approved') | Approved purchase history. |
| api.invoices.txns() | Full wallet ledger across all tracks. |
| api.wallet.summary() | Aggregate per-user balances and pending counts. |
| api.invoices.list() | Invoice records linkable back to purchases. |
| api.gifts.list().redemptions | Gift/coupon/redeem-code history. |
| api.logs.list(undefined, 5000) | Audit log entries for cross-referencing admin actions. |
Per-user wallet breakdown (real vs gifted vs coupon-discounted, spent vs held) is derived purely client-side by scanning ledger entries with note/kind/ref regex heuristics — this is the most detailed reporting surface in the app, but it is derived, not authoritative; the wallet ledger itself is the source of truth.
Linking accounts
A Telegram user is linked to their VPN account(s) by cross-referencing the accounts list against the user's Telegram identity — a VPN account purchased through the bot carries the buyer's Telegram id, which is how the Users page assembles "this Telegram user owns these accounts." The referral graph is built the same way, by cross-referencing referredBy (a raw referrer Telegram id or legacy referral code) against referralCode/telegramId, robust to both storage schemes.
Blocking
- 1Open the user's row/detail and click Block; type an internal reason.
- 2
api.users.block(id, reason)blocks the Telegram id from the bot entirely — enforcement happens server-side; the reason is stored for audit. - 3
api.users.unblock(id)reverses this instantly. Blocked users appear under the dedicated Blocked users tab.
Roles and resellers
The panel itself is a single-operator admin console (see Panel login/authentication below) — there is no separate reseller/sub-admin role model visible in the Users page data. Instead, "admin rights inside Telegram" are bootstrapped through the bot: whoever is set as adminChatId in Bot → Main bot (or the first user to run /admin when that field is empty) gets the admin-side Telegram screens described in /docs/telegram-bot §10.9 — effectively acting as a reseller/operator role reachable from inside Telegram, mirroring the web panel's moderation actions (grant points, extend accounts, delete/reset accounts, DM users).
Per-user actions
| Action | Call | What it does |
|---|---|---|
| Reset trial | api.users.resetTrial(id) | Lets the user claim a free trial again. |
| Block / Unblock | api.users.block(id, reason) | Bars the Telegram id from the bot; reversible. |
| Grant/deduct wallet | api.wallet.gift(telegramId, track, amount, reason, planId?) | Adds/removes Points, Top-up money, or Prepaid credit, with a moneyScope for targeting real vs gifted cash specifically on deduction. |
| Direct message | dmOpen/dmTarget/dmText dialog | DM this specific user via the bot. |
| Open invoice | findInvoice() fuzzy match | Matches a stored order:XXX ledger ref against invoices to open the right PDF. |
| Bulk actions | message / reset_traffic / gift_points / gift_topup / gift_prepaid | Runs one action across either the current filtered search results or every user. |
Business export
exportBusinessWorkbook(...) (from src/lib/business-export) generates a full workbook combining users, wallets, accounts, payments, invoices, gifts, and audit logs — for offline accounting/reporting. The totals row on the page separately aggregates real vs gifted vs coupon top-up/prepaid totals and total spend from real cash only, deliberately excluding internally-funded orders (paidWith of top-up/prepaid/points/gift) to avoid double-counting cash receipts vs internal transfers.
What it does
/_authed/support is a unified admin inbox for 1-on-1 Telegram support conversations, decoupled from real-time presence — admins can reply whenever, delivered via whichever bot (main or dedicated support bot) is configured.
Data and statuses
| Call | Returns / does |
|---|---|
| api.support.threads() | SupportThread[] — telegramId, name, accountId?, total, unread, lastAt, lastSender, lastText. Polled every 12s. |
| api.support.messages(telegramId) | SupportMessage[] for the selected thread: id, sender (admin|user), text, caption, fileId, mediaType, bot (main|support), createdAt, deliveredAt, deliveryError. Polled every 8s. |
| api.support.markRead(telegramId) | Clears the unread count when a thread is opened. |
| api.support.reply(telegramId, text) | Sends a text reply. If Telegram can't deliver immediately, it is queued and auto-retried. |
| api.support.replyMedia(telegramId, file, caption?) | Sends an image/video/audio/document reply (images, video, audio, pdf, zip, doc/docx/txt). |
| api.support.fileUrl(fileId, bot) | Resolves inbound media (photos, videos, voice, stickers, documents) for display. |
Delivery status per message is one of:
- Delivered
- Sent successfully.
- Queued
- Not yet delivered, will retry — usually the user hasn't started the bot yet.
- Undeliverable
- Permanent failure: user blocked the bot, never started/opened it, or their account is deactivated — detected by keyword-matching deliveryError.
Second support bot relation
Reply-mode configuration lives in Bot → Support bot → Reply mode (see /docs/telegram-bot). That setting controls what the bot itself auto-sends before or regardless of a human reply (silent forwarding, an ETA acknowledgement, or nothing); this Support page is where the admin's actual human answers are typed and sent, whichever bot process is delivering them.
Setup / usage
- 1A user messages the bot (or a dedicated support bot) with a question; it becomes a thread.
- 2Click the thread in the left list to open the conversation.
- 3Type a reply and press Enter/Send, or attach a file with the paperclip icon.
- 4Threads with unread messages show a red counter badge; opening one marks it read.
- 5If a reply is flagged Queued/Undeliverable, ask the user to press Start on the relevant bot — delivery retries automatically once they do.
Mechanism
The panel itself (/login) is a simple username + password form — no MFA is visible in this codebase. It submits via api.auth.login(username, password) → POST /auth/login with a JSON { username, password } body. On success the ["me"] query is invalidated and you're redirected to a safe relative ?next= path (must start with /, must not start with // or /api, preventing open-redirect / API-hijack), or to / by default.
Sessions
The _authed.* layout redirects to /login whenever there is no session, checked via hasSession() from src/lib/session. Session persistence is cookie-based, set on a successful /auth/login; logging out clears that session and returns you to /login.
2FA
No MFA in this codebase
Admin roles
This is a single-operator admin console: there is no visible user-registration or role-based access control for the web panel itself, and no self-signup flow — distinct from the many Telegram end-users managed through the bot (who do have a lightweight admin/user split, bootstrapped via adminChatId, as described in /docs/telegram-bot).
Error handling
| Condition | Message shown |
|---|---|
| 401 / 403 | Incorrect username or password. |
| 429 | Too many attempts. Wait a moment and try again. (server-side rate limiting) |
| 503 | Panel is under maintenance. Try again in a minute. |
| >=500 | The panel service is not responding. Try again shortly. |
| Network error | Can't reach the panel. Check your connection. |
| Other | Falls back to the raw error message. |
Setup — first admin login
- 1Navigate to /login.
- 2Enter the default/initial operator username and password provisioned during the
autoscriptinstallation. - 3On success you land on the Dashboard (/_authed/).
- 4Immediately go to Settings → System → Change admin password (
api.settings.changePassword(current, next)) to set a strong password — this is the only way to change the panel login credential in this codebase.
