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.
- 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 QR codes
- 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, and there is a /verify-email
flow for email verification.
- 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/invites— invite codes / referral/announcements— system announcements/settings— account settings / change password
Unauthenticated users hitting a protected route are redirected to /login
(?redirect=…); already-authenticated users visiting a public route are sent to the
dashboard.
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.