Browse Source

refactor: focused service files, leaf subpackages, and an internal/ layout (#5167)

* refactor(service): split client.go into focused files

client.go had grown to 4455 lines mixing ~10 responsibilities. Split it
verbatim into cohesive same-package files (no behavior change):

  client.go            foundation: ClientService, ClientWithAttachments,
                       ClientCreatePayload, ErrClientNotInInbound, sqlInChunk
  client_locks.go      inbound mutation locks, delete tombstones, compactOrphans
  client_lookup.go     read-only lookups (GetByID, List, EffectiveFlow, ...)
  client_link.go       inbound association sync (SyncInbound, DetachInbound, ...)
  client_crud.go       single-client CRUD + validation + protocol defaults
  client_inbound_apply.go  low-level inbound-settings mutators + by-email setters
  client_bulk.go       bulk attach/detach/adjust/delete/create + DelDepleted
  client_traffic.go    traffic-reset paths
  client_groups.go     client group management
  client_paging.go     paged listing, filtering, sorting, summary

Every declaration moved unchanged (verified: identical func/type/const/var
signature set before vs after). Imports redistributed per file via goimports.
go build ./..., go vet, and go test ./web/service/... all pass.

* refactor(service): split inbound.go into focused files

inbound.go was 4100 lines. Split it verbatim into cohesive same-package
files (no behavior change):

  inbound.go             core inbound CRUD + InboundService (keeps pkg doc)
  inbound_protocol.go    protocol / stream capability helpers
  inbound_node.go        node/runtime/remote coordination + online tracking
  inbound_traffic.go     traffic accounting, reset, client stats
  inbound_client_ips.go  per-client IP tracking
  inbound_clients.go     client lookups within inbounds + copy-clients
  inbound_disable.go     auto-disable invalid inbounds/clients
  inbound_migration.go   DB migrations
  inbound_sublink.go     subscription link providers
  inbound_util.go        generic slice/string helpers

Identical func/type/const/var signature set before vs after; package doc
comment preserved on inbound.go. Imports redistributed via goimports.
Build, vet, and go test ./web/service/... all pass.

* refactor(service): split tgbot.go into focused files

tgbot.go was 3738 lines dominated by a 1246-line answerCallback. Split it
verbatim into cohesive same-package files (no behavior change):

  tgbot.go           lifecycle, bot setup, caches, small utils
  tgbot_router.go    incoming update / command / callback dispatch
  tgbot_send.go      outbound messaging primitives
  tgbot_client.go    client views, actions, subscription links
  tgbot_inbound.go   inbound listing / pickers
  tgbot_report.go    server usage, exhausted, online, backups, notifications

Identical func/type/const/var signature set before vs after. Imports
redistributed via goimports. Build, vet, and go test ./web/service/... pass.

* refactor(client): dedupe single-field by-email setters

ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, and
ResetClientTrafficLimitByEmail shared an identical ~50-line body that
resolves the inbound by email, confirms the client exists, rewrites a
single-client settings payload, and delegates to UpdateInboundClient.

Extract that into applyClientFieldByEmail(inboundSvc, email, mutate) and
reduce each setter to a 3-line wrapper. Behavior is unchanged: same checks
and error strings, same single-client payload contract, same totalGB guard.

SetClientTelegramUserID (resolves by traffic id, different error text) and
ToggleClientEnableByEmail/SetClientEnableByEmail (different return shape and
a pre-read of the old state) intentionally keep their own bodies.

* refactor(service): extract panel/ subpackage

Move the panel-administration leaf services out of the flat service
package into web/service/panel/ (package panel):

  user.go         UserService (auth / 2FA / LDAP)
  panel.go        PanelService (restart / self-update) + version helpers
  panel_other.go  non-unix RestartPanel
  panel_unix.go   unix RestartPanel
  api_token.go    ApiTokenService
  websocket.go    WebSocketService
  panel_test.go   version/shellQuote unit tests

These are leaves: they depend on core (SettingService, Release) but no
core file references them, so the extraction creates no import cycle.
Core references are now qualified (service.SettingService, service.Release);
callers in main.go, web/web.go, and web/controller/* updated to panel.*.
Build, vet, and go test ./web/... pass.

* refactor(service): extract integration/ subpackage

Move the external-provider integration leaves into web/service/integration/
(package integration):

  warp.go        WarpService (Cloudflare WARP)
  nord.go        NordService (NordVPN)
  custom_geo.go  CustomGeoService (custom geo asset management)
  *_test.go      custom_geo / panel-proxy tests

These depend on core (SettingService, ServerService, XraySettingService) but
no core file references them. xray_setting.go stays in core because it calls
the unexported SettingService.saveSetting. The shared isBlockedIP SSRF helper
(used by core url_safety.go and by custom_geo) now has a small copy in each
package rather than being exported. Core references qualified; callers in
web/web.go, web/job/*, and web/controller/* updated to integration.*.
Build, vet, and go test ./web/... pass.

* refactor(service): extract tgbot/ subpackage

Move the Telegram bot (6 files + test) into web/service/tgbot/ (package
tgbot). It is a leaf: it embeds five core services (Inbound/Client/Setting/
Server/Xray) and the core never references it, so no import cycle.

To support the package boundary without changing behavior:
  - core exposes XrayProcess() *xray.Process so tgbot keeps calling the
    exact same running-process methods it used via the package-level `p`;
  - three core methods tgbot calls are exported: ClientService.checkIs-
    EnabledByEmail -> CheckIsEnabledByEmail, InboundService.getAllEmails ->
    GetAllEmails (callers updated in-package);
  - tgbot's embedded-field types and the few core type refs (Status,
    ClientCreatePayload, SanitizePublicHTTPURL) are now service-qualified.

Callers in main.go, web/web.go, web/job/*, and web/controller/* updated to
tgbot.*. Build, vet, and go test ./web/... pass.

* refactor(service): extract outbound/ subpackage

OutboundService (outbound.go) imports only neutral packages (config,
database, model, xray) and its production code is referenced by no core or
sibling service file — only by web/controller/xray_setting.go and
web/job/xray_traffic_job.go. Move it to web/service/outbound/ (package
outbound); no core qualification needed inside. Callers updated to outbound.*.

The one coupling was a tiny pure test helper, outboundsContainTag, used by
both outbound.go and the core outbound_subscription_test.go; it now has a
small copy in that test file rather than being shared across the boundary.
Build, vet, and go test ./web/... pass.

* refactor(util): move wireguard into its own subpackage

util/wireguard.go was the lone file of the root `util` package (24 lines,
one exported func GenerateWireguardKeypair), while every other util concern
lives in a focused subpackage (util/common, util/crypto, util/netsafe, ...).
Move it to util/wireguard/ (package wireguard) for consistency; its only
importer, web/service/integration/warp.go, is updated. The root `util`
package no longer exists.

* refactor(sub): drop redundant sub prefix from filenames

Inside package sub the subXxx.go prefix just repeats the package name
(like client_*.go did inside service). Rename for consistency; content and
type names are unchanged:

  subController.go    -> controller.go
  subService.go       -> service.go
  subClashService.go  -> clash_service.go
  subJsonService.go   -> json_service.go
  (+ matching _test.go files)

* refactor(controller): rename xui.go -> spa.go

XUIController serves the panel's single-page-app shell; spa.go names that
role plainly (the other controller files are domain-named). File rename only
— the type stays XUIController. api_docs_test.go keys route base paths by
filename, so its "xui.go" case is updated to "spa.go".

* refactor: move backend packages under internal/

Adopt the idiomatic Go application layout: the backend packages now live
under internal/ (a boundary the toolchain enforces), signalling private
implementation instead of a library-style flat root. No runtime behavior
changes — only import paths and a few build/config paths move.

Moved: config, database, logger, mtproto, sub, util, web, xray -> internal/.
main.go stays at the repo root and tools/openapigen stays under tools/ (both
still import internal/* because the internal rule keys off the module root).
The module path github.com/mhsanaei/3x-ui/v3 is unchanged; 149 .go files had
their import prefix rewritten to .../internal/<pkg>.

Couplings the Go compiler can't see, updated to the new layout:
  - frontend i18n imports of web/translation (react.ts, setup.components.ts)
  - vite outDir + eslint/tsconfig ignore globs -> internal/web/dist
  - Dockerfile COPY paths for web/dist and web/translation
  - locale.go os.DirFS("web") disk fallback -> "internal/web"
  - .gitignore and ci.yml go:embed stub for internal/web/dist
  - api_docs_test.go repo-root relative walk (one level deeper)
  - tools/openapigen filesystem package paths; ApiTokenView repointed to the
    web/service/panel subpackage and codegen regenerated (clears a stale
    type the ci.yml codegen check was failing on)

Verified: go build/vet/test (all packages), and frontend typecheck, lint,
vitest (478 tests), and production build into internal/web/dist.

* fix(config): keep test runs from writing logs into the source tree

GetLogFolder() returns a CWD-relative "./log" on Windows. Under `go test`
the working directory is each package's own folder, so InitLogger (called by
tests in web/job, web/service, xray, web/websocket) created stray log/
directories scattered through the source tree (e.g. internal/web/job/log/).

Redirect to a shared temp folder when testing.Testing() reports a test run.
Production behavior is unchanged: Windows still uses ./log next to the binary
and Linux /var/log/x-ui. The log files were always gitignored (*.log) and
never committed; this just stops the noise at the source.

* docs: move subscription-template guide out of root into docs/

sub_templates/ was a top-level folder holding only a README and no actual
templates (3x-ui ships none by design), referenced nowhere and unlinked from
any doc — it read like an empty placeholder cluttering the repo root.

Move the guide to docs/custom-subscription-templates.md (a proper docs home),
reword its intro to read as documentation rather than a folder note, link it
from the Features list in README.md, and drop the empty sub_templates/ folder.

* fix: update stale web/ path references after the internal/ move

The internal/ migration rewrote Go import paths but left some references to
the old top-level layout in docs, comments, and a few runtime disk paths.

Functional (dev-mode only): the disk-serving fallbacks that read the Vite
build from disk when running from source still pointed at web/dist/, which
moved to internal/web/dist/ — so `os.DirFS`/`os.Stat`/`os.ReadFile` in
internal/web/web.go and internal/sub/{sub,controller}.go are corrected.
Production was unaffected (it serves the embedded FS; verified by the Docker
build), but `go run` with a live frontend build silently fell back to embed.

Docs/comments: frontend/README.md, CONTRIBUTING.md, the claude-issue-bot and
release workflows, the openapigen -root help text, and assorted Go comments
now reference internal/web, internal/database, internal/sub, internal/xray,
etc. Package-name mentions (the "web" package), root paths (main.go,
frontend/, install scripts, /etc/x-ui), routes (/panel/api/xray), and the
historical "web/assets no longer exists" note were intentionally left as-is.

* refactor(web): remove the legacy /xui -> /panel redirect middleware

RedirectMiddleware existed only for backward compatibility with the old
`/xui` URL scheme (301-redirecting /xui and /xui/API to /panel and
/panel/api). That cutover was long ago, so drop the middleware, its
registration in initRouter, and the now-inaccurate "URL redirection"
mention in the middleware package doc. Old /xui URLs now 404 like any other
unknown path. HTTPS auto-redirect and auth redirects are unrelated and stay.

* build: fix .dockerignore for internal/ layout and exclude runtime dir

- web/dist -> internal/web/dist: the embedded frontend moved under internal/,
  so the stale exclude no longer matched and the locally-built dist could be
  sent to the build context (the frontend stage rebuilds it fresh anyway).
- exclude x-ui/: the local runtime directory (SQLite db, geo .dat files, xray
  binaries, certs — ~150MB) was being shipped into the build context for no
  reason. Verified the pattern excludes only the directory and still keeps
  x-ui.sh, which the Dockerfile copies to /usr/bin/x-ui.
Sanaei 8 giờ trước cách đây
mục cha
commit
41645255f1
100 tập tin đã thay đổi với 231 bổ sung222 xóa
  1. 2 1
      .dockerignore
  2. 4 4
      .github/workflows/ci.yml
  3. 18 19
      .github/workflows/claude-issue-bot.yml
  4. 2 2
      .github/workflows/release.yml
  5. 4 7
      .gitignore
  6. 9 9
      CONTRIBUTING.md
  7. 3 3
      Dockerfile
  8. 1 1
      README.md
  9. 1 1
      docs/custom-subscription-templates.md
  10. 5 5
      frontend/README.md
  11. 1 1
      frontend/eslint.config.js
  12. 1 1
      frontend/eslint.deprecated.config.js
  13. 0 1
      frontend/src/generated/types.ts
  14. 0 3
      frontend/src/generated/zod.ts
  15. 4 4
      frontend/src/i18n/react.ts
  16. 1 1
      frontend/src/test/setup.components.ts
  17. 1 1
      frontend/tsconfig.json
  18. 1 1
      frontend/vite.config.js
  19. 7 0
      internal/config/config.go
  20. 0 0
      internal/config/name
  21. 0 0
      internal/config/version
  22. 5 5
      internal/database/db.go
  23. 1 1
      internal/database/db_seed_test.go
  24. 0 0
      internal/database/dialect.go
  25. 0 0
      internal/database/dump_sqlite.go
  26. 2 2
      internal/database/dump_sqlite_test.go
  27. 3 3
      internal/database/migrate_data.go
  28. 1 1
      internal/database/migrate_data_test.go
  29. 2 2
      internal/database/model/model.go
  30. 0 0
      internal/database/model/model_mtproto_test.go
  31. 0 0
      internal/database/model/model_test.go
  32. 0 0
      internal/database/model/node_client_traffic.go
  33. 1 1
      internal/logger/logger.go
  34. 2 2
      internal/mtproto/manager.go
  35. 1 1
      internal/mtproto/manager_test.go
  36. 0 0
      internal/mtproto/orphans_linux.go
  37. 0 0
      internal/mtproto/orphans_other.go
  38. 2 2
      internal/mtproto/process.go
  39. 0 0
      internal/mtproto/process_other.go
  40. 1 1
      internal/mtproto/process_windows.go
  41. 1 1
      internal/sub/build_urls_test.go
  42. 3 3
      internal/sub/clash_service.go
  43. 1 1
      internal/sub/clash_service_test.go
  44. 4 4
      internal/sub/controller.go
  45. 0 0
      internal/sub/controller_test.go
  46. 0 0
      internal/sub/default.json
  47. 2 2
      internal/sub/dist.go
  48. 5 5
      internal/sub/json_service.go
  49. 1 1
      internal/sub/json_service_test.go
  50. 2 2
      internal/sub/links.go
  51. 0 0
      internal/sub/links_test.go
  52. 9 9
      internal/sub/service.go
  53. 1 1
      internal/sub/service_test.go
  54. 3 3
      internal/sub/service_userinfo_test.go
  55. 8 8
      internal/sub/sub.go
  56. 1 1
      internal/util/common/err.go
  57. 0 0
      internal/util/common/format.go
  58. 0 0
      internal/util/common/format_test.go
  59. 0 0
      internal/util/common/multi_error.go
  60. 0 0
      internal/util/common/multi_error_test.go
  61. 0 0
      internal/util/crypto/crypto.go
  62. 0 0
      internal/util/crypto/crypto_test.go
  63. 0 0
      internal/util/json_util/json.go
  64. 0 0
      internal/util/json_util/json_test.go
  65. 0 0
      internal/util/ldap/ldap.go
  66. 0 0
      internal/util/link/outbound.go
  67. 0 0
      internal/util/link/outbound_test.go
  68. 0 0
      internal/util/netproxy/netproxy.go
  69. 0 0
      internal/util/netproxy/netproxy_test.go
  70. 0 0
      internal/util/netsafe/netsafe.go
  71. 0 0
      internal/util/netsafe/netsafe_test.go
  72. 0 0
      internal/util/random/random.go
  73. 0 0
      internal/util/random/random_test.go
  74. 0 0
      internal/util/reflect_util/reflect.go
  75. 0 0
      internal/util/sys/psutil.go
  76. 0 0
      internal/util/sys/sys_darwin.go
  77. 0 0
      internal/util/sys/sys_linux.go
  78. 0 0
      internal/util/sys/sys_windows.go
  79. 1 1
      internal/util/wireguard/wireguard.go
  80. 11 8
      internal/web/controller/api.go
  81. 2 2
      internal/web/controller/api_docs_test.go
  82. 3 3
      internal/web/controller/base.go
  83. 3 3
      internal/web/controller/client.go
  84. 19 19
      internal/web/controller/custom_geo.go
  85. 3 3
      internal/web/controller/dist.go
  86. 0 0
      internal/web/controller/dist_test.go
  87. 2 2
      internal/web/controller/group.go
  88. 5 5
      internal/web/controller/inbound.go
  89. 14 12
      internal/web/controller/index.go
  90. 0 0
      internal/web/controller/login_limiter.go
  91. 0 0
      internal/web/controller/login_limiter_test.go
  92. 3 3
      internal/web/controller/node.go
  93. 9 8
      internal/web/controller/server.go
  94. 9 8
      internal/web/controller/setting.go
  95. 3 3
      internal/web/controller/spa.go
  96. 3 3
      internal/web/controller/util.go
  97. 0 0
      internal/web/controller/util_test.go
  98. 6 6
      internal/web/controller/websocket.go
  99. 7 5
      internal/web/controller/xray_setting.go
  100. 1 1
      internal/web/entity/entity.go

+ 2 - 1
.dockerignore

@@ -1,9 +1,10 @@
 .git
 **/node_modules
-web/dist
+internal/web/dist
 build
 db
 cert
 pgdata
+x-ui/
 *.db
 *.dump

+ 4 - 4
.github/workflows/ci.yml

@@ -30,8 +30,8 @@ jobs:
         with:
           go-version-file: go.mod
           cache: true
-      - name: Stub web/dist for go:embed
-        run: mkdir -p web/dist && touch web/dist/.gitkeep
+      - name: Stub internal/web/dist for go:embed
+        run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
       - name: Test
         run: |
           go list ./... | grep -v '/frontend/node_modules/' > /tmp/go-packages.txt
@@ -62,8 +62,8 @@ jobs:
         with:
           go-version-file: go.mod
           cache: true
-      - name: Stub web/dist for go:embed
-        run: mkdir -p web/dist && touch web/dist/.gitkeep
+      - name: Stub internal/web/dist for go:embed
+        run: mkdir -p internal/web/dist && touch internal/web/dist/.gitkeep
       - name: Install govulncheck
         run: go install golang.org/x/vuln/cmd/govulncheck@latest
       - name: Run govulncheck

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 18 - 19
.github/workflows/claude-issue-bot.yml


+ 2 - 2
.github/workflows/release.yml

@@ -53,8 +53,8 @@ jobs:
           check-latest: true
 
       # Frontend dist must be built BEFORE go build — Go's //go:embed
-      # all:dist directive in web/web.go requires web/dist/ to exist
-      # at compile time. web/dist/ is .gitignored, so on a fresh CI
+      # all:dist directive in internal/web/web.go requires internal/web/dist/ to exist
+      # at compile time. internal/web/dist/ is .gitignored, so on a fresh CI
       # checkout it doesn't exist until vite emits it.
       - name: Setup Node.js
         uses: actions/setup-node@v6

+ 4 - 7
.gitignore

@@ -16,20 +16,17 @@ tmp/
 # Ignore build and distribution directories
 backup/
 bin/
+x-ui/
 dist/
-!web/dist/
-web/dist/*
-!web/dist/.gitkeep
+!internal/web/dist/
+internal/web/dist/*
+!internal/web/dist/.gitkeep
 release/
 node_modules/
 
 # Ignore compiled binaries
 main
 
-# Ignore script and executable files
-/release.sh
-/x-ui
-
 # Ignore OS specific files
 .DS_Store
 Thumbs.db

+ 9 - 9
CONTRIBUTING.md

@@ -135,7 +135,7 @@ The panel UI is a **React 19 + Ant Design 6 + TypeScript** app under `frontend/`
 
 ### Architecture
 
-The frontend ships **three Vite bundles**, each emitted into `web/dist/` and embedded into the Go binary at compile time via `embed.FS`:
+The frontend ships **three Vite bundles**, each emitted into `internal/web/dist/` and embedded into the Go binary at compile time via `embed.FS`:
 
 - **`index.html`** — the admin panel, a **single-page app**. `src/main.tsx` mounts a `react-router` `createBrowserRouter` (see `src/routes.tsx`) under the `/panel` basename; every route (`/panel`, `/panel/inbounds`, `/panel/clients`, `/panel/groups`, `/panel/nodes`, `/panel/settings`, `/panel/xray`, `/panel/api-docs`) is lazy-loaded inside a shared `PanelLayout` (sidebar + header + `<Outlet>`).
 - **`login.html`** — the login + 2FA screen (`src/entries/login.tsx`), a standalone bundle.
@@ -153,7 +153,7 @@ Panel navigation happens client-side through React Router, and per-route code is
 
 ### i18n
 
-Locale strings live in `web/translation/<locale>.json`, **not** under `frontend/`. The Go binary embeds the same JSON and serves it to both backend templates and `react-i18next` (initialized in `src/i18n/react.ts`). When a new English key is added it must also land in **every** non-English locale — missing keys do not break the build, they just render the raw key in the UI.
+Locale strings live in `internal/web/translation/<locale>.json`, **not** under `frontend/`. The Go binary embeds the same JSON and serves it to both backend templates and `react-i18next` (initialized in `src/i18n/react.ts`). When a new English key is added it must also land in **every** non-English locale — missing keys do not break the build, they just render the raw key in the UI.
 
 ### Two dev workflows
 
@@ -184,7 +184,7 @@ Only a genuinely **standalone bundle** (like `login` or `subpage`, reachable wit
 - **No `//` line comments** in committed JS/TS/Vue/Go. HTML `<!-- ... -->` is fine for template structure. Names should carry the meaning; rename rather than annotate. Comments are reserved for the *why*, and only when the reason is surprising.
 - **RTL is a first-class concern.** Persian and Arabic users matter — RTL is enabled through AntD's `ConfigProvider direction="rtl"`. When writing Persian text in toasts or labels, isolate code identifiers on their own lines so RTL reading flows.
 - **Schemas over `any`.** New config shapes go in `src/schemas/`; `@typescript-eslint/no-explicit-any` is an error and production schemas use no `.loose()`. Validate form fields with `antdRule(Schema.shape.field, t)` rather than inline `z.string()` in rules.
-- **Document new endpoints.** Every new `g.POST`/`g.GET` in `web/controller/` needs a matching entry in `src/pages/api-docs/endpoints.ts` — it drives both the in-panel API docs and the generated OpenAPI/Zod (`npm run gen:api` / `gen:zod`).
+- **Document new endpoints.** Every new `g.POST`/`g.GET` in `internal/web/controller/` needs a matching entry in `src/pages/api-docs/endpoints.ts` — it drives both the in-panel API docs and the generated OpenAPI/Zod (`npm run gen:api` / `gen:zod`).
 - **Do not break link generation.** Share-link logic lives in `src/lib/xray/` (`inbound-link.ts`, `outbound-link-parser.ts`, …) and is round-tripped by the golden fixture suite — run `npm run test` after any change to URL generation, defaults, or TLS/Reality handling, and regenerate snapshots (`npx vitest run -u`) only for intentional changes. Two runtime paths consume it: the **inbounds page** and the **clients page** subscription links (`/panel/api/clients/subLinks/:subId` → backend `GetSubs`); exercise both.
 - **Vite is pinned to an exact version** (no `^`) in `frontend/package.json` — currently `8.0.16` — so local, CI, and release builds resolve identically. Bump it deliberately and verify both `npm run dev` and `npm run build` afterward.
 
@@ -209,7 +209,7 @@ frontend/
     ├── components/        — cross-page React components
     ├── hooks/             — reusable hooks (useTheme, useWebSocket, useClients, useDatepicker, …)
     ├── api/               — Axios + CSRF interceptor, TanStack Query provider/keys, WebSocket client
-    ├── i18n/              — react-i18next bootstrap (JSON lives in web/translation/)
+    ├── i18n/              — react-i18next bootstrap (JSON lives in internal/web/translation/)
     ├── lib/xray/          — pure xray logic: link generation, defaults, form ⇄ wire adapters
     ├── schemas/           — Zod source of truth for the xray config model
     ├── generated/         — code-generated Zod + TS types from Go (do not hand-edit)
@@ -226,12 +226,12 @@ For deeper notes on the frontend toolchain see [`frontend/README.md`](frontend/R
 | Path | Contents |
 |------|----------|
 | `main.go` | Process entry point, CLI subcommands, signal handling |
-| `web/` | Gin HTTP server, controllers, services, embedded frontend assets |
+| `internal/web/` | Gin HTTP server, controllers, services, embedded frontend assets |
 | `frontend/` | React + Ant Design 6 + TypeScript source for the panel UI |
-| `database/` | GORM models, migrations, seeders (SQLite / PostgreSQL) |
-| `xray/` | Xray-core process lifecycle and gRPC API client |
-| `sub/` | Subscription endpoints (raw, JSON, Clash) |
-| `config/` | Environment-variable helpers, paths, defaults |
+| `internal/database/` | GORM models, migrations, seeders (SQLite / PostgreSQL) |
+| `internal/xray/` | Xray-core process lifecycle and gRPC API client |
+| `internal/sub/` | Subscription endpoints (raw, JSON, Clash) |
+| `internal/config/` | Environment-variable helpers, paths, defaults |
 | `x-ui/` | **Runtime data** — db, logs, xray binary, geo files (gitignored) |
 
 ## Sending a pull request

+ 3 - 3
Dockerfile

@@ -6,7 +6,7 @@ WORKDIR /src/frontend
 COPY frontend/package.json frontend/package-lock.json ./
 RUN npm ci
 COPY frontend/ ./
-COPY web/translation /src/web/translation
+COPY internal/web/translation /src/internal/web/translation
 RUN npm run build
 
 # ========================================================
@@ -23,7 +23,7 @@ RUN apk --no-cache --update add \
   unzip
 
 COPY . .
-COPY --from=frontend /src/web/dist ./web/dist
+COPY --from=frontend /src/internal/web/dist ./internal/web/dist
 
 ENV CGO_ENABLED=1
 ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
@@ -48,7 +48,7 @@ RUN apk add --no-cache --update \
 COPY --from=builder /app/build/ /app/
 COPY --from=builder /app/DockerEntrypoint.sh /app/
 COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
-COPY --from=builder /app/web/translation /app/web/translation
+COPY --from=builder /app/internal/web/translation /app/internal/web/translation
 
 
 # Configure fail2ban

+ 1 - 1
README.md

@@ -33,7 +33,7 @@ Built as an enhanced fork of the original X-UI project, 3X-UI adds broader proto
 - **Traffic statistics** — per inbound, per client, and per outbound, with reset controls.
 - **Multi-node support** — manage and scale across multiple servers from a single panel.
 - **Outbound & routing** — WARP, NordVPN, custom routing rules, load balancers, and outbound proxy chaining.
-- **Built-in subscription server** with multiple output formats.
+- **Built-in subscription server** with multiple output formats and [custom page templates](docs/custom-subscription-templates.md).
 - **Telegram bot** for remote monitoring and management.
 - **RESTful API** with in-panel Swagger documentation.
 - **Flexible storage** — SQLite (default) or PostgreSQL.

+ 1 - 1
sub_templates/README.md → docs/custom-subscription-templates.md

@@ -1,6 +1,6 @@
 # 3x-ui Custom Subscription Templates
 
-This directory allows you to use custom HTML templates for your users' subscription pages.
+3x-ui can render your users' subscription pages from your own custom HTML templates.
 
 ## How to use a Custom Template
 

+ 5 - 5
frontend/README.md

@@ -3,7 +3,7 @@
 React 19 + Ant Design 6 + TypeScript + Vite 8. Three SPA bundles —
 `index.html` (admin panel SPA, all `/panel/*` routes), `login.html`
 (login + 2FA), and `subpage.html` (public subscription viewer). All
-three are built into `../web/dist/` and embedded into the Go binary
+three are built into `../internal/web/dist/` and embedded into the Go binary
 via `embed.FS`.
 
 State is split between local `useState`, TanStack Query for server
@@ -30,7 +30,7 @@ production-style links work without round-tripping through Go.
 | Command | What |
 |---|---|
 | `npm run dev` | Vite dev server with API + WS proxy to Go |
-| `npm run build` | Regenerates OpenAPI + Zod, then builds into `../web/dist/` |
+| `npm run build` | Regenerates OpenAPI + Zod, then builds into `../internal/web/dist/` |
 | `npm run preview` | Serve the built bundle locally |
 | `npm run typecheck` | `tsc --noEmit` (strict, no emit) |
 | `npm run lint` | ESLint flat config (`@typescript-eslint` + `react-hooks`) |
@@ -62,11 +62,11 @@ the wall-clock time.
 npm run build
 ```
 
-Outputs to `../web/dist/` (HTML at the root, hashed JS/CSS under
+Outputs to `../internal/web/dist/` (HTML at the root, hashed JS/CSS under
 `assets/`). `manualChunks` splits AntD, icons, codemirror, and
 react-query into separate vendor bundles to keep the per-page
 initial JS small. The Go binary embeds this directory at compile
-time and `web/controller/dist.go` serves the per-page HTML.
+time and `internal/web/controller/dist.go` serves the per-page HTML.
 
 ## Layout
 
@@ -93,7 +93,7 @@ frontend/
     ├── hooks/           # useClients, useTheme, useWebSocket, …
     ├── api/             # Axios + CSRF interceptor, TanStack Query bridge,
     │                    #   WebSocket client + queryClient.ts
-    ├── i18n/            # react-i18next init (locales in web/translation/)
+    ├── i18n/            # react-i18next init (locales in internal/web/translation/)
     ├── lib/xray/        # Pure functions: link generation, defaults,
     │                    #   form ⇄ wire adapters, protocol capabilities
     ├── schemas/         # Zod source-of-truth (see "Schemas" below)

+ 1 - 1
frontend/eslint.config.js

@@ -4,7 +4,7 @@ import reactHooks from 'eslint-plugin-react-hooks';
 import globals from 'globals';
 
 export default [
-  { ignores: ['node_modules/**', '../web/dist/**'] },
+  { ignores: ['node_modules/**', '../internal/web/dist/**'] },
   js.configs.recommended,
   ...tseslint.configs.recommended.map((config) => ({
     ...config,

+ 1 - 1
frontend/eslint.deprecated.config.js

@@ -2,7 +2,7 @@ import tseslint from 'typescript-eslint';
 import reactHooks from 'eslint-plugin-react-hooks';
 
 export default [
-  { ignores: ['node_modules/**', '../web/dist/**', 'src/generated/**'] },
+  { ignores: ['node_modules/**', '../internal/web/dist/**', 'src/generated/**'] },
   {
     files: ['**/*.{ts,tsx}'],
     plugins: {

+ 0 - 1
frontend/src/generated/types.ts

@@ -1,5 +1,4 @@
 // Code generated by tools/openapigen. DO NOT EDIT.
-export type LoginStatus = number;
 export type ProcessState = string;
 export type Protocol = string;
 export type SubLinkProvider = unknown;

+ 0 - 3
frontend/src/generated/zod.ts

@@ -1,8 +1,5 @@
 // Code generated by tools/openapigen. DO NOT EDIT.
 import { z } from 'zod';
-export const LoginStatusSchema = z.number().int();
-export type LoginStatus = z.infer<typeof LoginStatusSchema>;
-
 export const ProcessStateSchema = z.string();
 export type ProcessState = z.infer<typeof ProcessStateSchema>;
 

+ 4 - 4
frontend/src/i18n/react.ts

@@ -2,17 +2,17 @@ import i18next from 'i18next';
 import { initReactI18next } from 'react-i18next';
 
 import { LanguageManager } from '@/utils';
-import enUS from '../../../web/translation/en-US.json';
+import enUS from '../../../internal/web/translation/en-US.json';
 
 const FALLBACK = 'en-US';
 
 const lazyModules = import.meta.glob([
-  '../../../web/translation/*.json',
-  '!../../../web/translation/en-US.json',
+  '../../../internal/web/translation/*.json',
+  '!../../../internal/web/translation/en-US.json',
 ]);
 
 function moduleKeyFor(code: string): string {
-  return `../../../web/translation/${code}.json`;
+  return `../../../internal/web/translation/${code}.json`;
 }
 
 let active: string = LanguageManager.getLanguage();

+ 1 - 1
frontend/src/test/setup.components.ts

@@ -3,7 +3,7 @@ import { cleanup } from '@testing-library/react';
 import i18next from 'i18next';
 import { initReactI18next } from 'react-i18next';
 
-import enUS from '../../../web/translation/en-US.json';
+import enUS from '../../../internal/web/translation/en-US.json';
 
 vi.mock('persian-calendar-suite', () => ({
   PersianDateTimePicker: () => null,

+ 1 - 1
frontend/tsconfig.json

@@ -25,5 +25,5 @@
     }
   },
   "include": ["src/**/*.ts", "src/**/*.tsx"],
-  "exclude": ["node_modules", "../web/dist"]
+  "exclude": ["node_modules", "../internal/web/dist"]
 }

+ 1 - 1
frontend/vite.config.js

@@ -4,7 +4,7 @@ import fs from 'node:fs';
 import path from 'node:path';
 import { DatabaseSync } from 'node:sqlite';
 
-const outDir = path.resolve(__dirname, '../web/dist');
+const outDir = path.resolve(__dirname, '../internal/web/dist');
 const BACKEND_TARGET = 'http://localhost:2053';
 
 function resolveDBPath() {

+ 7 - 0
config/config.go → internal/config/config.go

@@ -10,6 +10,7 @@ import (
 	"path/filepath"
 	"runtime"
 	"strings"
+	"testing"
 )
 
 //go:embed version
@@ -140,6 +141,12 @@ func GetLogFolder() string {
 	if logFolderPath != "" {
 		return logFolderPath
 	}
+	// Under `go test` the Windows default below is CWD-relative ("./log"), which
+	// scatters a log/ directory through the source tree (one per tested package).
+	// Redirect test runs to a shared temp folder so the source tree stays clean.
+	if testing.Testing() {
+		return filepath.Join(os.TempDir(), "3x-ui-test-log")
+	}
 	if runtime.GOOS == "windows" {
 		return filepath.Join(".", "log")
 	}

+ 0 - 0
config/name → internal/config/name


+ 0 - 0
config/version → internal/config/version


+ 5 - 5
database/db.go → internal/database/db.go

@@ -16,11 +16,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/config"
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/util/crypto"
-	"github.com/mhsanaei/3x-ui/v3/util/random"
-	"github.com/mhsanaei/3x-ui/v3/xray"
+	"github.com/mhsanaei/3x-ui/v3/internal/config"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/random"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"

+ 1 - 1
database/db_seed_test.go → internal/database/db_seed_test.go

@@ -6,7 +6,7 @@ import (
 	"regexp"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func TestSeedClientsFromInboundJSON_IsIdempotentAgainstExistingClients(t *testing.T) {

+ 0 - 0
database/dialect.go → internal/database/dialect.go


+ 0 - 0
database/dump_sqlite.go → internal/database/dump_sqlite.go


+ 2 - 2
database/dump_sqlite_test.go → internal/database/dump_sqlite_test.go

@@ -5,8 +5,8 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/xray"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 
 	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"

+ 3 - 3
database/migrate_data.go → internal/database/migrate_data.go

@@ -11,8 +11,8 @@ import (
 	"strings"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/xray"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"
@@ -25,7 +25,7 @@ import (
 // related tests.
 //
 // Important: When adding a new top-level model (like OutboundSubscription),
-// you must add it here **in addition to** the list in database/db.go:initModels().
+// you must add it here **in addition to** the list in internal/database/db.go:initModels().
 // This list is used for:
 //   - Creating the destination schema during cross-DB migration
 //   - Truncating tables

+ 1 - 1
database/migrate_data_test.go → internal/database/migrate_data_test.go

@@ -4,7 +4,7 @@ import (
 	"os"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"

+ 2 - 2
database/model/model.go → internal/database/model/model.go

@@ -9,8 +9,8 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/util/json_util"
-	"github.com/mhsanaei/3x-ui/v3/xray"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 )
 
 // Protocol represents the protocol type for Xray inbounds.

+ 0 - 0
database/model/model_mtproto_test.go → internal/database/model/model_mtproto_test.go


+ 0 - 0
database/model/model_test.go → internal/database/model/model_test.go


+ 0 - 0
database/model/node_client_traffic.go → internal/database/model/node_client_traffic.go


+ 1 - 1
logger/logger.go → internal/logger/logger.go

@@ -9,7 +9,7 @@ import (
 	"runtime"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/config"
+	"github.com/mhsanaei/3x-ui/v3/internal/config"
 	"github.com/op/go-logging"
 
 	"gopkg.in/natefinch/lumberjack.v2"

+ 2 - 2
mtproto/manager.go → internal/mtproto/manager.go

@@ -12,8 +12,8 @@ import (
 	"sync"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 )
 
 // Instance is the desired runtime configuration of one mtproto inbound.

+ 1 - 1
mtproto/manager_test.go → internal/mtproto/manager_test.go

@@ -4,7 +4,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func TestParseMetricLine(t *testing.T) {

+ 0 - 0
mtproto/orphans_linux.go → internal/mtproto/orphans_linux.go


+ 0 - 0
mtproto/orphans_other.go → internal/mtproto/orphans_other.go


+ 2 - 2
mtproto/process.go → internal/mtproto/process.go

@@ -16,8 +16,8 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/config"
-	"github.com/mhsanaei/3x-ui/v3/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/config"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 )
 
 // GetBinaryName returns the mtg binary filename for the current OS and arch,

+ 0 - 0
mtproto/process_other.go → internal/mtproto/process_other.go


+ 1 - 1
mtproto/process_windows.go → internal/mtproto/process_windows.go

@@ -7,7 +7,7 @@ import (
 	"sync"
 	"unsafe"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 	"golang.org/x/sys/windows"
 )
 

+ 1 - 1
sub/build_urls_test.go → internal/sub/build_urls_test.go

@@ -5,7 +5,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
 )
 
 func initSubDB(t *testing.T) {

+ 3 - 3
sub/subClashService.go → internal/sub/clash_service.go

@@ -8,9 +8,9 @@ import (
 	"github.com/goccy/go-json"
 	yaml "github.com/goccy/go-yaml"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 )
 
 type SubClashService struct {

+ 1 - 1
sub/subClashService_test.go → internal/sub/clash_service_test.go

@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func TestEnsureUniqueProxyNames(t *testing.T) {

+ 4 - 4
sub/subController.go → internal/sub/controller.go

@@ -16,8 +16,8 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 )
 
 // writeSubError translates a service-layer result into an HTTP response.
@@ -182,14 +182,14 @@ func (a *SUBController) subs(c *gin.Context) {
 	}
 }
 
-// serveSubPage renders web/dist/subpage.html for the current subscription
+// serveSubPage renders internal/web/dist/subpage.html for the current subscription
 // request. The Vite-built SPA reads window.__SUB_PAGE_DATA__ on mount —
 // we inject that here, along with window.X_UI_BASE_PATH so the
 // page's static asset references resolve correctly when the panel runs
 // behind a URL prefix.
 func (a *SUBController) serveSubPage(c *gin.Context, basePath string, page PageData) {
 	var body []byte
-	if diskBody, diskErr := os.ReadFile("web/dist/subpage.html"); diskErr == nil {
+	if diskBody, diskErr := os.ReadFile("internal/web/dist/subpage.html"); diskErr == nil {
 		body = diskBody
 	} else {
 		readBody, err := distFS.ReadFile("dist/subpage.html")

+ 0 - 0
sub/subController_test.go → internal/sub/controller_test.go


+ 0 - 0
sub/default.json → internal/sub/default.json


+ 2 - 2
sub/dist.go → internal/sub/dist.go

@@ -4,9 +4,9 @@ import "embed"
 
 // distFS holds the Vite-built frontend filesystem, injected from main at
 // startup. The `web` package owns the //go:embed directive (because dist/
-// is at web/dist/), and hands the FS over via SetDistFS so the sub package
+// is at internal/web/dist/), and hands the FS over via SetDistFS so the sub package
 // doesn't import web — that would create an import cycle once any
-// web/controller handler reuses sub's link-building service.
+// internal/web/controller handler reuses sub's link-building service.
 var distFS embed.FS
 
 // SetDistFS installs the embedded frontend filesystem the sub server uses

+ 5 - 5
sub/subJsonService.go → internal/sub/json_service.go

@@ -7,11 +7,11 @@ import (
 	"maps"
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/util/json_util"
-	"github.com/mhsanaei/3x-ui/v3/util/random"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/random"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 )
 
 //go:embed default.json

+ 1 - 1
sub/subJsonService_test.go → internal/sub/json_service_test.go

@@ -4,7 +4,7 @@ import (
 	"encoding/json"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func hasDirectOutOutbound(svc *SubJsonService) bool {

+ 2 - 2
sub/links.go → internal/sub/links.go

@@ -3,8 +3,8 @@ package sub
 import (
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 )
 
 type LinkProvider struct {

+ 0 - 0
sub/links_test.go → internal/sub/links_test.go


+ 9 - 9
sub/subService.go → internal/sub/service.go

@@ -15,13 +15,13 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/goccy/go-json"
 
-	"github.com/mhsanaei/3x-ui/v3/database"
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/util/common"
-	"github.com/mhsanaei/3x-ui/v3/util/random"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/xray"
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/random"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 )
 
 // SubService provides business logic for generating subscription links and managing subscription data.
@@ -991,7 +991,7 @@ func applyVmessTLSParams(stream map[string]any, obj map[string]any) {
 
 // pinnedSha256List extracts tlsSettings.settings.pinnedPeerCertSha256 as a
 // []string. The field is panel-only (stripped before the run-config reaches
-// xray-core via web/service/xray.go) but flows into share links so clients
+// xray-core via internal/web/service/xray.go) but flows into share links so clients
 // can pin the server's certificate hash.
 func pinnedSha256List(tlsClientSettings any) ([]string, bool) {
 	raw, ok := searchKey(tlsClientSettings, "pinnedPeerCertSha256")
@@ -1025,7 +1025,7 @@ func pinnedSha256List(tlsClientSettings any) ([]string, bool) {
 // it. Hysteria2 clients hex-decode pinSHA256 and crash on a base64 value, so
 // each entry is coerced to bare hex here. Anything that is neither a 32-byte
 // hex nor a 32-byte base64 SHA-256 is returned unchanged so unexpected data is
-// not silently dropped. Mirrors decodeCertPin in web/service/node.go.
+// not silently dropped. Mirrors decodeCertPin in internal/web/service/node.go.
 func hysteriaPinHex(pin string) string {
 	pin = strings.TrimSpace(pin)
 	if h := strings.ReplaceAll(pin, ":", ""); len(h) == hex.EncodedLen(sha256.Size) {

+ 1 - 1
sub/subService_test.go → internal/sub/service_test.go

@@ -6,7 +6,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func TestSubscriptionExpiryFromClient(t *testing.T) {

+ 3 - 3
sub/subService_userinfo_test.go → internal/sub/service_userinfo_test.go

@@ -4,9 +4,9 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/mhsanaei/3x-ui/v3/database"
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/xray"
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 )
 
 func TestAggregateTrafficByEmails_FallsBackToClientLimits(t *testing.T) {

+ 8 - 8
sub/sub.go → internal/sub/sub.go

@@ -14,12 +14,12 @@ import (
 	"strings"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/util/common"
-	"github.com/mhsanaei/3x-ui/v3/web/locale"
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/network"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/network"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 
 	"github.com/gin-gonic/gin"
 )
@@ -191,8 +191,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	}
 
 	var assetsFS http.FileSystem
-	if _, err := os.Stat("web/dist/assets"); err == nil {
-		assetsFS = http.FS(os.DirFS("web/dist/assets"))
+	if _, err := os.Stat("internal/web/dist/assets"); err == nil {
+		assetsFS = http.FS(os.DirFS("internal/web/dist/assets"))
 	} else if subFS, err := fs.Sub(distFS, "dist/assets"); err == nil {
 		assetsFS = http.FS(subFS)
 	} else {

+ 1 - 1
util/common/err.go → internal/util/common/err.go

@@ -5,7 +5,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 )
 
 // NewErrorf creates a new error with formatted message.

+ 0 - 0
util/common/format.go → internal/util/common/format.go


+ 0 - 0
util/common/format_test.go → internal/util/common/format_test.go


+ 0 - 0
util/common/multi_error.go → internal/util/common/multi_error.go


+ 0 - 0
util/common/multi_error_test.go → internal/util/common/multi_error_test.go


+ 0 - 0
util/crypto/crypto.go → internal/util/crypto/crypto.go


+ 0 - 0
util/crypto/crypto_test.go → internal/util/crypto/crypto_test.go


+ 0 - 0
util/json_util/json.go → internal/util/json_util/json.go


+ 0 - 0
util/json_util/json_test.go → internal/util/json_util/json_test.go


+ 0 - 0
util/ldap/ldap.go → internal/util/ldap/ldap.go


+ 0 - 0
util/link/outbound.go → internal/util/link/outbound.go


+ 0 - 0
util/link/outbound_test.go → internal/util/link/outbound_test.go


+ 0 - 0
util/netproxy/netproxy.go → internal/util/netproxy/netproxy.go


+ 0 - 0
util/netproxy/netproxy_test.go → internal/util/netproxy/netproxy_test.go


+ 0 - 0
util/netsafe/netsafe.go → internal/util/netsafe/netsafe.go


+ 0 - 0
util/netsafe/netsafe_test.go → internal/util/netsafe/netsafe_test.go


+ 0 - 0
util/random/random.go → internal/util/random/random.go


+ 0 - 0
util/random/random_test.go → internal/util/random/random_test.go


+ 0 - 0
util/reflect_util/reflect.go → internal/util/reflect_util/reflect.go


+ 0 - 0
util/sys/psutil.go → internal/util/sys/psutil.go


+ 0 - 0
util/sys/sys_darwin.go → internal/util/sys/sys_darwin.go


+ 0 - 0
util/sys/sys_linux.go → internal/util/sys/sys_linux.go


+ 0 - 0
util/sys/sys_windows.go → internal/util/sys/sys_windows.go


+ 1 - 1
util/wireguard.go → internal/util/wireguard/wireguard.go

@@ -1,4 +1,4 @@
-package util
+package wireguard
 
 import (
 	"crypto/rand"

+ 11 - 8
web/controller/api.go → internal/web/controller/api.go

@@ -4,9 +4,12 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 
 	"github.com/gin-gonic/gin"
 )
@@ -20,13 +23,13 @@ type APIController struct {
 	settingController     *SettingController
 	xraySettingController *XraySettingController
 	settingService        service.SettingService
-	userService           service.UserService
-	apiTokenService       service.ApiTokenService
-	Tgbot                 service.Tgbot
+	userService           panel.UserService
+	apiTokenService       panel.ApiTokenService
+	Tgbot                 tgbot.Tgbot
 }
 
 // NewAPIController creates a new APIController instance and initializes its routes.
-func NewAPIController(g *gin.RouterGroup, customGeo *service.CustomGeoService) *APIController {
+func NewAPIController(g *gin.RouterGroup, customGeo *integration.CustomGeoService) *APIController {
 	a := &APIController{}
 	a.initRouter(g, customGeo)
 	return a
@@ -57,7 +60,7 @@ func (a *APIController) checkAPIAuth(c *gin.Context) {
 }
 
 // initRouter sets up the API routes for inbounds, server, and other endpoints.
-func (a *APIController) initRouter(g *gin.RouterGroup, customGeo *service.CustomGeoService) {
+func (a *APIController) initRouter(g *gin.RouterGroup, customGeo *integration.CustomGeoService) {
 	// Main API group
 	api := g.Group("/panel/api")
 	api.Use(a.checkAPIAuth)

+ 2 - 2
web/controller/api_docs_test.go → internal/web/controller/api_docs_test.go

@@ -29,7 +29,7 @@ func buildDocSet(t *testing.T) map[string]bool {
 	if err != nil {
 		t.Fatalf("failed to get current dir: %v", err)
 	}
-	endpointsPath := filepath.Join(controllerDir, "..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts")
+	endpointsPath := filepath.Join(controllerDir, "..", "..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts")
 	data, err := os.ReadFile(endpointsPath)
 	if err != nil {
 		t.Fatalf("failed to read endpoints.ts at %s: %v", endpointsPath, err)
@@ -81,7 +81,7 @@ func TestAPIRoutesDocumented(t *testing.T) {
 		switch entry.Name() {
 		case "index.go":
 			basePath = ""
-		case "xui.go":
+		case "spa.go":
 			basePath = "/panel"
 		case "api.go":
 			basePath = "/panel/api"

+ 3 - 3
web/controller/base.go → internal/web/controller/base.go

@@ -5,9 +5,9 @@ package controller
 import (
 	"net/http"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/locale"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 
 	"github.com/gin-gonic/gin"
 )

+ 3 - 3
web/controller/client.go → internal/web/controller/client.go

@@ -7,9 +7,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/websocket"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
 
 	"github.com/gin-gonic/gin"
 )

+ 19 - 19
web/controller/custom_geo.go → internal/web/controller/custom_geo.go

@@ -5,20 +5,20 @@ import (
 	"net/http"
 	"strconv"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/entity"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
 
 	"github.com/gin-gonic/gin"
 )
 
 type CustomGeoController struct {
 	BaseController
-	customGeoService *service.CustomGeoService
+	customGeoService *integration.CustomGeoService
 }
 
-func NewCustomGeoController(g *gin.RouterGroup, customGeo *service.CustomGeoService) *CustomGeoController {
+func NewCustomGeoController(g *gin.RouterGroup, customGeo *integration.CustomGeoService) *CustomGeoController {
 	a := &CustomGeoController{customGeoService: customGeo}
 	a.initRouter(g)
 	return a
@@ -39,33 +39,33 @@ func mapCustomGeoErr(c *gin.Context, err error) error {
 		return nil
 	}
 	switch {
-	case errors.Is(err, service.ErrCustomGeoInvalidType):
+	case errors.Is(err, integration.ErrCustomGeoInvalidType):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrInvalidType"))
-	case errors.Is(err, service.ErrCustomGeoAliasRequired):
+	case errors.Is(err, integration.ErrCustomGeoAliasRequired):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasRequired"))
-	case errors.Is(err, service.ErrCustomGeoAliasPattern):
+	case errors.Is(err, integration.ErrCustomGeoAliasPattern):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasPattern"))
-	case errors.Is(err, service.ErrCustomGeoAliasReserved):
+	case errors.Is(err, integration.ErrCustomGeoAliasReserved):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasReserved"))
-	case errors.Is(err, service.ErrCustomGeoURLRequired):
+	case errors.Is(err, integration.ErrCustomGeoURLRequired):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlRequired"))
-	case errors.Is(err, service.ErrCustomGeoInvalidURL):
+	case errors.Is(err, integration.ErrCustomGeoInvalidURL):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrInvalidUrl"))
-	case errors.Is(err, service.ErrCustomGeoURLScheme):
+	case errors.Is(err, integration.ErrCustomGeoURLScheme):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlScheme"))
-	case errors.Is(err, service.ErrCustomGeoURLHost):
+	case errors.Is(err, integration.ErrCustomGeoURLHost):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlHost"))
-	case errors.Is(err, service.ErrCustomGeoDuplicateAlias):
+	case errors.Is(err, integration.ErrCustomGeoDuplicateAlias):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrDuplicateAlias"))
-	case errors.Is(err, service.ErrCustomGeoNotFound):
+	case errors.Is(err, integration.ErrCustomGeoNotFound):
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrNotFound"))
-	case errors.Is(err, service.ErrCustomGeoDownload):
+	case errors.Is(err, integration.ErrCustomGeoDownload):
 		logger.Warning("custom geo download:", err)
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrDownload"))
-	case errors.Is(err, service.ErrCustomGeoSSRFBlocked):
+	case errors.Is(err, integration.ErrCustomGeoSSRFBlocked):
 		logger.Warning("custom geo SSRF blocked:", err)
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlHost"))
-	case errors.Is(err, service.ErrCustomGeoPathTraversal):
+	case errors.Is(err, integration.ErrCustomGeoPathTraversal):
 		logger.Warning("custom geo path traversal blocked:", err)
 		return errors.New(I18nWeb(c, "pages.index.customGeoErrDownload"))
 	default:

+ 3 - 3
web/controller/dist.go → internal/web/controller/dist.go

@@ -11,9 +11,9 @@ import (
 
 	"github.com/gin-gonic/gin"
 
-	"github.com/mhsanaei/3x-ui/v3/config"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/config"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 )
 
 var distFS embed.FS

+ 0 - 0
web/controller/dist_test.go → internal/web/controller/dist_test.go


+ 2 - 2
web/controller/group.go → internal/web/controller/group.go

@@ -3,8 +3,8 @@ package controller
 import (
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/util/common"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 
 	"github.com/gin-gonic/gin"
 )

+ 5 - 5
web/controller/inbound.go → internal/web/controller/inbound.go

@@ -6,11 +6,11 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
-	"github.com/mhsanaei/3x-ui/v3/web/websocket"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
 
 	"github.com/gin-gonic/gin"
 )

+ 14 - 12
web/controller/index.go → internal/web/controller/index.go

@@ -5,10 +5,12 @@ import (
 	"text/template"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 
 	"github.com/gin-gonic/gin"
 )
@@ -25,8 +27,8 @@ type IndexController struct {
 	BaseController
 
 	settingService service.SettingService
-	userService    service.UserService
-	tgbot          service.Tgbot
+	userService    panel.UserService
+	tgbot          tgbot.Tgbot
 }
 
 // NewIndexController creates a new IndexController and initializes its routes.
@@ -79,11 +81,11 @@ func (a *IndexController) login(c *gin.Context) {
 	if blockedUntil, ok := defaultLoginLimiter.allow(remoteIP, form.Username); !ok {
 		reason := "too many failed attempts"
 		logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", safeUser, remoteIP, reason, blockedUntil.Format(time.RFC3339))
-		a.tgbot.UserLoginNotify(service.LoginAttempt{
+		a.tgbot.UserLoginNotify(tgbot.LoginAttempt{
 			Username: safeUser,
 			IP:       remoteIP,
 			Time:     timeStr,
-			Status:   service.LoginFail,
+			Status:   tgbot.LoginFail,
 			Reason:   reason,
 		})
 		pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
@@ -99,11 +101,11 @@ func (a *IndexController) login(c *gin.Context) {
 		} else {
 			logger.Warningf("failed login: username=%q, IP=%q, reason=%q", safeUser, remoteIP, reason)
 		}
-		a.tgbot.UserLoginNotify(service.LoginAttempt{
+		a.tgbot.UserLoginNotify(tgbot.LoginAttempt{
 			Username: safeUser,
 			IP:       remoteIP,
 			Time:     timeStr,
-			Status:   service.LoginFail,
+			Status:   tgbot.LoginFail,
 			Reason:   reason,
 		})
 		pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
@@ -112,11 +114,11 @@ func (a *IndexController) login(c *gin.Context) {
 
 	defaultLoginLimiter.registerSuccess(remoteIP, form.Username)
 	logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, remoteIP)
-	a.tgbot.UserLoginNotify(service.LoginAttempt{
+	a.tgbot.UserLoginNotify(tgbot.LoginAttempt{
 		Username: safeUser,
 		IP:       remoteIP,
 		Time:     timeStr,
-		Status:   service.LoginSuccess,
+		Status:   tgbot.LoginSuccess,
 	})
 
 	if err := session.SetLoginUser(c, user); err != nil {

+ 0 - 0
web/controller/login_limiter.go → internal/web/controller/login_limiter.go


+ 0 - 0
web/controller/login_limiter_test.go → internal/web/controller/login_limiter_test.go


+ 3 - 3
web/controller/node.go → internal/web/controller/node.go

@@ -8,9 +8,9 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 
 	"github.com/gin-gonic/gin"
 )

+ 9 - 8
web/controller/server.go → internal/web/controller/server.go

@@ -8,13 +8,14 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/database"
-	"github.com/mhsanaei/3x-ui/v3/database/model"
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/entity"
-	"github.com/mhsanaei/3x-ui/v3/web/global"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/websocket"
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/global"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
 
 	"github.com/gin-gonic/gin"
 )
@@ -27,7 +28,7 @@ type ServerController struct {
 
 	serverService      service.ServerService
 	settingService     service.SettingService
-	panelService       service.PanelService
+	panelService       panel.PanelService
 	xrayMetricsService service.XrayMetricsService
 }
 

+ 9 - 8
web/controller/setting.go → internal/web/controller/setting.go

@@ -5,11 +5,12 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/util/crypto"
-	"github.com/mhsanaei/3x-ui/v3/web/entity"
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 
 	"github.com/gin-gonic/gin"
 )
@@ -25,9 +26,9 @@ type updateUserForm struct {
 // SettingController handles settings and user management operations.
 type SettingController struct {
 	settingService  service.SettingService
-	userService     service.UserService
-	panelService    service.PanelService
-	apiTokenService service.ApiTokenService
+	userService     panel.UserService
+	panelService    panel.PanelService
+	apiTokenService panel.ApiTokenService
 }
 
 // NewSettingController creates a new SettingController and initializes its routes.

+ 3 - 3
web/controller/xui.go → internal/web/controller/spa.go

@@ -3,9 +3,9 @@ package controller
 import (
 	"net/http"
 
-	"github.com/mhsanaei/3x-ui/v3/web/entity"
-	"github.com/mhsanaei/3x-ui/v3/web/middleware"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 
 	"github.com/gin-gonic/gin"
 )

+ 3 - 3
web/controller/util.go → internal/web/controller/util.go

@@ -9,9 +9,9 @@ import (
 	"runtime"
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/entity"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
 
 	"github.com/gin-gonic/gin"
 )

+ 0 - 0
web/controller/util_test.go → internal/web/controller/util_test.go


+ 6 - 6
web/controller/websocket.go → internal/web/controller/websocket.go

@@ -6,9 +6,9 @@ import (
 	"net/url"
 	"strings"
 
-	"github.com/mhsanaei/3x-ui/v3/logger"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
-	"github.com/mhsanaei/3x-ui/v3/web/session"
+	"github.com/mhsanaei/3x-ui/v3/internal/logger"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/session"
 
 	"github.com/gin-gonic/gin"
 	ws "github.com/gorilla/websocket"
@@ -49,14 +49,14 @@ func checkSameOrigin(r *http.Request) bool {
 
 // WebSocketController handles the HTTP→WebSocket upgrade for real-time updates.
 // All per-connection lifecycle (pumps, hub registration) lives in
-// service.WebSocketService — this controller is HTTP-layer only.
+// panel.WebSocketService — this controller is HTTP-layer only.
 type WebSocketController struct {
 	BaseController
-	service *service.WebSocketService
+	service *panel.WebSocketService
 }
 
 // NewWebSocketController creates a controller wired to the given service.
-func NewWebSocketController(svc *service.WebSocketService) *WebSocketController {
+func NewWebSocketController(svc *panel.WebSocketService) *WebSocketController {
 	return &WebSocketController{service: svc}
 }
 

+ 7 - 5
web/controller/xray_setting.go → internal/web/controller/xray_setting.go

@@ -6,8 +6,10 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/util/common"
-	"github.com/mhsanaei/3x-ui/v3/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service/outbound"
 
 	"github.com/gin-gonic/gin"
 )
@@ -17,10 +19,10 @@ type XraySettingController struct {
 	XraySettingService          service.XraySettingService
 	SettingService              service.SettingService
 	InboundService              service.InboundService
-	OutboundService             service.OutboundService
+	OutboundService             outbound.OutboundService
 	XrayService                 service.XrayService
-	WarpService                 service.WarpService
-	NordService                 service.NordService
+	WarpService                 integration.WarpService
+	NordService                 integration.NordService
 	OutboundSubscriptionService service.OutboundSubscriptionService
 }
 

+ 1 - 1
web/entity/entity.go → internal/web/entity/entity.go

@@ -8,7 +8,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/mhsanaei/3x-ui/v3/util/common"
+	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
 )
 
 // Msg represents a standard API response message with success status, message text, and optional data object.

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác