User-facing portal for VGate, built with Vue 3 + Vite + TypeScript. Customers use
it to log in, subscribe to a node, browse plans, place and pay orders, and monitor
their traffic. It talks to the manager's REST API under /api/v1.
When paying, the order's pay_mode drives the UX: a QR code is shown whenever pay_mode === 'qr'
(with a provider-aware hint covering both Alipay and WeChat Pay), an iap (Apple in-app) mode
sends the user to complete the purchase in the iOS app, and otherwise the portal opens a redirect
link (e.g. Alipay / Stripe / PayPal Checkout). Note the iap handling applies to the plans,
traffic-packages, and change-plan flows — the Orders page's "Pay" action only distinguishes
qr from everything else, so an iap order there opens its pay_url in a new tab instead.
The dashboard shows the user's effective speed cap (sourced from the active plan or a manual
override), and the orders page displays the payment platform (alipay / wechat / stripe /
paypal / apple / manual, …) for each order.
- Vue 3 (Composition API,
<script setup>) - Vite — dev server & build tool
- TypeScript
- Element Plus — UI components (auto-imported)
- Pinia — state management
- Vue Router — routing
- Axios — HTTP client
- qrcode — subscription and payment QR codes
- @element-plus/icons-vue — icon set
- @fortawesome/fontawesome-free — icon font
- dompurify — sanitizes HTML in plan/package descriptions
- Node.js 18+
- npm (this project ships
package-lock.jsononly — there is nopnpm-lock.yaml, so usenpm, notpnpm)
# install dependencies
npm install
# start the dev server (http://localhost:5174)
npm run dev
# type-check without emitting (vue-tsc)
npm run typecheck
# production build → dist/
npm run build
# preview the production build locally
npm run previewIn development, Vite proxies /api to the manager backend at
http://localhost:8081 (see server.proxy in vite.config.ts), so the app talks to
the backend without CORS issues during local development. The user dev server runs on
port 5174 (the admin frontend uses 5173).
The API base URL is read at runtime from a global variable
(window.__ENV__.API_BASE_URL) injected by public/env.js. The file is copied
verbatim into dist/env.js on build and is not bundled, so you can edit the
backend address after deployment without rebuilding.
src/api/http.ts uses it as the axios baseURL, falling back to the relative path
/api/v1 when it is empty:
// dist/env.js — edit this file on the deployed server
window.__ENV__ = { API_BASE_URL: '' } // '' → relative /api/v1 (reverse-proxy / same-origin)
// window.__ENV__ = { API_BASE_URL: 'http://192.168.1.10:8081/api/v1' } // separate host:port- Leave
API_BASE_URLempty when the frontend and backend are served from the same origin (e.g. behind an Nginx reverse proxy that routes/apito the manager). - Set the full backend URL (including the
/api/v1path) when the manager runs on a different host/port. In that case the manager must allow the frontend origin via its CORSallowed_originssystem config.
User sessions use JWT access tokens that are not refreshable. The axios response
interceptor in src/api/http.ts treats a 401 as "session expired": it clears the
stored token and redirects the user to /login (there is no silent auto-refresh,
unlike the admin console). The login form also supports a Cloudflare Turnstile field
(cf_turnstile_response) when the manager requires it.
Registration auto-logs-in: POST /user/register returns a session on both 201
(active) and 202 (pending email verification), so a new user lands straight on the
dashboard's verify banner. The Register option appears inside the /login page only when
the manager enables self-service registration (user.register_enabled). Email verification
gates purchases and proxy traffic, not login — an unverified user can browse but cannot place
orders or consume traffic until email_verified is true. The /verify-email page (also reachable
while already logged in) completes verification and clears the banner.
- Public (no auth):
/login,/verify-email - Protected (under
UserLayout, require auth):/— Dashboard/subscription— current subscription + QR code/plans— browse and pick a plan/orders— order history and pending orders/traffic— traffic usage/traffic-packages— buy one-off traffic add-on packages (requires an active plan)/invites— invite codes / referral/redeem— redeem a redemption code (issued by the admin console)/announcements— system announcements/tickets— support tickets (open, reply, close; choose Telegram/email notifications)/settings— account settings / change password, and link a Telegram account
Unauthenticated users hitting a protected route are redirected to /login
(?redirect=…). The one public-route exception is /verify-email, which stays reachable
while authenticated so a logged-in, unverified user can complete verification; every other
public route sends an authenticated visitor to the dashboard.
- Telegram binding — link a personal Telegram account from Settings through a
/startdeep link. Once linked, you can receive ticket alerts there. - Support tickets — open tickets from the Tickets page, reply in the thread, and close your own ticket once it is resolved. When creating a ticket you choose how you are notified of admin replies: Telegram (the default when your account is linked), Email, or None.
npm run buildproduces a staticdist/directory.- Serve
dist/with any static file server (Nginx, Caddy, etc.). - Edit
dist/env.jsto pointAPI_BASE_URLat your manager backend. - (Recommended) Put a reverse proxy in front so
/apiis forwarded to the manager — thenAPI_BASE_URLcan stay empty and no CORS configuration is needed.