├── .docker
├── .data
│ ├── .gitignore
│ └── redis
│ │ └── .gitignore
└── supervisor
│ └── supervisord.conf
├── .dockerignore
├── .editorconfig
├── .env.example
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ └── feature-request.md
└── workflows
│ └── docker-publish.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── Console
│ ├── Commands
│ │ ├── BackupDatabase.php
│ │ ├── CheckCommission.php
│ │ ├── CheckOrder.php
│ │ ├── CheckServer.php
│ │ ├── CheckTicket.php
│ │ ├── ClearUser.php
│ │ ├── ExportV2Log.php
│ │ ├── MigrateFromV2b.php
│ │ ├── ResetLog.php
│ │ ├── ResetPassword.php
│ │ ├── ResetTraffic.php
│ │ ├── ResetUser.php
│ │ ├── SendRemindMail.php
│ │ ├── Test.php
│ │ ├── XboardInstall.php
│ │ ├── XboardRollback.php
│ │ ├── XboardStatistics.php
│ │ └── XboardUpdate.php
│ └── Kernel.php
├── Contracts
│ └── PaymentInterface.php
├── Exceptions
│ ├── ApiException.php
│ ├── BusinessException.php
│ └── Handler.php
├── Helpers
│ ├── ApiResponse.php
│ ├── Functions.php
│ └── ResponseEnum.php
├── Http
│ ├── Controllers
│ │ ├── Controller.php
│ │ ├── V1
│ │ │ ├── Client
│ │ │ │ ├── AppController.php
│ │ │ │ └── ClientController.php
│ │ │ ├── Guest
│ │ │ │ ├── CommController.php
│ │ │ │ ├── PaymentController.php
│ │ │ │ ├── PlanController.php
│ │ │ │ └── TelegramController.php
│ │ │ ├── Passport
│ │ │ │ ├── AuthController.php
│ │ │ │ └── CommController.php
│ │ │ ├── Server
│ │ │ │ ├── ShadowsocksTidalabController.php
│ │ │ │ ├── TrojanTidalabController.php
│ │ │ │ └── UniProxyController.php
│ │ │ └── User
│ │ │ │ ├── CommController.php
│ │ │ │ ├── CouponController.php
│ │ │ │ ├── InviteController.php
│ │ │ │ ├── KnowledgeController.php
│ │ │ │ ├── NoticeController.php
│ │ │ │ ├── OrderController.php
│ │ │ │ ├── PlanController.php
│ │ │ │ ├── ServerController.php
│ │ │ │ ├── StatController.php
│ │ │ │ ├── TelegramController.php
│ │ │ │ ├── TicketController.php
│ │ │ │ └── UserController.php
│ │ └── V2
│ │ │ └── Admin
│ │ │ ├── ConfigController.php
│ │ │ ├── CouponController.php
│ │ │ ├── KnowledgeController.php
│ │ │ ├── NoticeController.php
│ │ │ ├── OrderController.php
│ │ │ ├── PaymentController.php
│ │ │ ├── PlanController.php
│ │ │ ├── PluginController.php
│ │ │ ├── Server
│ │ │ ├── GroupController.php
│ │ │ ├── ManageController.php
│ │ │ └── RouteController.php
│ │ │ ├── StatController.php
│ │ │ ├── SystemController.php
│ │ │ ├── ThemeController.php
│ │ │ ├── TicketController.php
│ │ │ ├── UpdateController.php
│ │ │ └── UserController.php
│ ├── Kernel.php
│ ├── Middleware
│ │ ├── Admin.php
│ │ ├── Authenticate.php
│ │ ├── CheckForMaintenanceMode.php
│ │ ├── Client.php
│ │ ├── EncryptCookies.php
│ │ ├── ForceJson.php
│ │ ├── InitializePlugins.php
│ │ ├── Language.php
│ │ ├── RedirectIfAuthenticated.php
│ │ ├── RequestLog.php
│ │ ├── Server.php
│ │ ├── Staff.php
│ │ ├── TrimStrings.php
│ │ ├── TrustProxies.php
│ │ ├── User.php
│ │ └── VerifyCsrfToken.php
│ ├── Requests
│ │ ├── Admin
│ │ │ ├── ConfigSave.php
│ │ │ ├── CouponGenerate.php
│ │ │ ├── KnowledgeCategorySave.php
│ │ │ ├── KnowledgeCategorySort.php
│ │ │ ├── KnowledgeSave.php
│ │ │ ├── KnowledgeSort.php
│ │ │ ├── MailSend.php
│ │ │ ├── NoticeSave.php
│ │ │ ├── OrderAssign.php
│ │ │ ├── OrderFetch.php
│ │ │ ├── OrderUpdate.php
│ │ │ ├── PlanSave.php
│ │ │ ├── PlanSort.php
│ │ │ ├── PlanUpdate.php
│ │ │ ├── ServerSave.php
│ │ │ ├── UserFetch.php
│ │ │ ├── UserGenerate.php
│ │ │ ├── UserSendMail.php
│ │ │ └── UserUpdate.php
│ │ ├── Passport
│ │ │ ├── AuthForget.php
│ │ │ ├── AuthLogin.php
│ │ │ ├── AuthRegister.php
│ │ │ └── CommSendEmailVerify.php
│ │ ├── Staff
│ │ │ └── UserUpdate.php
│ │ └── User
│ │ │ ├── OrderSave.php
│ │ │ ├── TicketSave.php
│ │ │ ├── TicketWithdraw.php
│ │ │ ├── UserChangePassword.php
│ │ │ ├── UserTransfer.php
│ │ │ └── UserUpdate.php
│ ├── Resources
│ │ ├── ComissionLogResource.php
│ │ ├── CouponResource.php
│ │ ├── InviteCodeResource.php
│ │ ├── MessageResource.php
│ │ ├── NodeResource.php
│ │ ├── OrderResource.php
│ │ ├── PlanResource.php
│ │ ├── TicketResource.php
│ │ └── TrafficLogResource.php
│ └── Routes
│ │ ├── V1
│ │ ├── ClientRoute.php
│ │ ├── GuestRoute.php
│ │ ├── PassportRoute.php
│ │ ├── ServerRoute.php
│ │ └── UserRoute.php
│ │ └── V2
│ │ ├── AdminRoute.php
│ │ ├── PassportRoute.php
│ │ └── UserRoute.php
├── Jobs
│ ├── OrderHandleJob.php
│ ├── SendEmailJob.php
│ ├── SendTelegramJob.php
│ ├── StatServerJob.php
│ ├── StatUserJob.php
│ ├── SyncUserOnlineStatusJob.php
│ └── TrafficFetchJob.php
├── Logging
│ ├── MysqlLogger.php
│ └── MysqlLoggerHandler.php
├── Models
│ ├── CommissionLog.php
│ ├── Coupon.php
│ ├── InviteCode.php
│ ├── Knowledge.php
│ ├── Log.php
│ ├── MailLog.php
│ ├── Notice.php
│ ├── Order.php
│ ├── Payment.php
│ ├── Plan.php
│ ├── Plugin.php
│ ├── Server.php
│ ├── ServerGroup.php
│ ├── ServerLog.php
│ ├── ServerRoute.php
│ ├── ServerStat.php
│ ├── Setting.php
│ ├── Stat.php
│ ├── StatServer.php
│ ├── StatUser.php
│ ├── Ticket.php
│ ├── TicketMessage.php
│ └── User.php
├── Payments
│ ├── AlipayF2F.php
│ ├── BTCPay.php
│ ├── BinancePay.php
│ ├── CoinPayments.php
│ ├── Coinbase.php
│ ├── EPay.php
│ ├── EPayWxpay.php
│ ├── HiiCashPayment.php
│ ├── MGate.php
│ └── PayPal.php
├── Plugins
│ └── Telegram
│ │ ├── Commands
│ │ ├── Bind.php
│ │ ├── GetLatestUrl.php
│ │ ├── ReplyTicket.php
│ │ ├── Traffic.php
│ │ └── UnBind.php
│ │ └── Telegram.php
├── Protocols
│ ├── Clash.php
│ ├── ClashMeta.php
│ ├── General.php
│ ├── Loon.php
│ ├── QuantumultX.php
│ ├── Shadowrocket.php
│ ├── Shadowsocks.php
│ ├── SingBox.php
│ ├── Stash.php
│ ├── Surfboard.php
│ └── Surge.php
├── Providers
│ ├── AuthServiceProvider.php
│ ├── BroadcastServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── HorizonServiceProvider.php
│ ├── OctaneServiceProvider.php
│ ├── PluginServiceProvider.php
│ ├── ProtocolServiceProvider.php
│ ├── RouteServiceProvider.php
│ └── SettingServiceProvider.php
├── Scope
│ └── FilterScope.php
├── Services
│ ├── Auth
│ │ ├── LoginService.php
│ │ ├── MailLinkService.php
│ │ └── RegisterService.php
│ ├── AuthService.php
│ ├── CouponService.php
│ ├── MailService.php
│ ├── OrderService.php
│ ├── PaymentService.php
│ ├── PlanService.php
│ ├── Plugin
│ │ ├── AbstractPlugin.php
│ │ ├── HookManager.php
│ │ ├── InterceptResponseException.php
│ │ ├── PluginConfigService.php
│ │ └── PluginManager.php
│ ├── ServerService.php
│ ├── SettingService.php
│ ├── StatisticalService.php
│ ├── TelegramService.php
│ ├── ThemeService.php
│ ├── TicketService.php
│ ├── UpdateService.php
│ ├── UserOnlineService.php
│ └── UserService.php
├── Support
│ ├── AbstractProtocol.php
│ ├── ProtocolManager.php
│ └── Setting.php
├── Traits
│ └── QueryOperators.php
└── Utils
│ ├── CacheKey.php
│ ├── Dict.php
│ └── Helper.php
├── artisan
├── bootstrap
├── app.php
└── cache
│ └── .gitignore
├── compose.sample.yaml
├── composer.json
├── config
├── app.php
├── auth.php
├── broadcasting.php
├── cache.php
├── cloud_storage.php
├── cors.php
├── database.php
├── debugbar.php
├── filesystems.php
├── hashing.php
├── hidden_features.php
├── horizon.php
├── logging.php
├── mail.php
├── octane.php
├── queue.php
├── sanctum.php
├── scribe.php
├── services.php
├── session.php
├── theme
│ └── .gitignore
└── view.php
├── database
├── .gitignore
├── factories
│ └── UserFactory.php
├── migrations
│ ├── 2019_08_19_000000_create_failed_jobs_table.php
│ ├── 2019_12_14_000001_create_personal_access_tokens_table.php
│ ├── 2023_03_19_000000_create_v2_tables.php
│ ├── 2023_08_14_221234_create_v2_settings_table.php
│ ├── 2023_09_04_190923_add_column_excludes_to_server_table.php
│ ├── 2023_09_06_195956_add_column_ips_to_server_table.php
│ ├── 2023_09_14_013244_add_column_alpn_to_server_hysteria_table.php
│ ├── 2023_09_24_040317_add_column_network_and_network_settings_to_v2_server_trojan.php
│ ├── 2023_09_29_044957_add_column_version_and_is_obfs_to_server_hysteria_table.php
│ ├── 2023_11_19_205026_change_column_value_to_v2_settings_table.php
│ ├── 2023_12_12_212239_add_index_to_v2_user_table.php
│ ├── 2024_03_19_103149_modify_icon_column_to_v2_payment_table.php
│ ├── 2025_01_01_130644_modify_commission_status_in_v2_order_table.php
│ ├── 2025_01_04_optimize_plan_table.php
│ ├── 2025_01_05_131425_create_v2_server_table.php
│ ├── 2025_01_10_152139_add_device_limit_column.php
│ ├── 2025_01_12_190315_add_sort_to_v2_notice_table.php
│ ├── 2025_01_12_200936_modify_commission_status_in_v2_order_table.php
│ ├── 2025_01_13_000000_convert_order_period_fields.php
│ ├── 2025_01_15_000002_add_stat_performance_indexes.php
│ ├── 2025_01_16_142320_add_updated_at_index_to_v2_order_table.php
│ └── 2025_01_18_140511_create_plugins_table.php
└── seeders
│ ├── DatabaseSeeder.php
│ └── OriginV2bMigrationsTableSeeder.php
├── docs
├── en
│ ├── development
│ │ ├── device-limit.md
│ │ └── performance.md
│ ├── installation
│ │ ├── 1panel.md
│ │ ├── aapanel-docker.md
│ │ ├── aapanel.md
│ │ └── docker-compose.md
│ └── migration
│ │ ├── config.md
│ │ ├── v2board-1.7.3.md
│ │ ├── v2board-1.7.4.md
│ │ ├── v2board-dev.md
│ │ └── v2board-wyx2685.md
└── images
│ ├── admin.png
│ └── user.png
├── init.sh
├── library
└── AlipayF2F.php
├── package.json
├── phpstan.neon
├── plugins
└── .gitignore
├── public
├── assets
│ └── admin
│ │ ├── assets
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── vendor.css
│ │ └── vendor.js
│ │ └── locales
│ │ ├── en-US.js
│ │ ├── ko-KR.js
│ │ └── zh-CN.js
├── index.php
├── robots.txt
├── theme
│ └── .gitignore
└── web.config
├── resources
├── js
│ ├── app.js
│ └── bootstrap.js
├── lang
│ ├── en-US.json
│ ├── zh-CN.json
│ └── zh-TW.json
├── rules
│ ├── .gitignore
│ ├── app.clash.yaml
│ ├── default.clash.yaml
│ ├── default.sing-box.json
│ ├── default.surfboard.conf
│ └── default.surge.conf
├── sass
│ └── app.scss
└── views
│ ├── admin.blade.php
│ ├── client
│ └── subscribe.blade.php
│ ├── errors
│ └── 500.blade.php
│ └── mail
│ ├── classic
│ ├── mailLogin.blade.php
│ ├── notify.blade.php
│ ├── remindExpire.blade.php
│ ├── remindTraffic.blade.php
│ └── verify.blade.php
│ └── default
│ ├── mailLogin.blade.php
│ ├── notify.blade.php
│ ├── remindExpire.blade.php
│ ├── remindTraffic.blade.php
│ └── verify.blade.php
├── routes
├── channels.php
├── console.php
└── web.php
├── storage
├── backup
│ └── .gitignore
├── debugbar
│ └── .gitignore
├── framework
│ ├── cache
│ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ └── views
│ │ └── .gitignore
├── logs
│ └── .gitignore
├── theme
│ └── .gitignore
├── tmp
│ └── .gitignore
└── views
│ └── .gitignore
├── theme
├── .gitignore
├── Xboard
│ ├── assets
│ │ ├── images
│ │ │ └── background.svg
│ │ ├── umi.js
│ │ ├── umi.js.br
│ │ └── umi.js.gz
│ ├── config.json
│ └── dashboard.blade.php
└── v2board
│ ├── assets
│ ├── components.async.js
│ ├── components.chunk.css
│ ├── env.example.js
│ ├── i18n
│ │ ├── en-US.js
│ │ ├── fa-IR.js
│ │ ├── ja-JP.js
│ │ ├── ko-KR.js
│ │ ├── vi-VN.js
│ │ ├── zh-CN.js
│ │ └── zh-TW.js
│ ├── images
│ │ └── icon
│ │ │ ├── Clash For Android.png
│ │ │ ├── Clash For Windows.png
│ │ │ ├── Clash Meta For Android.png
│ │ │ ├── Clash Verge.png
│ │ │ ├── ClashX.png
│ │ │ ├── Clashx Meta.png
│ │ │ ├── Hysteria2.svg
│ │ │ ├── NekoBox.png
│ │ │ ├── QuantumultX.png
│ │ │ ├── Shadowrocket.png
│ │ │ ├── Stash.png
│ │ │ ├── Surfboard.png
│ │ │ ├── Surge.png
│ │ │ └── Vless.png
│ ├── static
│ │ ├── Simple-Line-Icons.0cb0b9c5.woff2
│ │ ├── Simple-Line-Icons.78f07e2c.woff
│ │ ├── Simple-Line-Icons.d2285965.ttf
│ │ ├── Simple-Line-Icons.ed67e5a3.svg
│ │ ├── Simple-Line-Icons.f33df365.eot
│ │ ├── fa-brands-400.14c590d1.eot
│ │ ├── fa-brands-400.3e1b2a65.woff2
│ │ ├── fa-brands-400.5e8aa9ea.ttf
│ │ ├── fa-brands-400.91fd86e5.svg
│ │ ├── fa-brands-400.df02c782.woff
│ │ ├── fa-regular-400.285a9d2a.ttf
│ │ ├── fa-regular-400.5623624d.woff
│ │ ├── fa-regular-400.6b5ed912.svg
│ │ ├── fa-regular-400.aa66d0e0.eot
│ │ ├── fa-regular-400.ac21cac3.woff2
│ │ ├── fa-solid-900.3ded831d.woff
│ │ ├── fa-solid-900.42e1fbd2.eot
│ │ ├── fa-solid-900.649208f1.svg
│ │ ├── fa-solid-900.896e20e2.ttf
│ │ └── fa-solid-900.d6d8d5da.woff2
│ ├── theme
│ │ ├── black.css
│ │ ├── darkblue.css
│ │ ├── default.css
│ │ └── green.css
│ ├── umi.css
│ ├── umi.js
│ └── vendors.async.js
│ ├── config.json
│ └── dashboard.blade.php
└── update.sh
/.docker/.data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !redis
--------------------------------------------------------------------------------
/.docker/.data/redis/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/.docker/supervisor/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | nodaemon=true
3 | user=root
4 | logfile=/dev/stdout
5 | logfile_maxbytes=0
6 | pidfil=/www/storage/logs/supervisor/supervisord.pid
7 | loglevel=info
8 |
9 | [program:octane]
10 | process_name=%(program_name)s_%(process_num)02d
11 | command=php /www/artisan octane:start --host=0.0.0.0 --port=7001
12 | autostart=%(ENV_ENABLE_WEB)s
13 | autorestart=true
14 | user=www
15 | redirect_stderr=true
16 | stdout_logfile=/dev/stdout
17 | stdout_logfile_maxbytes=0
18 | stdout_logfile_backups=0
19 | numprocs=1
20 | stopwaitsecs=10
21 | stopsignal=QUIT
22 | stopasgroup=true
23 | killasgroup=true
24 | priority=100
25 |
26 | [program:horizon]
27 | process_name=%(program_name)s_%(process_num)02d
28 | command=php /www/artisan horizon
29 | autostart=%(ENV_ENABLE_HORIZON)s
30 | autorestart=true
31 | user=www
32 | redirect_stderr=true
33 | stdout_logfile=/dev/stdout
34 | stdout_logfile_maxbytes=0
35 | stdout_logfile_backups=0
36 | numprocs=1
37 | stopwaitsecs=3
38 | stopsignal=SIGINT
39 | stopasgroup=true
40 | killasgroup=true
41 | priority=200
42 |
43 | [program:redis]
44 | process_name=%(program_name)s_%(process_num)02d
45 | command=redis-server --dir /data
46 | --dbfilename dump.rdb
47 | --save 900 1
48 | --save 300 10
49 | --save 60 10000
50 | --unixsocket /data/redis.sock
51 | --unixsocketperm 777
52 | autostart=%(ENV_ENABLE_REDIS)s
53 | autorestart=true
54 | user=redis
55 | redirect_stderr=true
56 | stdout_logfile=/dev/stdout
57 | stdout_logfile_maxbytes=0
58 | stdout_logfile_backups=0
59 | numprocs=1
60 | stopwaitsecs=3
61 | stopsignal=TERM
62 | stopasgroup=true
63 | killasgroup=true
64 | priority=300
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /config/v2board.php
3 | /public/hot
4 | /public/storage
5 | /public/env.example.js
6 | /storage/*.key
7 | /vendor
8 | .env
9 | .env.backup
10 | .phpunit.result.cache
11 | .idea
12 | .lock
13 | Homestead.json
14 | Homestead.yaml
15 | npm-debug.log
16 | yarn-error.log
17 | composer.phar
18 | composer.lock
19 | yarn.lock
20 | docker-compose.yml
21 | .DS_Store
22 | /docker
23 | storage/laravels.conf
24 | storage/laravels.pid
25 | storage/laravels-timer-process.pid
26 | /frontend
27 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 4
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.{yml,yaml}]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=XBoard
2 | APP_ENV=local
3 | APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
4 | APP_DEBUG=false
5 | APP_URL=http://localhost
6 |
7 | LOG_CHANNEL=stack
8 |
9 | DB_CONNECTION=mysql
10 | DB_HOST=127.0.0.1
11 | DB_PORT=3306
12 | DB_DATABASE=xboard
13 | DB_USERNAME=root
14 | DB_PASSWORD=
15 |
16 | REDIS_HOST=127.0.0.1
17 | REDIS_PASSWORD=null
18 | REDIS_PORT=6379
19 |
20 | BROADCAST_DRIVER=log
21 | CACHE_DRIVER=redis
22 | QUEUE_CONNECTION=redis
23 |
24 | MAIL_DRIVER=smtp
25 | MAIL_HOST=smtp.mailtrap.io
26 | MAIL_PORT=2525
27 | MAIL_USERNAME=null
28 | MAIL_PASSWORD=null
29 | MAIL_ENCRYPTION=null
30 | MAIL_FROM_ADDRESS=null
31 | MAIL_FROM_NAME=null
32 | MAILGUN_DOMAIN=
33 | MAILGUN_SECRET=
34 |
35 | # google cloud storage
36 | ENABLE_AUTO_BACKUP_AND_UPDATE=false
37 | GOOGLE_CLOUD_KEY_FILE=config/googleCloudStorageKey.json
38 | GOOGLE_CLOUD_STORAGE_BUCKET=
39 |
40 | # Prevent reinstallation
41 | INSTALLED=false
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.css linguist-vendored
3 | *.scss linguist-vendored
4 | *.js linguist-vendored
5 | CHANGELOG.md export-ignore
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 问题反馈 | Bug Report
3 | about: 提交使用过程中遇到的问题 | Report an issue
4 | title: "问题:"
5 | labels: '🐛 bug'
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 |
12 | > ⚠️ 请务必按照模板填写完整信息,没有详细描述的issue可能会被忽略或关闭
13 | > ⚠️ Please follow the template to provide complete information, issues without detailed description may be ignored or closed
14 |
15 | **基本信息 | Basic Info**
16 | ```yaml
17 | XBoard版本 | Version:
18 | 部署方式 | Deployment: [Docker/手动部署]
19 | PHP版本 | Version:
20 | 数据库 | Database:
21 | ```
22 |
23 | **问题描述 | Description**
24 |
25 |
26 |
27 | **复现步骤 | Steps**
28 |
29 | 1.
30 | 2.
31 |
32 | **相关截图 | Screenshots**
33 |
34 |
35 | **日志信息 | Logs**
36 |
37 | ```log
38 | // 粘贴日志内容到这里
39 | ```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ✨ 功能请求 | Feature Request
3 | about: 提交新功能建议或改进意见 | Suggest an idea
4 | title: "建议:"
5 | labels: '✨ enhancement'
6 | assignees: ''
7 | ---
8 |
9 | > ⚠️ 请务必按照模板详细描述你的需求,没有详细描述的issue可能会被忽略或关闭
10 | > ⚠️ Please follow the template to describe your request in detail, issues without detailed description may be ignored or closed
11 |
12 | **需求描述 | Description**
13 |
14 |
15 |
16 | **使用场景 | Use Case**
17 |
18 |
19 |
20 | **功能建议 | Suggestion**
21 |
22 | ```yaml
23 | 功能形式 | Type: [新功能/功能优化/界面改进]
24 | 预期效果 | Expected:
25 | ```
26 |
27 | **补充说明 | Additional**
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /config/v2board.php
3 | /config/googleCloudStorageKey.json
4 | /public/hot
5 | /public/storage
6 | /public/env.example.js
7 | *.user.ini
8 | /storage/*.key
9 | /vendor
10 | .env
11 | .env.backup
12 | .phpunit.result.cache
13 | .idea
14 | .lock
15 | Homestead.json
16 | Homestead.yaml
17 | npm-debug.log
18 | yarn-error.log
19 | composer.phar
20 | composer.lock
21 | yarn.lock
22 | docker-compose.yml
23 | .DS_Store
24 | /docker
25 | storage/laravels.conf
26 | storage/laravels.pid
27 | storage/update_pending
28 | storage/laravels-timer-process.pid
29 | cli-php.ini
30 | frontend
31 | docker-compose.yaml
32 | bun.lockb
33 | compose.yaml
34 | .scribe
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM phpswoole/swoole:php8.2-alpine
2 |
3 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
4 |
5 | # Install PHP extensions one by one with lower optimization level for ARM64 compatibility
6 | RUN CFLAGS="-O0" install-php-extensions pcntl && \
7 | CFLAGS="-O0 -g0" install-php-extensions bcmath && \
8 | install-php-extensions zip && \
9 | install-php-extensions redis && \
10 | apk --no-cache add shadow sqlite mysql-client mysql-dev mariadb-connector-c git patch supervisor redis && \
11 | addgroup -S -g 1000 www && adduser -S -G www -u 1000 www && \
12 | (getent group redis || addgroup -S redis) && \
13 | (getent passwd redis || adduser -S -G redis -H -h /data redis)
14 |
15 | WORKDIR /www
16 |
17 | COPY .docker /
18 |
19 | # Add build arguments
20 | ARG CACHEBUST
21 | ARG REPO_URL
22 | ARG BRANCH_NAME
23 |
24 | RUN echo "Attempting to clone branch: ${BRANCH_NAME} from ${REPO_URL} with CACHEBUST: ${CACHEBUST}" && \
25 | rm -rf ./* && \
26 | rm -rf .git && \
27 | git config --global --add safe.directory /www && \
28 | git clone --depth 1 --branch ${BRANCH_NAME} ${REPO_URL} .
29 |
30 | COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
31 |
32 | RUN composer install --no-cache --no-dev \
33 | && php artisan storage:link \
34 | && chown -R www:www /www \
35 | && chmod -R 775 /www \
36 | && mkdir -p /data \
37 | && chown redis:redis /data
38 |
39 | ENV ENABLE_WEB=true \
40 | ENABLE_HORIZON=true \
41 | ENABLE_REDIS=false
42 |
43 | EXPOSE 7001
44 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Tokumeikoi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/Console/Commands/CheckOrder.php:
--------------------------------------------------------------------------------
1 | orderBy('created_at', 'ASC')
49 | ->get();
50 | foreach ($orders as $order) {
51 | OrderHandleJob::dispatch($order->trade_no);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/Console/Commands/CheckServer.php:
--------------------------------------------------------------------------------
1 | checkOffline();
45 | }
46 |
47 | private function checkOffline()
48 | {
49 | $servers = ServerService::getAllServers();
50 | foreach ($servers as $server) {
51 | if ($server['parent_id']) continue;
52 | if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) {
53 | $telegramService = new TelegramService();
54 | $message = sprintf(
55 | "节点掉线通知\r\n----\r\n节点名称:%s\r\n节点地址:%s\r\n",
56 | $server['name'],
57 | $server['host']
58 | );
59 | $telegramService->sendMessageWithAdmin($message);
60 | Cache::forget(CacheKey::get(sprintf("SERVER_%s_LAST_CHECK_AT", strtoupper($server['type'])), $server->id));
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/Console/Commands/CheckTicket.php:
--------------------------------------------------------------------------------
1 | where('updated_at', '<=', time() - 24 * 3600)
44 | ->where('reply_status', 0)
45 | ->get();
46 | foreach ($tickets as $ticket) {
47 | if ($ticket->user_id === $ticket->last_reply_user_id) continue;
48 | $ticket->status = Ticket::STATUS_CLOSED;
49 | $ticket->save();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/Console/Commands/ClearUser.php:
--------------------------------------------------------------------------------
1 | where('transfer_enable', 0)
44 | ->where('expired_at', 0)
45 | ->where('last_login_at', NULL);
46 | $count = $builder->count();
47 | if ($builder->delete()) {
48 | $this->info("已删除{$count}位没有任何数据的用户");
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/Console/Commands/ExportV2Log.php:
--------------------------------------------------------------------------------
1 | argument('days');
22 | $date = Carbon::now()->subDays((float) $days)->startOfDay();
23 |
24 | $logs = DB::table('v2_log')
25 | ->where('created_at', '>=', $date->timestamp)
26 | ->get();
27 |
28 | $fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv";
29 | $handle = fopen(storage_path("logs/$fileName"), 'w');
30 |
31 | // 根据您的表结构
32 | fputcsv($handle, ['Level', 'ID', 'Title', 'Host', 'URI', 'Method', 'Data', 'IP', 'Context', 'Created At', 'Updated At']);
33 |
34 | foreach ($logs as $log) {
35 | fputcsv($handle, [
36 | $log->level,
37 | $log->id,
38 | $log->title,
39 | $log->host,
40 | $log->uri,
41 | $log->method,
42 | $log->data,
43 | $log->ip,
44 | $log->context,
45 | Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
46 | Carbon::createFromTimestamp($log->updated_at)->toDateTimeString()
47 | ]);
48 | }
49 |
50 | fclose($handle);
51 | $this->info("日志成功导出到: " . storage_path("logs/$fileName"));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/Console/Commands/ResetLog.php:
--------------------------------------------------------------------------------
1 | delete();
49 | StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
50 | Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/Console/Commands/ResetPassword.php:
--------------------------------------------------------------------------------
1 | argument('password') ;
46 | $user = User::where('email', $this->argument('email'))->first();
47 | if (!$user) abort(500, '邮箱不存在');
48 | $password = $password ?? Helper::guid(false);
49 | $user->password = password_hash($password, PASSWORD_DEFAULT);
50 | $user->password_algo = null;
51 | if (!$user->save()) abort(500, '重置失败');
52 | $this->info("!!!重置成功!!!");
53 | $this->info("新密码为:{$password},请尽快修改密码。");
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/Console/Commands/ResetUser.php:
--------------------------------------------------------------------------------
1 | confirm("确定要重置所有用户安全信息吗?")) {
46 | return;
47 | }
48 | ini_set('memory_limit', -1);
49 | $users = User::all();
50 | foreach ($users as $user)
51 | {
52 | $user->token = Helper::guid();
53 | $user->uuid = Helper::guid(true);
54 | $user->save();
55 | $this->info("已重置用户{$user->email}的安全信息");
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/Console/Commands/Test.php:
--------------------------------------------------------------------------------
1 | info('正在回滚数据库请稍等...');
42 | \Artisan::call("migrate:rollback");
43 | $this->info(\Artisan::output());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Console/Commands/XboardUpdate.php:
--------------------------------------------------------------------------------
1 | info('正在导入数据库请稍等...');
42 | \Artisan::call("migrate");
43 | $this->info(\Artisan::output());
44 | \Artisan::call('horizon:terminate');
45 | $this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/Contracts/PaymentInterface.php:
--------------------------------------------------------------------------------
1 | message = $message;
16 | $this->code = $code;
17 | $this->errors = $errors;
18 | }
19 | public function errors(){
20 | return $this->errors;
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app/Exceptions/BusinessException.php:
--------------------------------------------------------------------------------
1 | toArray();
16 | }
17 |
18 | if (is_array($key)) {
19 | App::make(Setting::class)->save($key);
20 | return '';
21 | }
22 | $default = config('v2board.'. $key) ?? $default;
23 | return App::make(Setting::class)->get($key) ?? $default ;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | admin_setting('tos_url'),
15 | 'is_email_verify' => (int)admin_setting('email_verify', 0) ? 1 : 0,
16 | 'is_invite_force' => (int)admin_setting('invite_force', 0) ? 1 : 0,
17 | 'email_whitelist_suffix' => (int)admin_setting('email_whitelist_enable', 0)
18 | ? $this->getEmailSuffix()
19 | : 0,
20 | 'is_recaptcha' => (int)admin_setting('recaptcha_enable', 0) ? 1 : 0,
21 | 'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
22 | 'app_description' => admin_setting('app_description'),
23 | 'app_url' => admin_setting('app_url'),
24 | 'logo' => admin_setting('logo'),
25 | ];
26 | return $this->success($data);
27 | }
28 |
29 | private function getEmailSuffix()
30 | {
31 | $suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
32 | if (!is_array($suffix)) {
33 | return preg_split('/,/', $suffix);
34 | }
35 | return $suffix;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/Guest/PlanController.php:
--------------------------------------------------------------------------------
1 | planService = $planService;
19 | }
20 | public function fetch(Request $request)
21 | {
22 | $plan = $this->planService->getAvailablePlans();
23 | return $this->success(PlanResource::collection($plan));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/CommController.php:
--------------------------------------------------------------------------------
1 | (int)admin_setting('telegram_bot_enable', 0),
17 | 'telegram_discuss_link' => admin_setting('telegram_discuss_link'),
18 | 'stripe_pk' => admin_setting('stripe_pk_live'),
19 | 'withdraw_methods' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
20 | 'withdraw_close' => (int)admin_setting('withdraw_close_enable', 0),
21 | 'currency' => admin_setting('currency', 'CNY'),
22 | 'currency_symbol' => admin_setting('currency_symbol', '¥'),
23 | 'commission_distribution_enable' => (int)admin_setting('commission_distribution_enable', 0),
24 | 'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
25 | 'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
26 | 'commission_distribution_l3' => admin_setting('commission_distribution_l3')
27 | ];
28 | return $this->success($data);
29 | }
30 |
31 | public function getStripePublicKey(Request $request)
32 | {
33 | $payment = Payment::where('id', $request->input('id'))
34 | ->where('payment', 'StripeCredit')
35 | ->first();
36 | if (!$payment) throw new ApiException('payment is not found');
37 | return $this->success($payment->config['stripe_pk_live']);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/CouponController.php:
--------------------------------------------------------------------------------
1 | input('code'))) {
16 | return $this->fail([422, __('Coupon cannot be empty')]);
17 | }
18 | $couponService = new CouponService($request->input('code'));
19 | $couponService->setPlanId($request->input('plan_id'));
20 | $couponService->setUserId($request->user()->id);
21 | $couponService->setPeriod($request->input('period'));
22 | $couponService->check();
23 | return $this->success(CouponResource::make($couponService->getCoupon()));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/NoticeController.php:
--------------------------------------------------------------------------------
1 | input('current') ? $request->input('current') : 1;
14 | $pageSize = 5;
15 | $model = Notice::orderBy('sort', 'ASC')
16 | ->where('show', true);
17 | $total = $model->count();
18 | $res = $model->forPage($current, $pageSize)
19 | ->get();
20 | return response([
21 | 'data' => $res,
22 | 'total' => $total
23 | ]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/PlanController.php:
--------------------------------------------------------------------------------
1 | planService = $planService;
20 | }
21 | public function fetch(Request $request)
22 | {
23 | $user = User::find($request->user()->id);
24 | if ($request->input('id')) {
25 | $plan = Plan::where('id', $request->input('id'))->first();
26 | if (!$plan) {
27 | return $this->fail([400, __('Subscription plan does not exist')]);
28 | }
29 | if (!$this->planService->isPlanAvailableForUser($plan, $user)) {
30 | return $this->fail([400, __('Subscription plan does not exist')]);
31 | }
32 | return $this->success(PlanResource::make($plan));
33 | }
34 |
35 | $plans = $this->planService->getAvailablePlans();
36 | return $this->success(PlanResource::collection($plans));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/ServerController.php:
--------------------------------------------------------------------------------
1 | user()->id);
17 | $servers = [];
18 | $userService = new UserService();
19 | if ($userService->isAvailable($user)) {
20 | $servers = ServerService::getAvailableServers($user);
21 | }
22 | $eTag = sha1(json_encode(array_column($servers, 'cache_key')));
23 | if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) {
24 | return response(null,304);
25 | }
26 | $data = NodeResource::collection($servers);
27 | return response([
28 | 'data' => $data
29 | ])->header('ETag', "\"{$eTag}\"");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/StatController.php:
--------------------------------------------------------------------------------
1 | startOfMonth()->timestamp;
17 | $records = StatUser::query()
18 | ->where('user_id', $request->user()->id)
19 | ->where('record_at', '>=', $startDate)
20 | ->orderBy('record_at', 'DESC')
21 | ->get();
22 |
23 | $data = TrafficLogResource::collection(collect($records));
24 | return $this->success($data);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V1/User/TelegramController.php:
--------------------------------------------------------------------------------
1 | getMe();
16 | $data = [
17 | 'username' => $response->result->username
18 | ];
19 | return $this->success($data);
20 | }
21 |
22 | public function unbind(Request $request)
23 | {
24 | $user = User::where('user_id', $request->user()->id)->first();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Controllers/V2/Admin/UpdateController.php:
--------------------------------------------------------------------------------
1 | updateService = $updateService;
16 | }
17 |
18 | public function checkUpdate()
19 | {
20 | return $this->success($this->updateService->checkForUpdates());
21 | }
22 |
23 | public function executeUpdate()
24 | {
25 | $result = $this->updateService->executeUpdate();
26 | return $result['success'] ? $this->success($result) : $this->fail([500, $result['message']]);
27 | }
28 | }
--------------------------------------------------------------------------------
/app/Http/Middleware/Admin.php:
--------------------------------------------------------------------------------
1 | user();
23 |
24 | if (!$user || !$user->is_admin) {
25 | return response()->json(['message' => 'Unauthorized'], 403);
26 | }
27 |
28 | return $next($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Authenticate.php:
--------------------------------------------------------------------------------
1 | expectsJson() ? null : null;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/CheckForMaintenanceMode.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | protected $except = [
14 | // 示例:
15 | // '/api/health-check',
16 | // '/status'
17 | ];
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Client.php:
--------------------------------------------------------------------------------
1 | input('token', $request->route('token'));
22 | if (empty($token)) {
23 | throw new ApiException('token is null',403);
24 | }
25 | $user = User::where('token', $token)->first();
26 | if (!$user) {
27 | throw new ApiException('token is error',403);
28 | }
29 |
30 | Auth::setUser($user);
31 | return $next($request);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | protected $except = [
14 | //
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Http/Middleware/ForceJson.php:
--------------------------------------------------------------------------------
1 | headers->set('accept', 'application/json');
20 | return $next($request);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Middleware/InitializePlugins.php:
--------------------------------------------------------------------------------
1 | pluginManager = $pluginManager;
18 | }
19 |
20 | public function handle(Request $request, Closure $next)
21 | {
22 | try {
23 | $plugins = Plugin::query()
24 | ->where('is_enabled', true)
25 | ->get();
26 |
27 | foreach ($plugins as $plugin) {
28 | $this->pluginManager->enable($plugin->code);
29 | }
30 | } catch (\Exception $e) {
31 | Log::error('Failed to load plugins: ' . $e->getMessage());
32 | }
33 |
34 | return $next($request);
35 | }
36 | }
--------------------------------------------------------------------------------
/app/Http/Middleware/Language.php:
--------------------------------------------------------------------------------
1 | header('content-language')) {
13 | App::setLocale($request->header('content-language'));
14 | }
15 | return $next($request);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
21 | return redirect('/home');
22 | }
23 |
24 | return $next($request);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RequestLog.php:
--------------------------------------------------------------------------------
1 | method() === 'POST') {
19 | $path = $request->path();
20 | info("POST {$path}");
21 | };
22 | return $next($request);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Server.php:
--------------------------------------------------------------------------------
1 | validateRequest($request);
17 |
18 | $serverInfo = ServerService::getServer(
19 | $request->input('node_id'),
20 | $request->input('node_type') ?? $nodeType
21 | );
22 | if (!$serverInfo) {
23 | throw new ApiException('Server does not exist');
24 | }
25 |
26 | $request->attributes->set('node_info', $serverInfo);
27 | return $next($request);
28 | }
29 |
30 | private function validateRequest(Request $request): void
31 | {
32 | $request->validate([
33 | 'token' => [
34 | 'string',
35 | 'required',
36 | function ($attribute, $value, $fail) {
37 | if ($value !== admin_setting('server_token')) {
38 | $fail("Invalid {$attribute}");
39 | }
40 | },
41 | ],
42 | 'node_id' => 'required',
43 | 'node_type' => [
44 | 'nullable',
45 | function ($attribute, $value, $fail) use ($request) {
46 | if (!ServerModel::isValidType($value)) {
47 | $fail("Invalid node type specified");
48 | return;
49 | }
50 | $request->merge([$attribute => ServerModel::normalizeType($value)]);
51 | },
52 | ]
53 | ]);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Staff.php:
--------------------------------------------------------------------------------
1 | input('auth_data') ?? $request->header('authorization');
21 | if (!$authorization) throw new ApiException( '未登录或登陆已过期', 403);
22 |
23 | $user = AuthService::decryptAuthData($authorization);
24 | if (!$user || !$user['is_staff']) throw new ApiException('未登录或登陆已过期', 403);
25 | $request->merge([
26 | 'user' => $user
27 | ]);
28 | return $next($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrimStrings.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | protected $except = [
14 | 'password',
15 | 'password_confirmation',
16 | 'encrypted_data',
17 | 'signature'
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustProxies.php:
--------------------------------------------------------------------------------
1 | |string|null
13 | */
14 | protected $proxies = [
15 | "173.245.48.0/20",
16 | "103.21.244.0/22",
17 | "103.22.200.0/22",
18 | "103.31.4.0/22",
19 | "141.101.64.0/18",
20 | "108.162.192.0/18",
21 | "190.93.240.0/20",
22 | "188.114.96.0/20",
23 | "197.234.240.0/22",
24 | "198.41.128.0/17",
25 | "162.158.0.0/15",
26 | "104.16.0.0/13",
27 | "104.24.0.0/14",
28 | "172.64.0.0/13",
29 | "131.0.72.0/22",
30 | "10.0.0.0/8",
31 | "172.16.0.0/12",
32 | "192.168.0.0/16",
33 | "169.254.0.0/16",
34 | "127.0.0.0/8",
35 | ];
36 |
37 | /**
38 | * 代理头映射
39 | * @var int
40 | */
41 | protected $headers =
42 | Request::HEADER_X_FORWARDED_FOR |
43 | Request::HEADER_X_FORWARDED_HOST |
44 | Request::HEADER_X_FORWARDED_PORT |
45 | Request::HEADER_X_FORWARDED_PROTO |
46 | Request::HEADER_X_FORWARDED_AWS_ELB;
47 | }
48 |
--------------------------------------------------------------------------------
/app/Http/Middleware/User.php:
--------------------------------------------------------------------------------
1 | check()) {
23 | throw new ApiException('未登录或登陆已过期', 403);
24 | }
25 | return $next($request);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Middleware/VerifyCsrfToken.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | protected $except = [
20 | //
21 | ];
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/CouponGenerate.php:
--------------------------------------------------------------------------------
1 | 'nullable|integer|max:500',
18 | 'name' => 'required',
19 | 'type' => 'required|in:1,2',
20 | 'value' => 'required|integer',
21 | 'started_at' => 'required|integer',
22 | 'ended_at' => 'required|integer',
23 | 'limit_use' => 'nullable|integer',
24 | 'limit_use_with_user' => 'nullable|integer',
25 | 'limit_plan_ids' => 'nullable|array',
26 | 'limit_period' => 'nullable|array',
27 | 'code' => ''
28 | ];
29 | }
30 |
31 | public function messages()
32 | {
33 | return [
34 | 'generate_count.integer' => '生成数量必须为数字',
35 | 'generate_count.max' => '生成数量最大为500个',
36 | 'name.required' => '名称不能为空',
37 | 'type.required' => '类型不能为空',
38 | 'type.in' => '类型格式有误',
39 | 'value.required' => '金额或比例不能为空',
40 | 'value.integer' => '金额或比例格式有误',
41 | 'started_at.required' => '开始时间不能为空',
42 | 'started_at.integer' => '开始时间格式有误',
43 | 'ended_at.required' => '结束时间不能为空',
44 | 'ended_at.integer' => '结束时间格式有误',
45 | 'limit_use.integer' => '最大使用次数格式有误',
46 | 'limit_use_with_user.integer' => '限制用户使用次数格式有误',
47 | 'limit_plan_ids.array' => '指定订阅格式有误',
48 | 'limit_period.array' => '指定周期格式有误'
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/KnowledgeCategorySave.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'language' => 'required'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'name.required' => '分类名称不能为空',
26 | 'language.required' => '分类语言不能为空'
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/KnowledgeCategorySort.php:
--------------------------------------------------------------------------------
1 | 'required|array'
18 | ];
19 | }
20 |
21 | public function messages()
22 | {
23 | return [
24 | 'knowledge_category_ids.required' => '分类不能为空',
25 | 'knowledge_category_ids.array' => '分类格式有误'
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/KnowledgeSave.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'language' => 'required',
19 | 'title' => 'required',
20 | 'body' => 'required'
21 | ];
22 | }
23 |
24 | public function messages()
25 | {
26 | return [
27 | 'title.required' => '标题不能为空',
28 | 'category.required' => '分类不能为空',
29 | 'body.required' => '内容不能为空',
30 | 'language.required' => '语言不能为空'
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/KnowledgeSort.php:
--------------------------------------------------------------------------------
1 | 'required|array'
18 | ];
19 | }
20 |
21 | public function messages()
22 | {
23 | return [
24 | 'knowledge_ids.required' => '知识ID不能为空',
25 | 'knowledge_ids.array' => '知识ID格式有误'
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/MailSend.php:
--------------------------------------------------------------------------------
1 | 'required|in:1,2,3,4',
18 | 'subject' => 'required',
19 | 'content' => 'required',
20 | 'receiver' => 'array'
21 | ];
22 | }
23 |
24 | public function messages()
25 | {
26 | return [
27 | 'type.required' => '发送类型不能为空',
28 | 'type.in' => '发送类型格式有误',
29 | 'subject.required' => '主题不能为空',
30 | 'content.required' => '内容不能为空',
31 | 'receiver.array' => '收件人格式有误'
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/NoticeSave.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'content' => 'required',
19 | 'img_url' => 'nullable|url',
20 | 'tags' => 'nullable|array'
21 | ];
22 | }
23 |
24 | public function messages()
25 | {
26 | return [
27 | 'title.required' => '标题不能为空',
28 | 'content.required' => '内容不能为空',
29 | 'img_url.url' => '图片URL格式不正确',
30 | 'tags.array' => '标签格式不正确'
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/OrderAssign.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'email' => 'required',
19 | 'total_amount' => 'required',
20 | 'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
21 | ];
22 | }
23 |
24 | public function messages()
25 | {
26 | return [
27 | 'plan_id.required' => '订阅不能为空',
28 | 'email.required' => '邮箱不能为空',
29 | 'total_amount.required' => '支付金额不能为空',
30 | 'period.required' => '订阅周期不能为空',
31 | 'period.in' => '订阅周期格式有误'
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/OrderFetch.php:
--------------------------------------------------------------------------------
1 | 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id,callback_no,commission_balance',
18 | 'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
19 | 'filter.*.value' => ''
20 | ];
21 | }
22 |
23 | public function messages()
24 | {
25 | return [
26 | 'filter.*.key.required' => '过滤键不能为空',
27 | 'filter.*.key.in' => '过滤键参数有误',
28 | 'filter.*.condition.required' => '过滤条件不能为空',
29 | 'filter.*.condition.in' => '过滤条件参数有误',
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/OrderUpdate.php:
--------------------------------------------------------------------------------
1 | 'in:0,1,2,3',
18 | 'commission_status' => 'in:0,1,3'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'status.in' => '销售状态格式不正确',
26 | 'commission_status.in' => '佣金状态格式不正确'
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/PlanSort.php:
--------------------------------------------------------------------------------
1 | 'required|array'
18 | ];
19 | }
20 |
21 | public function messages()
22 | {
23 | return [
24 | 'plan_ids.required' => '订阅计划ID不能为空',
25 | 'plan_ids.array' => '订阅计划ID格式有误'
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/PlanUpdate.php:
--------------------------------------------------------------------------------
1 | 'in:0,1',
18 | 'renew' => 'in:0,1'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'show.in' => '销售状态格式不正确',
26 | 'renew.in' => '续费状态格式不正确'
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/UserFetch.php:
--------------------------------------------------------------------------------
1 | 'required|in:id,email,transfer_enable,d,expired_at,uuid,token,invite_by_email,invite_user_id,plan_id,banned,remarks,is_admin',
18 | 'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
19 | 'filter.*.value' => 'required'
20 | ];
21 | }
22 |
23 | public function messages()
24 | {
25 | return [
26 | 'filter.*.key.required' => '过滤键不能为空',
27 | 'filter.*.key.in' => '过滤键参数有误',
28 | 'filter.*.condition.required' => '过滤条件不能为空',
29 | 'filter.*.condition.in' => '过滤条件参数有误',
30 | 'filter.*.value.required' => '过滤值不能为空'
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/UserGenerate.php:
--------------------------------------------------------------------------------
1 | 'nullable|integer|max:500',
18 | 'expired_at' => 'nullable|integer',
19 | 'plan_id' => 'nullable|integer',
20 | 'email_prefix' => 'nullable',
21 | 'email_suffix' => 'required',
22 | 'password' => 'nullable'
23 | ];
24 | }
25 |
26 | public function messages()
27 | {
28 | return [
29 | 'generate_count.integer' => '生成数量必须为数字',
30 | 'generate_count.max' => '生成数量最大为500个'
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/UserSendMail.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'content' => 'required',
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'subject.required' => '主题不能为空',
26 | 'content.required' => '发送内容不能为空'
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/Passport/AuthForget.php:
--------------------------------------------------------------------------------
1 | 'required|email:strict',
18 | 'password' => 'required|min:8',
19 | 'email_code' => 'required'
20 | ];
21 | }
22 |
23 | public function messages()
24 | {
25 | return [
26 | 'email.required' => __('Email can not be empty'),
27 | 'email.email' => __('Email format is incorrect'),
28 | 'password.required' => __('Password can not be empty'),
29 | 'password.min' => __('Password must be greater than 8 digits'),
30 | 'email_code.required' => __('Email verification code cannot be empty')
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Http/Requests/Passport/AuthLogin.php:
--------------------------------------------------------------------------------
1 | 'required|email:strict',
18 | 'password' => 'required|min:8'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'email.required' => __('Email can not be empty'),
26 | 'email.email' => __('Email format is incorrect'),
27 | 'password.required' => __('Password can not be empty'),
28 | 'password.min' => __('Password must be greater than 8 digits')
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Requests/Passport/AuthRegister.php:
--------------------------------------------------------------------------------
1 | 'required|email:strict',
18 | 'password' => 'required|min:8'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'email.required' => __('Email can not be empty'),
26 | 'email.email' => __('Email format is incorrect'),
27 | 'password.required' => __('Password can not be empty'),
28 | 'password.min' => __('Password must be greater than 8 digits')
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Requests/Passport/CommSendEmailVerify.php:
--------------------------------------------------------------------------------
1 | 'required|email:strict'
18 | ];
19 | }
20 |
21 | public function messages()
22 | {
23 | return [
24 | 'email.required' => __('Email can not be empty'),
25 | 'email.email' => __('Email format is incorrect')
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/Staff/UserUpdate.php:
--------------------------------------------------------------------------------
1 | 'required|email:strict',
18 | 'password' => 'nullable',
19 | 'transfer_enable' => 'numeric',
20 | 'expired_at' => 'nullable|integer',
21 | 'banned' => 'required|in:0,1',
22 | 'plan_id' => 'nullable|integer',
23 | 'commission_rate' => 'nullable|integer|min:0|max:100',
24 | 'discount' => 'nullable|integer|min:0|max:100',
25 | 'u' => 'integer',
26 | 'd' => 'integer',
27 | 'balance' => 'integer',
28 | 'commission_balance' => 'integer'
29 | ];
30 | }
31 |
32 | public function messages()
33 | {
34 | return [
35 | 'email.required' => '邮箱不能为空',
36 | 'email.email' => '邮箱格式不正确',
37 | 'transfer_enable.numeric' => '流量格式不正确',
38 | 'expired_at.integer' => '到期时间格式不正确',
39 | 'banned.required' => '是否封禁不能为空',
40 | 'banned.in' => '是否封禁格式不正确',
41 | 'plan_id.integer' => '订阅计划格式不正确',
42 | 'commission_rate.integer' => '推荐返利比例格式不正确',
43 | 'commission_rate.nullable' => '推荐返利比例格式不正确',
44 | 'commission_rate.min' => '推荐返利比例最小为0',
45 | 'commission_rate.max' => '推荐返利比例最大为100',
46 | 'discount.integer' => '专属折扣比例格式不正确',
47 | 'discount.nullable' => '专属折扣比例格式不正确',
48 | 'discount.min' => '专属折扣比例最小为0',
49 | 'discount.max' => '专属折扣比例最大为100',
50 | 'u.integer' => '上行流量格式不正确',
51 | 'd.integer' => '下行流量格式不正确',
52 | 'balance.integer' => '余额格式不正确',
53 | 'commission_balance.integer' => '佣金格式不正确'
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/Http/Requests/User/OrderSave.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'plan_id.required' => __('Plan ID cannot be empty'),
26 | 'period.required' => __('Plan period cannot be empty'),
27 | 'period.in' => __('Wrong plan period')
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/User/TicketSave.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'level' => 'required|in:0,1,2',
19 | 'message' => 'required'
20 | ];
21 | }
22 |
23 | public function messages()
24 | {
25 | return [
26 | 'subject.required' => __('Ticket subject cannot be empty'),
27 | 'level.required' => __('Ticket level cannot be empty'),
28 | 'level.in' => __('Incorrect ticket level format'),
29 | 'message.required' => __('Message cannot be empty')
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Http/Requests/User/TicketWithdraw.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'withdraw_account' => 'required'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'withdraw_method.required' => __('The withdrawal method cannot be empty'),
26 | 'withdraw_account.required' => __('The withdrawal account cannot be empty')
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/User/UserChangePassword.php:
--------------------------------------------------------------------------------
1 | 'required',
18 | 'new_password' => 'required|min:8'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'old_password.required' => __('Old password cannot be empty'),
26 | 'new_password.required' => __('New password cannot be empty'),
27 | 'new_password.min' => __('Password must be greater than 8 digits')
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/Http/Requests/User/UserTransfer.php:
--------------------------------------------------------------------------------
1 | 'required|integer|min:1'
18 | ];
19 | }
20 |
21 | public function messages()
22 | {
23 | return [
24 | 'transfer_amount.required' => __('The transfer amount cannot be empty'),
25 | 'transfer_amount.integer' => __('The transfer amount parameter is wrong'),
26 | 'transfer_amount.min' => __('The transfer amount parameter is wrong')
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Requests/User/UserUpdate.php:
--------------------------------------------------------------------------------
1 | 'in:0,1',
18 | 'remind_traffic' => 'in:0,1'
19 | ];
20 | }
21 |
22 | public function messages()
23 | {
24 | return [
25 | 'show.in' => __('Incorrect format of expiration reminder'),
26 | 'renew.in' => __('Incorrect traffic alert format')
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Resources/ComissionLogResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | return [
18 | "id"=> $this['id'],
19 | "order_amount" => $this['order_amount'],
20 | "trade_no" => $this['trade_no'],
21 | "get_amount" => $this['get_amount'],
22 | "created_at" => $this['created_at']
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Resources/CouponResource.php:
--------------------------------------------------------------------------------
1 | 转换后的数组
23 | */
24 | public function toArray(Request $request): array
25 | {
26 | return [
27 | ...$this->resource->toArray(),
28 | 'limit_plan_ids' => empty($this->limit_plan_ids) ? null : collect($this->limit_plan_ids)
29 | ->map(fn(mixed $id): string => (string) $id)
30 | ->values()
31 | ->all(),
32 | 'limit_period' => empty($this->limit_period) ? null : collect($this->limit_period)
33 | ->map(fn(mixed $period): string => (string) PlanService::convertToLegacyPeriod($period))
34 | ->values()
35 | ->all(),
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Http/Resources/InviteCodeResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | $data = [
18 | "user_id" => $this['user_id'],
19 | "code" => $this['code'],
20 | "pv" => $this['pv'],
21 | "status" => $this['status'],
22 | "created_at" => $this['created_at'],
23 | "updated_at" => $this['updated_at']
24 | ];
25 | if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id'];
26 | return $data;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Resources/MessageResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | return [
18 | "id" => $this['id'],
19 | "ticket_id" => $this['ticket_id'],
20 | "is_me" => $this['is_from_user'],
21 | "message" => $this["message"],
22 | "created_at" => $this['created_at'],
23 | "updated_at" => $this['updated_at']
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Resources/NodeResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | return [
18 | 'id' => $this['id'],
19 | 'type' => $this['type'],
20 | 'version' => $this['version'] ?? null,
21 | 'name' => $this['name'],
22 | 'rate' => $this['rate'],
23 | 'tags' => $this['tags'],
24 | 'is_online' => $this['is_online'],
25 | 'cache_key' => $this['cache_key'],
26 | 'last_check_at' => $this['last_check_at']
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Resources/OrderResource.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public function toArray(Request $request): array
21 | {
22 | return [
23 | ...parent::toArray($request),
24 | 'period' => PlanService::getLegacyPeriod((string)$this->period),
25 | 'plan' => $this->whenLoaded('plan', fn() => PlanResource::make($this->plan)),
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Resources/TicketResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | $data = [
18 | "id" => $this['id'],
19 | "level" => $this['level'],
20 | "reply_status" => $this['reply_status'],
21 | "status" => $this['status'],
22 | "subject" => $this['subject'],
23 | "message" => array_key_exists('message',$this->additional) ? MessageResource::collection($this['message']) : null,
24 | "created_at" => $this['created_at'],
25 | "updated_at" => $this['updated_at']
26 | ];
27 | if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id'];
28 | return $data;
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Resources/TrafficLogResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | $data = [
18 | "d" => $this['d'],
19 | "u" => $this['u'],
20 | "record_at" => $this['record_at'],
21 | "server_rate" => $this['server_rate'],
22 | ];
23 | if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id'];
24 | return $data;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Routes/V1/ClientRoute.php:
--------------------------------------------------------------------------------
1 | group([
13 | 'prefix' => 'client',
14 | 'middleware' => 'client'
15 | ], function ($router) {
16 | // Client
17 | $router->get('/subscribe', [ClientController::class, 'subscribe'])->name('client.subscribe.legacy');
18 | // App
19 | $router->get('/app/getConfig', [AppController::class, 'getConfig']);
20 | $router->get('/app/getVersion', [AppController::class, 'getVersion']);
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Routes/V1/GuestRoute.php:
--------------------------------------------------------------------------------
1 | group([
15 | 'prefix' => 'guest'
16 | ], function ($router) {
17 | // Plan
18 | $router->get('/plan/fetch', [PlanController::class, 'fetch']);
19 | // Telegram
20 | $router->post('/telegram/webhook', [TelegramController::class, 'webhook']);
21 | // Payment
22 | $router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', [PaymentController::class, 'notify']);
23 | // Comm
24 | $router->get('/comm/config', [CommController::class, 'config']);
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Routes/V1/PassportRoute.php:
--------------------------------------------------------------------------------
1 | group([
13 | 'prefix' => 'passport'
14 | ], function ($router) {
15 | // Auth
16 | $router->post('/auth/register', [AuthController::class, 'register']);
17 | $router->post('/auth/login', [AuthController::class, 'login']);
18 | $router->get('/auth/token2Login', [AuthController::class, 'token2Login']);
19 | $router->post('/auth/forget', [AuthController::class, 'forget']);
20 | $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
21 | $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
22 | // Comm
23 | $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
24 | $router->post('/comm/pv', [CommController::class, 'pv']);
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Routes/V1/ServerRoute.php:
--------------------------------------------------------------------------------
1 | group([
15 | 'prefix' => 'server',
16 | ], function ($router) {
17 | $router->group([
18 | 'prefix' => 'UniProxy',
19 | 'middleware' => 'server'
20 | ], function ($route) {
21 | $route->get('config', [UniProxyController::class, 'config']);
22 | $route->get('user', [UniProxyController::class, 'user']);
23 | $route->post('push', [UniProxyController::class, 'push']);
24 | $route->post('alive', [UniProxyController::class, 'alive']);
25 | $route->get('alivelist', [UniProxyController::class, 'alivelist']);
26 | $route->post('status', [UniProxyController::class, 'status']);
27 | });
28 | $router->group([
29 | 'prefix' => 'ShadowsocksTidalab',
30 | 'middleware' => 'server:shadowsocks'
31 | ], function ($route) {
32 | $route->get('user', [ShadowsocksTidalabController::class, 'user']);
33 | $route->post('submit', [ShadowsocksTidalabController::class, 'submit']);
34 | });
35 | $router->group([
36 | 'prefix' => 'TrojanTidalab',
37 | 'middleware' => 'server:trojan'
38 | ], function ($route) {
39 | $route->get('config', [TrojanTidalabController::class, 'config']);
40 | $route->get('user', [TrojanTidalabController::class, 'user']);
41 | $route->post('submit', [TrojanTidalabController::class, 'submit']);
42 | });
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Http/Routes/V2/PassportRoute.php:
--------------------------------------------------------------------------------
1 | group([
13 | 'prefix' => 'passport'
14 | ], function ($router) {
15 | // Auth
16 | $router->post('/auth/register', [AuthController::class, 'register']);
17 | $router->post('/auth/login', [AuthController::class, 'login']);
18 | $router->get ('/auth/token2Login', [AuthController::class, 'token2Login']);
19 | $router->post('/auth/forget', [AuthController::class, 'forget']);
20 | $router->post('/auth/getQuickLoginUrl', [AuthController::class, 'getQuickLoginUrl']);
21 | $router->post('/auth/loginWithMailLink', [AuthController::class, 'loginWithMailLink']);
22 | // Comm
23 | $router->post('/comm/sendEmailVerify', [CommController::class, 'sendEmailVerify']);
24 | $router->post('/comm/pv', [CommController::class, 'pv']);
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Routes/V2/UserRoute.php:
--------------------------------------------------------------------------------
1 | group([
12 | 'prefix' => 'user',
13 | 'middleware' => 'user'
14 | ], function ($router) {
15 | // User
16 | $router->get('/resetSecurity', [UserController::class, 'resetSecurity']);
17 | $router->get('/info', [UserController::class, 'info']);
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/Jobs/OrderHandleJob.php:
--------------------------------------------------------------------------------
1 | onQueue('order_handle');
29 | $this->tradeNo = $tradeNo;
30 | }
31 |
32 | /**
33 | * Execute the job.
34 | *
35 | * @return void
36 | */
37 | public function handle()
38 | {
39 | $order = Order::where('trade_no', $this->tradeNo)
40 | ->lockForUpdate()
41 | ->first();
42 | if (!$order) return;
43 | $orderService = new OrderService($order);
44 | switch ($order->status) {
45 | // cancel
46 | case Order::STATUS_PENDING:
47 | if ($order->created_at <= (time() - 3600 * 2)) {
48 | $orderService->cancel();
49 | }
50 | break;
51 | case Order::STATUS_PROCESSING:
52 | $orderService->open();
53 | break;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/Jobs/SendEmailJob.php:
--------------------------------------------------------------------------------
1 | onQueue($queue);
27 | $this->params = $params;
28 | }
29 |
30 | /**
31 | * Execute the job.
32 | *
33 | * @return void
34 | */
35 | public function handle()
36 | {
37 | $mailLog = MailService::sendEmail($this->params);
38 | if($mailLog['error']){
39 | $this->release(); //发送失败将触发重试
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Jobs/SendTelegramJob.php:
--------------------------------------------------------------------------------
1 | onQueue('send_telegram');
29 | $this->telegramId = $telegramId;
30 | $this->text = $text;
31 | }
32 |
33 | /**
34 | * Execute the job.
35 | *
36 | * @return void
37 | */
38 | public function handle()
39 | {
40 | $telegramService = new TelegramService();
41 | $telegramService->sendMessage($this->telegramId, $this->text, 'markdown');
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Jobs/SyncUserOnlineStatusJob.php:
--------------------------------------------------------------------------------
1 | updates)) {
39 | return;
40 | }
41 | collect($this->updates)
42 | ->chunk(1000)
43 | ->each(function (Collection $chunk) {
44 | $userIds = $chunk->pluck('id')->all();
45 | User::query()
46 | ->whereIn('id', $userIds)
47 | ->each(function (User $user) use ($chunk) {
48 | $update = $chunk->firstWhere('id', $user->id);
49 | if ($update) {
50 | $user->update([
51 | 'online_count' => $update['count'],
52 | 'last_online_at' => now(),
53 | ]);
54 | }
55 | });
56 | });
57 | }
58 |
59 | /**
60 | * 任务失败的处理
61 | */
62 | public function failed(\Throwable $exception): void
63 | {
64 | \Log::error('Failed to sync user online status', [
65 | 'error' => $exception->getMessage(),
66 | 'updates_count' => count($this->updates)
67 | ]);
68 | }
69 | }
--------------------------------------------------------------------------------
/app/Jobs/TrafficFetchJob.php:
--------------------------------------------------------------------------------
1 | onQueue('traffic_fetch');
30 | $this->server = $server;
31 | $this->data = $data;
32 | $this->protocol = $protocol;
33 | $this->timestamp = $timestamp;
34 | }
35 |
36 | public function handle(): void
37 | {
38 | foreach ($this->data as $uid => $v) {
39 | User::where('id', $uid)
40 | ->incrementEach(
41 | [
42 | 'u' => $v[0] * $this->server['rate'],
43 | 'd' => $v[1] * $this->server['rate'],
44 | ],
45 | ['t' => time()]
46 | );
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/Logging/MysqlLogger.php:
--------------------------------------------------------------------------------
1 | pushHandler(new MysqlLoggerHandler());
9 | });
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/Logging/MysqlLoggerHandler.php:
--------------------------------------------------------------------------------
1 | toArray();
20 | try {
21 | if (isset($record['context']['exception']) && is_object($record['context']['exception'])) {
22 | $record['context']['exception'] = (array)$record['context']['exception'];
23 | }
24 |
25 | $record['request_data'] = request()->all();
26 |
27 | $log = [
28 | 'title' => $record['message'],
29 | 'level' => $record['level_name'],
30 | 'host' => $record['extra']['request_host'] ?? request()->getSchemeAndHttpHost(),
31 | 'uri' => $record['extra']['request_uri'] ?? request()->getRequestUri(),
32 | 'method' => $record['extra']['request_method'] ?? request()->getMethod(),
33 | 'ip' => request()->getClientIp(),
34 | 'data' => json_encode($record['request_data']),
35 | 'context' => json_encode($record['context']),
36 | 'created_at' => $record['datetime']->getTimestamp(),
37 | 'updated_at' => $record['datetime']->getTimestamp(),
38 | ];
39 |
40 | LogModel::insert($log);
41 | } catch (\Exception $e) {
42 | // Log::channel('daily')->error($e->getMessage().$e->getFile().$e->getTraceAsString());
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Models/CommissionLog.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp'
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/Coupon.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
15 | 'updated_at' => 'timestamp',
16 | 'limit_plan_ids' => 'array',
17 | 'limit_period' => 'array'
18 | ];
19 |
20 | public function getLimitPeriodAttribute($value)
21 | {
22 | return collect(json_decode((string) $value, true))->map(function ($item) {
23 | return PlanService::getPeriodKey($item);
24 | })->toArray();
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/Models/InviteCode.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
13 | 'updated_at' => 'timestamp'
14 | ];
15 | }
16 |
--------------------------------------------------------------------------------
/app/Models/Knowledge.php:
--------------------------------------------------------------------------------
1 | 'boolean',
14 | 'created_at' => 'timestamp',
15 | 'updated_at' => 'timestamp',
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/app/Models/Log.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
15 | 'updated_at' => 'timestamp'
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/app/Models/MailLog.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp'
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/Notice.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp',
15 | 'tags' => 'array',
16 | 'show' => 'boolean',
17 | ];
18 | }
19 |
--------------------------------------------------------------------------------
/app/Models/Payment.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp',
15 | 'config' => 'array',
16 | 'enable' => 'boolean'
17 | ];
18 | }
19 |
--------------------------------------------------------------------------------
/app/Models/Plugin.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
24 | 'updated_at' => 'timestamp'
25 | ];
26 |
27 | public function users(): HasMany
28 | {
29 | return $this->hasMany(User::class, 'group_id', 'id');
30 | }
31 |
32 | public function servers()
33 | {
34 | return Server::whereJsonContains('group_ids', (string) $this->id)->get();
35 | }
36 |
37 | /**
38 | * 获取服务器数量
39 | */
40 | protected function serverCount(): Attribute
41 | {
42 | return Attribute::make(
43 | get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(),
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/Models/ServerLog.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp'
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/ServerRoute.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp',
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/ServerStat.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp'
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/Setting.php:
--------------------------------------------------------------------------------
1 | 'string',
13 | 'value' => 'string',
14 | ];
15 |
16 | public function getValueAttribute($value)
17 | {
18 | if ($value === null) {
19 | return null;
20 | }
21 |
22 | if (is_array($value)) {
23 | return $value;
24 | }
25 |
26 | if (is_numeric($value) && !preg_match('/[^\d.]/', $value)) {
27 | return $value;
28 | }
29 |
30 | $decodedValue = json_decode($value, true);
31 | if (json_last_error() === JSON_ERROR_NONE) {
32 | return $decodedValue;
33 | }
34 |
35 | return $value;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Models/Stat.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
14 | 'updated_at' => 'timestamp'
15 | ];
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/StatServer.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
26 | 'updated_at' => 'timestamp'
27 | ];
28 |
29 | public function server()
30 | {
31 | return $this->belongsTo(Server::class, 'server_id');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Models/StatUser.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
26 | 'updated_at' => 'timestamp'
27 | ];
28 | }
29 |
--------------------------------------------------------------------------------
/app/Models/Ticket.php:
--------------------------------------------------------------------------------
1 | $messages 关联的工单消息
24 | */
25 | class Ticket extends Model
26 | {
27 | protected $table = 'v2_ticket';
28 | protected $dateFormat = 'U';
29 | protected $guarded = ['id'];
30 | protected $casts = [
31 | 'created_at' => 'timestamp',
32 | 'updated_at' => 'timestamp'
33 | ];
34 |
35 | const STATUS_OPENING = 0;
36 | const STATUS_CLOSED = 1;
37 | public static $statusMap = [
38 | self::STATUS_OPENING => '开启',
39 | self::STATUS_CLOSED => '关闭'
40 | ];
41 |
42 | public function user(): BelongsTo
43 | {
44 | return $this->belongsTo(User::class, 'user_id', 'id');
45 | }
46 |
47 | /**
48 | * 关联的工单消息
49 | */
50 | public function messages(): HasMany
51 | {
52 | return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
53 | }
54 |
55 | // 即将删除
56 | public function message(): HasMany
57 | {
58 | return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/Models/TicketMessage.php:
--------------------------------------------------------------------------------
1 | 'timestamp',
28 | 'updated_at' => 'timestamp'
29 | ];
30 |
31 | protected $appends = ['is_from_user', 'is_from_admin'];
32 |
33 | /**
34 | * 关联的工单
35 | */
36 | public function ticket(): BelongsTo
37 | {
38 | return $this->belongsTo(Ticket::class, 'ticket_id', 'id');
39 | }
40 |
41 | /**
42 | * 判断消息是否由工单发起人发送
43 | */
44 | public function getIsFromUserAttribute(): bool
45 | {
46 | return $this->ticket->user_id === $this->user_id;
47 | }
48 |
49 | /**
50 | * 判断消息是否由管理员发送
51 | */
52 | public function getIsFromAdminAttribute(): bool
53 | {
54 | return $this->ticket->user_id !== $this->user_id;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/Plugins/Telegram/Commands/Bind.php:
--------------------------------------------------------------------------------
1 | is_private) return;
15 | if (!isset($message->args[0])) {
16 | throw new ApiException('参数有误,请携带订阅地址发送', 422);
17 | }
18 | $subscribeUrl = $message->args[0];
19 | $subscribeUrl = parse_url($subscribeUrl);
20 |
21 | // 首先尝试从查询参数获取token
22 | $token = null;
23 | if (isset($subscribeUrl['query'])) {
24 | parse_str($subscribeUrl['query'], $query);
25 | $token = $query['token'] ?? null;
26 | }
27 |
28 | if (!$token && isset($subscribeUrl['path'])) {
29 | $pathParts = explode('/', trim($subscribeUrl['path'], '/'));
30 | $token = end($pathParts);
31 | }
32 |
33 | if (!$token) {
34 | throw new ApiException('订阅地址无效');
35 | }
36 | $user = User::where('token', $token)->first();
37 | if (!$user) {
38 | throw new ApiException('用户不存在');
39 | }
40 | if ($user->telegram_id) {
41 | throw new ApiException('该账号已经绑定了Telegram账号');
42 | }
43 | $user->telegram_id = $message->chat_id;
44 | if (!$user->save()) {
45 | throw new ApiException('设置失败');
46 | }
47 | $telegramService = $this->telegramService;
48 | $telegramService->sendMessage($message->chat_id, '绑定成功');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/Plugins/Telegram/Commands/GetLatestUrl.php:
--------------------------------------------------------------------------------
1 | telegramService;
14 | $text = sprintf(
15 | "%s的最新网址是:%s",
16 | admin_setting('app_name', 'XBoard'),
17 | admin_setting('app_url')
18 | );
19 | $telegramService->sendMessage($message->chat_id, $text, 'markdown');
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Plugins/Telegram/Commands/ReplyTicket.php:
--------------------------------------------------------------------------------
1 | is_private) return;
16 | $this->replayTicket($message, $match[1]);
17 | }
18 |
19 |
20 | private function replayTicket($msg, $ticketId)
21 | {
22 | $user = User::where('telegram_id', $msg->chat_id)->first();
23 | if (!$user) {
24 | throw new ApiException('用户不存在');
25 | }
26 | if (!$msg->text) return;
27 | if (!($user->is_admin || $user->is_staff)) return;
28 | $ticketService = new TicketService();
29 | $ticketService->replyByAdmin(
30 | $ticketId,
31 | $msg->text,
32 | $user->id
33 | );
34 | $telegramService = $this->telegramService;
35 | $telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
36 | $telegramService->sendMessageWithAdmin("#`{$ticketId}` 的工单已由 {$user->email} 进行回复", true);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Plugins/Telegram/Commands/Traffic.php:
--------------------------------------------------------------------------------
1 | telegramService;
15 | if (!$message->is_private) return;
16 | $user = User::where('telegram_id', $message->chat_id)->first();
17 | if (!$user) {
18 | $telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
19 | return;
20 | }
21 | $transferEnable = Helper::trafficConvert($user->transfer_enable);
22 | $up = Helper::trafficConvert($user->u);
23 | $down = Helper::trafficConvert($user->d);
24 | $remaining = Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d));
25 | $text = "🚥流量查询\n———————————————\n计划流量:`{$transferEnable}`\n已用上行:`{$up}`\n已用下行:`{$down}`\n剩余流量:`{$remaining}`";
26 | $telegramService->sendMessage($message->chat_id, $text, 'markdown');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Plugins/Telegram/Commands/UnBind.php:
--------------------------------------------------------------------------------
1 | is_private) return;
15 | $user = User::where('telegram_id', $message->chat_id)->first();
16 | $telegramService = $this->telegramService;
17 | if (!$user) {
18 | $telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
19 | return;
20 | }
21 | $user->telegram_id = NULL;
22 | if (!$user->save()) {
23 | throw new ApiException('解绑失败');
24 | }
25 | $telegramService->sendMessage($message->chat_id, '解绑成功', 'markdown');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Plugins/Telegram/Telegram.php:
--------------------------------------------------------------------------------
1 | telegramService = new TelegramService();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/Protocols/Shadowsocks.php:
--------------------------------------------------------------------------------
1 | servers;
14 | $user = $this->user;
15 |
16 | $configs = [];
17 | $subs = [];
18 | $subs['servers'] = [];
19 | $subs['bytes_used'] = '';
20 | $subs['bytes_remaining'] = '';
21 |
22 | $bytesUsed = $user['u'] + $user['d'];
23 | $bytesRemaining = $user['transfer_enable'] - $bytesUsed;
24 |
25 | foreach ($servers as $item) {
26 | if (
27 | $item['type'] === 'shadowsocks'
28 | && in_array(data_get($item, 'protocol_settings.cipher'), ['aes-128-gcm', 'aes-256-gcm', 'aes-192-gcm', 'chacha20-ietf-poly1305'])
29 | ) {
30 | array_push($configs, self::SIP008($item, $user));
31 | }
32 | }
33 |
34 | $subs['version'] = 1;
35 | $subs['bytes_used'] = $bytesUsed;
36 | $subs['bytes_remaining'] = $bytesRemaining;
37 | $subs['servers'] = array_merge($subs['servers'], $configs);
38 |
39 | return response()->json($subs)
40 | ->header('content-type', 'application/json');
41 | }
42 |
43 | public static function SIP008($server, $user)
44 | {
45 | $config = [
46 | "id" => $server['id'],
47 | "remarks" => $server['name'],
48 | "server" => $server['host'],
49 | "server_port" => $server['port'],
50 | "password" => $user['uuid'],
51 | "method" => data_get($server, 'protocol_settings.cipher')
52 | ];
53 | return $config;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $policies = [
15 | // 'App\Model' => 'App\Policies\ModelPolicy',
16 | ];
17 |
18 | /**
19 | * 注册任何认证/授权服务
20 | * @return void
21 | */
22 | public function boot()
23 | {
24 | $this->registerPolicies();
25 |
26 | //
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | >
13 | */
14 | protected $listen = [
15 | ];
16 |
17 | /**
18 | * 注册任何事件
19 | * @return void
20 | */
21 | public function boot()
22 | {
23 | parent::boot();
24 |
25 | //
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Providers/HorizonServiceProvider.php:
--------------------------------------------------------------------------------
1 | email, [
39 | //
40 | ]);
41 | });
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Providers/OctaneServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
22 | return;
23 | }
24 | if ($this->app->bound('octane')) {
25 | $this->app['events']->listen(WorkerStarting::class, function () {
26 | app(UpdateService::class)->updateVersionCache();
27 | HookManager::reset();
28 | });
29 | }
30 | // 每半钟执行一次调度检查
31 | Octane::tick('scheduler', function () {
32 | $lock = Cache::lock('scheduler-lock', 30);
33 |
34 | if ($lock->get()) {
35 | try {
36 | Artisan::call('schedule:run');
37 | } finally {
38 | $lock->release();
39 | }
40 | }
41 | })->seconds(30);
42 | }
43 | }
--------------------------------------------------------------------------------
/app/Providers/PluginServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->scoped(PluginManager::class, function ($app) {
19 | return new PluginManager();
20 | });
21 | }
22 |
23 | public function boot(): void
24 | {
25 | if (!file_exists(base_path('plugins'))) {
26 | mkdir(base_path('plugins'), 0755, true);
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/Providers/ProtocolServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->scoped('protocols.manager', function ($app) {
18 | return new ProtocolManager($app);
19 | });
20 |
21 | $this->app->scoped('protocols.flags', function ($app) {
22 | return $app->make('protocols.manager')->getAllFlags();
23 | });
24 | }
25 |
26 | /**
27 | * 启动服务
28 | *
29 | * @return void
30 | */
31 | public function boot()
32 | {
33 | // 在启动时预加载协议类并缓存
34 | $this->app->make('protocols.manager')->registerAllProtocols();
35 |
36 | }
37 |
38 | /**
39 | * 提供的服务
40 | *
41 | * @return array
42 | */
43 | public function provides()
44 | {
45 | return [
46 | 'protocols.manager',
47 | 'protocols.flags',
48 | ];
49 | }
50 | }
--------------------------------------------------------------------------------
/app/Providers/SettingServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->scoped(Setting::class, function (Application $app) {
19 | return new Setting();
20 | });
21 |
22 | }
23 |
24 | /**
25 | * Bootstrap services.
26 | *
27 | * @return void
28 | */
29 | public function boot()
30 | {
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Scope/FilterScope.php:
--------------------------------------------------------------------------------
1 | validate([
15 | 'filter.*.key' => "required|in:{$allowKeys}",
16 | 'filter.*.condition' => 'required|in:in,is,not,like,lt,gt',
17 | 'filter.*.value' => 'required'
18 | ]);
19 | $filters = $request->input('filter');
20 | if ($filters) {
21 | foreach ($filters as $k => $filter) {
22 | if ($filter['condition'] === 'in') {
23 | $builder->whereIn($filter['key'], $filter['value']);
24 | continue;
25 | }
26 | if ($filter['condition'] === 'is') {
27 | $builder->where($filter['key'], $filter['value']);
28 | continue;
29 | }
30 | if ($filter['condition'] === 'not') {
31 | $builder->where($filter['key'], '!=', $filter['value']);
32 | continue;
33 | }
34 | if ($filter['condition'] === 'gt') {
35 | $builder->where($filter['key'], '>', $filter['value']);
36 | continue;
37 | }
38 | if ($filter['condition'] === 'lt') {
39 | $builder->where($filter['key'], '<', $filter['value']);
40 | continue;
41 | }
42 | if ($filter['condition'] === 'like') {
43 | $builder->where($filter['key'], 'like', "%{$filter['value']}%");
44 | continue;
45 | }
46 | }
47 | }
48 | return $builder;
49 | }
50 | }
--------------------------------------------------------------------------------
/app/Services/Plugin/InterceptResponseException.php:
--------------------------------------------------------------------------------
1 | response = $response;
16 | }
17 |
18 | public function getResponse(): Response
19 | {
20 | return $this->response;
21 | }
22 | }
--------------------------------------------------------------------------------
/app/Services/SettingService.php:
--------------------------------------------------------------------------------
1 | first();
12 | return $setting ? $setting->value : $default;
13 | }
14 |
15 | public function getAll(){
16 | return SettingModel::all()->pluck('value', 'name')->toArray();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Traits/QueryOperators.php:
--------------------------------------------------------------------------------
1 | '=',
17 | 'gt' => '>',
18 | 'gte' => '>=',
19 | 'lt' => '<',
20 | 'lte' => '<=',
21 | 'like' => 'like',
22 | 'notlike' => 'not like',
23 | 'null' => 'null',
24 | 'notnull' => 'notnull',
25 | default => 'like'
26 | };
27 | }
28 |
29 | /**
30 | * 获取查询值格式化
31 | *
32 | * @param string $operator
33 | * @param mixed $value
34 | * @return mixed
35 | */
36 | protected function formatQueryValue(string $operator, mixed $value): mixed
37 | {
38 | return match (strtolower($operator)) {
39 | 'like', 'notlike' => "%{$value}%",
40 | 'null', 'notnull' => null,
41 | default => $value
42 | };
43 | }
44 |
45 | /**
46 | * 应用查询条件
47 | *
48 | * @param \Illuminate\Database\Eloquent\Builder $query
49 | * @param string $field
50 | * @param string $operator
51 | * @param mixed $value
52 | * @return void
53 | */
54 | protected function applyQueryCondition($query, string $field, string $operator, mixed $value): void
55 | {
56 | $queryOperator = $this->getQueryOperator($operator);
57 |
58 | if ($queryOperator === 'null') {
59 | $query->whereNull($field);
60 | } elseif ($queryOperator === 'notnull') {
61 | $query->whereNotNull($field);
62 | } else {
63 | $query->where($field, $queryOperator, $this->formatQueryValue($operator, $value));
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/app/Utils/Dict.php:
--------------------------------------------------------------------------------
1 | make(Illuminate\Contracts\Console\Kernel::class);
34 |
35 | $status = $kernel->handle(
36 | $input = new Symfony\Component\Console\Input\ArgvInput,
37 | new Symfony\Component\Console\Output\ConsoleOutput
38 | );
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Shutdown The Application
43 | |--------------------------------------------------------------------------
44 | |
45 | | Once Artisan has finished running, we will fire off the shutdown events
46 | | so that any final work may be done by the application before we shut
47 | | down the process. This is the last thing to happen to the request.
48 | |
49 | */
50 |
51 | $kernel->terminate($input, $status);
52 |
53 | exit($status);
54 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | singleton(
30 | Illuminate\Contracts\Http\Kernel::class,
31 | App\Http\Kernel::class
32 | );
33 |
34 | $app->singleton(
35 | Illuminate\Contracts\Console\Kernel::class,
36 | App\Console\Kernel::class
37 | );
38 |
39 | $app->singleton(
40 | Illuminate\Contracts\Debug\ExceptionHandler::class,
41 | App\Exceptions\Handler::class
42 | );
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | Return The Application
47 | |--------------------------------------------------------------------------
48 | |
49 | | This script returns the application instance. The instance is given to
50 | | the calling script so we can separate the building of the instances
51 | | from the actual running of the application and sending responses.
52 | |
53 | */
54 |
55 | return $app;
56 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/compose.sample.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | web:
3 | image: ghcr.io/cedar2025/xboard:new
4 | volumes:
5 | - ./.docker/.data/redis/:/data/
6 | - ./:/www/
7 | environment:
8 | - docker=true
9 | depends_on:
10 | - redis
11 | network_mode: host
12 | command: php artisan octane:start --port=7001 --host=0.0.0.0
13 | restart: always
14 | horizon:
15 | image: ghcr.io/cedar2025/xboard:new
16 | volumes:
17 | - ./.docker/.data/redis/:/data/
18 | - ./:/www/
19 | restart: always
20 | network_mode: host
21 | command: php artisan horizon
22 | depends_on:
23 | - redis
24 | redis:
25 | image: redis:7-alpine
26 | command: redis-server --unixsocket /data/redis.sock --unixsocketperm 777
27 | restart: unless-stopped
28 | volumes:
29 | - ./.docker/.data/redis:/data
30 | sysctls:
31 | net.core.somaxconn: 1024
32 |
--------------------------------------------------------------------------------
/config/broadcasting.php:
--------------------------------------------------------------------------------
1 | env('BROADCAST_DRIVER', 'null'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Broadcast Connections
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the broadcast connections that will be used
26 | | to broadcast events to other systems or over websockets. Samples of
27 | | each available type of connection are provided inside this array.
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'pusher' => [
34 | 'driver' => 'pusher',
35 | 'key' => env('PUSHER_APP_KEY'),
36 | 'secret' => env('PUSHER_APP_SECRET'),
37 | 'app_id' => env('PUSHER_APP_ID'),
38 | 'options' => [
39 | 'cluster' => env('PUSHER_APP_CLUSTER'),
40 | 'useTLS' => true,
41 | ],
42 | ],
43 |
44 | 'redis' => [
45 | 'driver' => 'redis',
46 | 'connection' => 'default',
47 | ],
48 |
49 | 'log' => [
50 | 'driver' => 'log',
51 | ],
52 |
53 | 'null' => [
54 | 'driver' => 'null',
55 | ],
56 |
57 | ],
58 |
59 | ];
60 |
--------------------------------------------------------------------------------
/config/cloud_storage.php:
--------------------------------------------------------------------------------
1 | [
6 | 'key_file' => env('GOOGLE_CLOUD_KEY_FILE') ? base_path(env('GOOGLE_CLOUD_KEY_FILE')) : null,
7 | 'storage_bucket' => env('GOOGLE_CLOUD_STORAGE_BUCKET'),
8 | ],
9 |
10 | ];
--------------------------------------------------------------------------------
/config/cors.php:
--------------------------------------------------------------------------------
1 | ['api/*'],
19 |
20 | 'allowed_methods' => ['*'],
21 |
22 | 'allowed_origins' => ['*'],
23 |
24 | 'allowed_origins_patterns' => [],
25 |
26 | 'allowed_headers' => ['*'],
27 |
28 | 'exposed_headers' => [],
29 |
30 | 'max_age' => 0,
31 |
32 | 'supports_credentials' => false,
33 |
34 | ];
35 |
--------------------------------------------------------------------------------
/config/hashing.php:
--------------------------------------------------------------------------------
1 | 'bcrypt',
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Bcrypt Options
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may specify the configuration options that should be used when
26 | | passwords are hashed using the Bcrypt algorithm. This will allow you
27 | | to control the amount of time it takes to hash the given password.
28 | |
29 | */
30 |
31 | 'bcrypt' => [
32 | 'rounds' => env('BCRYPT_ROUNDS', 10),
33 | ],
34 |
35 | /*
36 | |--------------------------------------------------------------------------
37 | | Argon Options
38 | |--------------------------------------------------------------------------
39 | |
40 | | Here you may specify the configuration options that should be used when
41 | | passwords are hashed using the Argon algorithm. These will allow you
42 | | to control the amount of time it takes to hash the given password.
43 | |
44 | */
45 |
46 | 'argon' => [
47 | 'memory' => 8192,
48 | 'threads' => 2,
49 | 'time' => 2,
50 | ],
51 |
52 | ];
53 |
--------------------------------------------------------------------------------
/config/hidden_features.php:
--------------------------------------------------------------------------------
1 | (env('ENABLE_EXPOSED_USER_COUNT_FIX') === base64_decode('M2YwNmYxODI='))
5 | ];
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'domain' => env('MAILGUN_DOMAIN'),
19 | 'secret' => env('MAILGUN_SECRET'),
20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
21 | ],
22 |
23 | 'postmark' => [
24 | 'token' => env('POSTMARK_TOKEN'),
25 | ],
26 |
27 | 'ses' => [
28 | 'key' => env('AWS_ACCESS_KEY_ID'),
29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
30 | 'region' => env('AWS_V2BOARD_REGION', 'us-east-1'),
31 | ],
32 |
33 | ];
34 |
--------------------------------------------------------------------------------
/config/theme/.gitignore:
--------------------------------------------------------------------------------
1 | *.php
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/config/view.php:
--------------------------------------------------------------------------------
1 | [
17 | resource_path('views'),
18 | ],
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Compiled View Path
23 | |--------------------------------------------------------------------------
24 | |
25 | | This option determines where all the compiled Blade templates will be
26 | | stored for your application. Typically, this is within the storage
27 | | directory. However, as usual, you are free to change this value.
28 | |
29 | */
30 |
31 | 'compiled' => env(
32 | 'VIEW_COMPILED_PATH',
33 | realpath(storage_path('framework/views'))
34 | ),
35 |
36 | ];
37 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 | *.sqlite-journal
3 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | define(User::class, function (Faker $faker) {
21 | return [
22 | 'name' => $faker->name,
23 | 'email' => $faker->unique()->safeEmail,
24 | 'email_verified_at' => now(),
25 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
26 | 'remember_token' => Str::random(10),
27 | ];
28 | });
29 |
--------------------------------------------------------------------------------
/database/migrations/2019_08_19_000000_create_failed_jobs_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
19 | $table->text('connection');
20 | $table->text('queue');
21 | $table->longText('payload');
22 | $table->longText('exception');
23 | $table->timestamp('failed_at')->useCurrent();
24 | });
25 | }
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('failed_jobs');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->morphs('tokenable');
17 | $table->string('name');
18 | $table->string('token', 64)->unique();
19 | $table->text('abilities')->nullable();
20 | $table->timestamp('last_used_at')->nullable();
21 | $table->timestamp('expires_at')->nullable();
22 | $table->timestamps();
23 | });
24 | }
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | */
30 | public function down(): void
31 | {
32 | Schema::dropIfExists('personal_access_tokens');
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/database/migrations/2023_08_14_221234_create_v2_settings_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('group')->comment('设置分组')->nullable();
19 | $table->string('type')->comment('设置类型')->nullable();
20 | $table->string('name')->comment('设置名称')->uniqid();
21 | $table->string('value')->comment('设置值')->nullable();
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | *
29 | * @return void
30 | */
31 | public function down()
32 | {
33 | Schema::dropIfExists('v2_settings');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2023_09_04_190923_add_column_excludes_to_server_table.php:
--------------------------------------------------------------------------------
1 | text("excludes")->nullable()->after('tags');
18 | });
19 | Schema::table('v2_server_shadowsocks', function (Blueprint $table) {
20 | $table->text("excludes")->nullable()->after('tags');
21 | });
22 | Schema::table('v2_server_trojan', function (Blueprint $table) {
23 | $table->text("excludes")->nullable()->after('tags');
24 | });
25 | Schema::table('v2_server_vless', function (Blueprint $table) {
26 | $table->text("excludes")->nullable()->after('tags');
27 | });
28 | Schema::table('v2_server_vmess', function (Blueprint $table) {
29 | $table->text("excludes")->nullable()->after('tags');
30 | });
31 | }
32 |
33 | /**
34 | * Reverse the migrations.
35 | *
36 | * @return void
37 | */
38 | public function down()
39 | {
40 | Schema::table('v2_server_hysteria', function (Blueprint $table) {
41 | $table->dropColumn('excludes');
42 | });
43 | Schema::table('v2_server_shadowsocks', function (Blueprint $table) {
44 | $table->dropColumn('excludes');
45 | });
46 | Schema::table('v2_server_trojan', function (Blueprint $table) {
47 | $table->dropColumn('excludes');
48 | });
49 | Schema::table('v2_server_vless', function (Blueprint $table) {
50 | $table->dropColumn('excludes');
51 | });
52 | Schema::table('v2_server_vmess', function (Blueprint $table) {
53 | $table->dropColumn('excludes');
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/database/migrations/2023_09_06_195956_add_column_ips_to_server_table.php:
--------------------------------------------------------------------------------
1 | string("ips")->nullable()->after('excludes');
18 | });
19 | Schema::table('v2_server_shadowsocks', function (Blueprint $table) {
20 | $table->string("ips")->nullable()->after('excludes');
21 | });
22 | Schema::table('v2_server_trojan', function (Blueprint $table) {
23 | $table->string("ips")->nullable()->after('excludes');
24 | });
25 | Schema::table('v2_server_vless', function (Blueprint $table) {
26 | $table->string("ips")->nullable()->after('excludes');
27 | });
28 | Schema::table('v2_server_vmess', function (Blueprint $table) {
29 | $table->string("ips")->nullable()->after('excludes');
30 | });
31 | }
32 |
33 | /**
34 | * Reverse the migrations.
35 | *
36 | * @return void
37 | */
38 | public function down()
39 | {
40 | Schema::table('v2_server_hysteria', function (Blueprint $table) {
41 | $table->dropColumn('ips');
42 | });
43 | Schema::table('v2_server_shadowsocks', function (Blueprint $table) {
44 | $table->dropColumn('ips');
45 | });
46 | Schema::table('v2_server_trojan', function (Blueprint $table) {
47 | $table->dropColumn('ips');
48 | });
49 | Schema::table('v2_server_vless', function (Blueprint $table) {
50 | $table->dropColumn('ips');
51 | });
52 | Schema::table('v2_server_vmess', function (Blueprint $table) {
53 | $table->dropColumn('ips');
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/database/migrations/2023_09_14_013244_add_column_alpn_to_server_hysteria_table.php:
--------------------------------------------------------------------------------
1 | tinyInteger('alpn',false,true)->default(0)->comment('ALPN,0:hysteria、1:http/1.1、2:h2、3:h3');
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | *
24 | * @return void
25 | */
26 | public function down()
27 | {
28 | Schema::table('v2_server_hysteria', function (Blueprint $table) {
29 | $table->dropColumn('alpn');
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/database/migrations/2023_09_24_040317_add_column_network_and_network_settings_to_v2_server_trojan.php:
--------------------------------------------------------------------------------
1 | string('network', 11)->default('tcp')->after('server_name')->comment('传输协议');
18 | $table->text('networkSettings')->nullable()->after('network')->comment('传输协议配置');
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | *
25 | * @return void
26 | */
27 | public function down()
28 | {
29 | Schema::table('v2_server_trojan', function (Blueprint $table) {
30 | $table->dropColumn(["network","networkSettings"]);
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_09_29_044957_add_column_version_and_is_obfs_to_server_hysteria_table.php:
--------------------------------------------------------------------------------
1 | tinyInteger('version',false,true)->default(1)->comment('hysteria版本,Version:1\2');
18 | $table->boolean('is_obfs')->default(true)->comment('是否开启obfs');
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | *
25 | * @return void
26 | */
27 | public function down()
28 | {
29 | Schema::table('v2_server_hysteria', function (Blueprint $table) {
30 | $table->dropColumn('version','is_obfs');
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_11_19_205026_change_column_value_to_v2_settings_table.php:
--------------------------------------------------------------------------------
1 | text('value')->comment('设置值')->nullable()->change();
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('v2_settings', function (Blueprint $table) {
25 | $table->string('value')->comment('设置值')->nullable()->change();
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/2023_12_12_212239_add_index_to_v2_user_table.php:
--------------------------------------------------------------------------------
1 | index(['u','d','expired_at','group_id','banned','transfer_enable']);
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('v2_user', function (Blueprint $table) {
25 | $table->dropIndex(['u','d','expired_at','group_id','banned','transfer_enable']);
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/2024_03_19_103149_modify_icon_column_to_v2_payment_table.php:
--------------------------------------------------------------------------------
1 | text('icon')->nullable()->change();
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('v2_payment', function (Blueprint $table) {
25 | $table->string('icon')->nullable()->change();
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_01_130644_modify_commission_status_in_v2_order_table.php:
--------------------------------------------------------------------------------
1 | integer('commission_status')->nullable()->default(null)->comment('0待确认1发放中2有效3无效')->change();
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('v2_order', function (Blueprint $table) {
25 | $table->integer('commission_status')->default(false)->comment('0待确认1发放中2有效3无效')->change();
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_10_152139_add_device_limit_column.php:
--------------------------------------------------------------------------------
1 | unsignedInteger('device_limit')->nullable()->after('speed_limit');
12 | });
13 | Schema::table('v2_user', function (Blueprint $table) {
14 | $table->integer('device_limit')->nullable()->after('expired_at');
15 | $table->integer('online_count')->nullable()->after('device_limit');
16 | $table->timestamp('last_online_at')->nullable()->after('online_count');
17 | });
18 | }
19 |
20 | public function down(): void
21 | {
22 | Schema::table('v2_user', function (Blueprint $table) {
23 | $table->dropColumn('device_limit');
24 | $table->dropColumn('online_count');
25 | $table->dropColumn('last_online_at');
26 | });
27 | Schema::table('v2_plan', function (Blueprint $table) {
28 | $table->dropColumn('device_limit');
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_12_190315_add_sort_to_v2_notice_table.php:
--------------------------------------------------------------------------------
1 | integer('sort')->nullable()->after('id')->index();
17 | });
18 |
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | */
24 | public function down(): void
25 | {
26 | Schema::table('v2_notice', function (Blueprint $table) {
27 | $table->dropColumn('sort');
28 | });
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_12_200936_modify_commission_status_in_v2_order_table.php:
--------------------------------------------------------------------------------
1 | where('commission_status', null)->update([
16 | 'commission_status' => 0
17 | ]);
18 | Schema::table('v2_order', function (Blueprint $table) {
19 | $table->integer('commission_status')->default(value: 0)->comment('0待确认1发放中2有效3无效')->change();
20 | });
21 |
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down(): void
28 | {
29 | Schema::table('v2_order', function (Blueprint $table) {
30 | $table->integer('commission_status')->nullable()->comment('0待确认1发放中2有效3无效')->change();
31 | });
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_13_000000_convert_order_period_fields.php:
--------------------------------------------------------------------------------
1 | 'monthly',
13 | 'quarter_price' => 'quarterly',
14 | 'half_year_price' => 'half_yearly',
15 | 'year_price' => 'yearly',
16 | 'two_year_price' => 'two_yearly',
17 | 'three_year_price' => 'three_yearly',
18 | 'onetime_price' => 'onetime',
19 | 'reset_price' => 'reset_traffic'
20 | ];
21 |
22 | /**
23 | * Run the migrations.
24 | */
25 | public function up(): void
26 | {
27 | // 批量更新订单的周期字段
28 | foreach (self::PERIOD_MAPPING as $oldPeriod => $newPeriod) {
29 | DB::table('v2_order')
30 | ->where('period', $oldPeriod)
31 | ->update(['period' => $newPeriod]);
32 | }
33 |
34 | // 检查是否还有未转换的记录
35 | $unconvertedCount = DB::table('v2_order')
36 | ->whereNotIn('period', array_values(self::PERIOD_MAPPING))
37 | ->count();
38 |
39 | if ($unconvertedCount > 0) {
40 | Log::warning("Found {$unconvertedCount} orders with unconverted period values");
41 | }
42 | }
43 |
44 | /**
45 | * Reverse the migrations.
46 | */
47 | public function down(): void
48 | {
49 | // 回滚操作 - 将新的周期值转换回旧的价格字段名
50 | foreach (self::PERIOD_MAPPING as $oldPeriod => $newPeriod) {
51 | DB::table('v2_order')
52 | ->where('period', $newPeriod)
53 | ->update(['period' => $oldPeriod]);
54 | }
55 | }
56 | };
--------------------------------------------------------------------------------
/database/migrations/2025_01_16_142320_add_updated_at_index_to_v2_order_table.php:
--------------------------------------------------------------------------------
1 | index('updated_at');
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('v2_order', function (Blueprint $table) {
25 | $table->dropIndex(['updated_at']);
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_18_140511_create_plugins_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('name');
17 | $table->string('code')->unique();
18 | $table->string('version', 50);
19 | $table->boolean('is_enabled')->default(false);
20 | $table->json('config')->nullable();
21 | $table->timestamp('installed_at')->nullable();
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('v2_plugins');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call(UsersTableSeeder::class)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/docs/en/migration/config.md:
--------------------------------------------------------------------------------
1 | # Configuration Migration Guide
2 |
3 | This guide explains how to migrate configuration files from v2board to Xboard. Xboard stores configurations in the database instead of files.
4 |
5 | ### 1. Docker Compose Environment
6 |
7 | 1. Prepare configuration file:
8 | ```bash
9 | # Create config directory
10 | mkdir config
11 |
12 | # Copy old configuration file
13 | cp old-project-path/config/v2board.php config/
14 | ```
15 |
16 | 2. Modify `docker-compose.yaml`, uncomment the following line:
17 | ```yaml
18 | - ./config/v2board.php:/www/config/v2board.php
19 | ```
20 |
21 | 3. Execute migration:
22 | ```bash
23 | docker compose run -it --rm web php artisan migrateFromV2b config
24 | ```
25 |
26 | ### 2. aaPanel Environment
27 |
28 | 1. Copy configuration file:
29 | ```bash
30 | cp old-project-path/config/v2board.php config/v2board.php
31 | ```
32 |
33 | 2. Execute migration:
34 | ```bash
35 | php artisan migrateFromV2b config
36 | ```
37 |
38 | ### 3. aaPanel + Docker Environment
39 |
40 | 1. Copy configuration file:
41 | ```bash
42 | cp old-project-path/config/v2board.php config/v2board.php
43 | ```
44 |
45 | 2. Execute migration:
46 | ```bash
47 | docker compose run -it --rm web php artisan migrateFromV2b config
48 | ```
49 |
50 | ### Important Notes
51 |
52 | - After modifying the admin path, service restart is required:
53 | - Docker environment: `docker compose restart`
54 | - aaPanel environment: Restart the Octane daemon process
--------------------------------------------------------------------------------
/docs/en/migration/v2board-1.7.3.md:
--------------------------------------------------------------------------------
1 | # V2board 1.7.3 Migration Guide
2 |
3 | This guide explains how to migrate from V2board version 1.7.3 to Xboard.
4 |
5 | ### 1. Database Changes Overview
6 |
7 | - `v2_stat_order` table renamed to `v2_stat`:
8 | - `order_amount` → `order_total`
9 | - `commission_amount` → `commission_total`
10 | - New fields added:
11 | - `paid_count` (integer, nullable)
12 | - `paid_total` (integer, nullable)
13 | - `register_count` (integer, nullable)
14 | - `invite_count` (integer, nullable)
15 | - `transfer_used_total` (string(32), nullable)
16 |
17 | - New tables added:
18 | - `v2_log`
19 | - `v2_server_hysteria`
20 | - `v2_server_vless`
21 |
22 | ### 2. Prerequisites
23 |
24 | ⚠️ Please complete the basic Xboard installation first (SQLite not supported):
25 | - [Docker Compose Deployment](../installation/docker-compose.md)
26 | - [aaPanel + Docker Deployment](../installation/aapanel-docker.md)
27 | - [aaPanel Deployment](../installation/aapanel.md)
28 |
29 | ### 3. Migration Steps
30 |
31 | #### Docker Environment
32 |
33 | ```bash
34 | # 1. Stop services
35 | docker compose down
36 |
37 | # 2. Clear database
38 | docker compose run -it --rm web php artisan db:wipe
39 |
40 | # 3. Import old database (Important)
41 | # Please manually import the V2board 1.7.3 database
42 |
43 | # 4. Execute migration
44 | docker compose run -it --rm web php artisan migratefromv2b 1.7.3
45 | ```
46 |
47 | #### aaPanel Environment
48 |
49 | ```bash
50 | # 1. Clear database
51 | php artisan db:wipe
52 |
53 | # 2. Import old database (Important)
54 | # Please manually import the V2board 1.7.3 database
55 |
56 | # 3. Execute migration
57 | php artisan migratefromv2b 1.7.3
58 | ```
59 |
60 | ### 4. Configuration Migration
61 |
62 | After completing the data migration, you need to migrate the configuration file:
63 | - [Configuration Migration Guide](./config.md)
--------------------------------------------------------------------------------
/docs/en/migration/v2board-1.7.4.md:
--------------------------------------------------------------------------------
1 | # V2board 1.7.4 Migration Guide
2 |
3 | This guide explains how to migrate from V2board version 1.7.4 to Xboard.
4 |
5 | ### 1. Database Changes Overview
6 |
7 | - New table added:
8 | - `v2_server_vless`
9 |
10 | ### 2. Prerequisites
11 |
12 | ⚠️ Please complete the basic Xboard installation first (SQLite not supported):
13 | - [Docker Compose Deployment](../installation/docker-compose.md)
14 | - [aaPanel + Docker Deployment](../installation/aapanel-docker.md)
15 | - [aaPanel Deployment](../installation/aapanel.md)
16 |
17 | ### 3. Migration Steps
18 |
19 | #### Docker Environment
20 |
21 | ```bash
22 | # 1. Stop services
23 | docker compose down
24 |
25 | # 2. Clear database
26 | docker compose run -it --rm web php artisan db:wipe
27 |
28 | # 3. Import old database (Important)
29 | # Please manually import the V2board 1.7.4 database
30 |
31 | # 4. Execute migration
32 | docker compose run -it --rm web php artisan migratefromv2b 1.7.4
33 | ```
34 |
35 | #### aaPanel Environment
36 |
37 | ```bash
38 | # 1. Clear database
39 | php artisan db:wipe
40 |
41 | # 2. Import old database (Important)
42 | # Please manually import the V2board 1.7.4 database
43 |
44 | # 3. Execute migration
45 | php artisan migratefromv2b 1.7.4
46 | ```
47 |
48 | ### 4. Configuration Migration
49 |
50 | After completing the data migration, you need to migrate the configuration file:
51 | - [Configuration Migration Guide](./config.md)
--------------------------------------------------------------------------------
/docs/en/migration/v2board-dev.md:
--------------------------------------------------------------------------------
1 | # V2board Dev Migration Guide
2 |
3 | This guide explains how to migrate from V2board Dev version (2023/10/27) to Xboard.
4 |
5 | ⚠️ Please upgrade to version 2023/10/27 following the official guide before proceeding with migration.
6 |
7 | ### 1. Database Changes Overview
8 |
9 | - `v2_order` table:
10 | - Added `surplus_order_ids` (text, nullable) - Deduction orders
11 |
12 | - `v2_plan` table:
13 | - Removed `daily_unit_price` - Affects period value
14 | - Removed `transfer_unit_price` - Affects traffic value
15 |
16 | - `v2_server_hysteria` table:
17 | - Removed `ignore_client_bandwidth` - Affects bandwidth configuration
18 | - Removed `obfs_type` - Affects obfuscation type configuration
19 |
20 | ### 2. Prerequisites
21 |
22 | ⚠️ Please complete the basic Xboard installation first (SQLite not supported):
23 | - [Docker Compose Deployment](../installation/docker-compose.md)
24 | - [aaPanel + Docker Deployment](../installation/aapanel-docker.md)
25 | - [aaPanel Deployment](../installation/aapanel.md)
26 |
27 | ### 3. Migration Steps
28 |
29 | #### Docker Environment
30 |
31 | ```bash
32 | # 1. Stop services
33 | docker compose down
34 |
35 | # 2. Clear database
36 | docker compose run -it --rm web php artisan db:wipe
37 |
38 | # 3. Import old database (Important)
39 | # Please manually import the V2board Dev database
40 |
41 | # 4. Execute migration
42 | docker compose run -it --rm web php artisan migratefromv2b dev231027
43 | ```
44 |
45 | #### aaPanel Environment
46 |
47 | ```bash
48 | # 1. Clear database
49 | php artisan db:wipe
50 |
51 | # 2. Import old database (Important)
52 | # Please manually import the V2board Dev database
53 |
54 | # 3. Execute migration
55 | php artisan migratefromv2b dev231027
56 | ```
57 |
58 | ### 4. Configuration Migration
59 |
60 | After completing the data migration, you need to migrate the configuration file:
61 | - [Configuration Migration Guide](./config.md)
--------------------------------------------------------------------------------
/docs/en/migration/v2board-wyx2685.md:
--------------------------------------------------------------------------------
1 | # V2board wyx2685 Migration Guide
2 |
3 | This guide explains how to migrate from V2board wyx2685 version (2023/11/17) to Xboard.
4 |
5 | ⚠️ Important migration notes:
6 | - Device limitation feature will be lost
7 | - Special Trojan features will be lost
8 | - Hysteria2 routes need to be reconfigured
9 |
10 | ### 1. Database Changes Overview
11 |
12 | - `v2_plan` table:
13 | - Removed `device_limit` (nullable)
14 |
15 | - `v2_server_hysteria` table:
16 | - Removed `version`
17 | - Removed `obfs`
18 | - Removed `obfs_password`
19 |
20 | - `v2_server_trojan` table:
21 | - Removed `network`
22 | - Removed `network_settings`
23 |
24 | - `v2_user` table:
25 | - Removed `device_limit`
26 |
27 | ### 2. Prerequisites
28 |
29 | ⚠️ Please complete the basic Xboard installation first (SQLite not supported):
30 | - [Docker Compose Deployment](../installation/docker-compose.md)
31 | - [aaPanel + Docker Deployment](../installation/aapanel-docker.md)
32 | - [aaPanel Deployment](../installation/aapanel.md)
33 |
34 | ### 3. Migration Steps
35 |
36 | #### Docker Environment
37 |
38 | ```bash
39 | # 1. Stop services
40 | docker compose down
41 |
42 | # 2. Clear database
43 | docker compose run -it --rm web php artisan db:wipe
44 |
45 | # 3. Import old database (Important)
46 | # Please manually import the V2board wyx2685 database
47 |
48 | # 4. Execute migration
49 | docker compose run -it --rm web php artisan migratefromv2b wyx2685
50 | ```
51 |
52 | #### aaPanel Environment
53 |
54 | ```bash
55 | # 1. Clear database
56 | php artisan db:wipe
57 |
58 | # 2. Import old database (Important)
59 | # Please manually import the V2board wyx2685 database
60 |
61 | # 3. Execute migration
62 | php artisan migratefromv2b wyx2685
63 | ```
64 |
65 | ### 4. Configuration Migration
66 |
67 | After completing the data migration, you need to migrate the configuration file:
68 | - [Configuration Migration Guide](./config.md)
--------------------------------------------------------------------------------
/docs/images/admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/docs/images/admin.png
--------------------------------------------------------------------------------
/docs/images/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/docs/images/user.png
--------------------------------------------------------------------------------
/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm -rf composer.phar
4 | wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
5 | php composer.phar install -vvv
6 | php artisan xboard:install
7 |
8 | if [ -f "/etc/init.d/bt" ] || [ -f "/.dockerenv" ]; then
9 | chown -R www:www $(pwd);
10 | fi
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "chokidar": "^4.0.3"
4 | }
5 | }
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - vendor/larastan/larastan/extension.neon
3 | - vendor/nesbot/carbon/extension.neon
4 |
5 | parameters:
6 |
7 | paths:
8 | - app/
9 |
10 | # Level 10 is the highest level
11 | level: 5
12 |
13 | ignoreErrors:
14 | - '#Negated boolean expression is always false\.#'
15 |
16 | # excludePaths:
17 | # - ./*/*/FileToBeExcluded.php
--------------------------------------------------------------------------------
/plugins/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 |
10 | define('LARAVEL_START', microtime(true));
11 |
12 | /*
13 | |--------------------------------------------------------------------------
14 | | Register The Auto Loader
15 | |--------------------------------------------------------------------------
16 | |
17 | | Composer provides a convenient, automatically generated class loader for
18 | | our application. We just need to utilize it! We'll simply require it
19 | | into the script here so that we don't have to worry about manual
20 | | loading any of our classes later on. It feels great to relax.
21 | |
22 | */
23 |
24 | require __DIR__.'/../vendor/autoload.php';
25 |
26 | /*
27 | |--------------------------------------------------------------------------
28 | | Turn On The Lights
29 | |--------------------------------------------------------------------------
30 | |
31 | | We need to illuminate PHP development, so let us turn on the lights.
32 | | This bootstraps the framework and gets it ready for use, then it
33 | | will load up this application so that we can run it and send
34 | | the responses back to the browser and delight our users.
35 | |
36 | */
37 |
38 | $app = require_once __DIR__.'/../bootstrap/app.php';
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Run The Application
43 | |--------------------------------------------------------------------------
44 | |
45 | | Once we have the application, we can handle the incoming request
46 | | through the kernel, and send the associated response back to
47 | | the client's browser allowing them to enjoy the creative
48 | | and wonderful application we have prepared for them.
49 | |
50 | */
51 |
52 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
53 |
54 | $response = $kernel->handle(
55 | $request = Illuminate\Http\Request::capture()
56 | );
57 |
58 | $response->send();
59 |
60 | $kernel->terminate($request, $response);
61 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/public/theme/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/public/web.config:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | require('./bootstrap');
2 |
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | window._ = require('lodash');
2 |
3 | /**
4 | * We'll load the axios HTTP library which allows us to easily issue requests
5 | * to our Laravel back-end. This library automatically handles sending the
6 | * CSRF token as a header based on the value of the "XSRF" token cookie.
7 | */
8 |
9 | window.axios = require('axios');
10 |
11 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
12 |
13 | /**
14 | * Echo exposes an expressive API for subscribing to channels and listening
15 | * for events that are broadcast by Laravel. Echo and event broadcasting
16 | * allows your team to easily build robust real-time web applications.
17 | */
18 |
19 | // import Echo from 'laravel-echo';
20 |
21 | // window.Pusher = require('pusher-js');
22 |
23 | // window.Echo = new Echo({
24 | // broadcaster: 'pusher',
25 | // key: process.env.MIX_PUSHER_APP_KEY,
26 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER,
27 | // encrypted: true
28 | // });
29 |
--------------------------------------------------------------------------------
/resources/rules/.gitignore:
--------------------------------------------------------------------------------
1 | custom.*
2 |
--------------------------------------------------------------------------------
/resources/sass/app.scss:
--------------------------------------------------------------------------------
1 | //
2 |
--------------------------------------------------------------------------------
/resources/views/admin.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ $title }}
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/resources/views/errors/500.blade.php:
--------------------------------------------------------------------------------
1 | @extends('errors::minimal')
2 |
3 | @section('title', __('Server Error'))
4 | @section('code', '500')
5 | @section('message', __($exception->getMessage() ?: 'Server Error'))
6 |
--------------------------------------------------------------------------------
/resources/views/mail/default/notify.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{$name}} |
11 |
12 |
13 |
14 |
15 | 网站通知 |
16 |
17 |
18 |
19 | 尊敬的用户您好!
20 |
21 |
22 | {!! nl2br($content) !!}
23 | |
24 |
25 |
26 |
27 |
28 |
29 |
30 | |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/resources/views/mail/default/remindTraffic.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{$name}} |
11 |
12 |
13 |
14 |
15 | 流量通知 |
16 |
17 |
18 |
19 | 尊敬的用户您好!
20 |
21 |
22 | 你的流量已经使用80%。为了不造成使用上的影响请合理安排流量的使用。
23 | |
24 |
25 |
26 |
27 |
28 |
29 |
30 | |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int)$id;
19 | });
20 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | })->describe('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/storage/backup/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/storage/debugbar/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/theme/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/storage/tmp/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/storage/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/theme/.gitignore:
--------------------------------------------------------------------------------
1 | /*
2 | !v2board
3 | !Xboard
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/theme/Xboard/assets/images/background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/theme/Xboard/assets/umi.js.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/Xboard/assets/umi.js.br
--------------------------------------------------------------------------------
/theme/Xboard/assets/umi.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/Xboard/assets/umi.js.gz
--------------------------------------------------------------------------------
/theme/Xboard/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Xboard",
3 | "description": "Xboard",
4 | "version": "1.0.0",
5 | "images": [
6 | "https://raw.githubusercontent.com/cedar2025/Xboard/master/docs/images/user.png"
7 | ],
8 | "configs": [
9 | {
10 | "label": "主题色",
11 | "placeholder": "请选择主题颜色",
12 | "field_name": "theme_color",
13 | "field_type": "select",
14 | "select_options": {
15 | "default": "默认(绿色)",
16 | "blue": "蓝色",
17 | "black": "黑色",
18 | "darkblue": "暗蓝色"
19 | },
20 | "default_value": "default"
21 | },
22 | {
23 | "label": "背景",
24 | "placeholder": "请输入背景图片URL",
25 | "field_name": "background_url",
26 | "field_type": "input"
27 | },
28 | {
29 | "label": "自定义页脚HTML",
30 | "placeholder": "可以实现客服JS代码的加入等",
31 | "field_name": "custom_html",
32 | "field_type": "textarea"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/theme/Xboard/dashboard.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{$title}}
8 |
9 |
10 |
11 |
12 |
13 |
36 |
37 | {!! $theme_config['custom_html'] !!}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/theme/v2board/assets/env.example.js:
--------------------------------------------------------------------------------
1 | window.settings = {
2 | // 站点标题
3 | title: 'V2Board',
4 | // 站点描述
5 | description: 'V2Board is best',
6 | // API
7 | host: '',
8 | // 主题
9 | theme: {
10 | sidebar: 'light',
11 | header: 'dark',
12 | color: 'default'
13 | },
14 | // 背景
15 | background_url: '',
16 | // crisp
17 | crisp_id: '',
18 | i18n: [
19 | 'zh-CN',
20 | 'en-US',
21 | 'ja-JP',
22 | 'vi-VN',
23 | 'ko-KR',
24 | 'zh-TW',
25 | 'fa-IR'
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Clash For Android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Clash For Android.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Clash For Windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Clash For Windows.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Clash Meta For Android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Clash Meta For Android.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Clash Verge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Clash Verge.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/ClashX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/ClashX.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Clashx Meta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Clashx Meta.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Hysteria2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/NekoBox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/NekoBox.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/QuantumultX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/QuantumultX.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Shadowrocket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Shadowrocket.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Stash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Stash.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Surfboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Surfboard.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Surge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Surge.png
--------------------------------------------------------------------------------
/theme/v2board/assets/images/icon/Vless.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/images/icon/Vless.png
--------------------------------------------------------------------------------
/theme/v2board/assets/static/Simple-Line-Icons.0cb0b9c5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/Simple-Line-Icons.0cb0b9c5.woff2
--------------------------------------------------------------------------------
/theme/v2board/assets/static/Simple-Line-Icons.78f07e2c.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/Simple-Line-Icons.78f07e2c.woff
--------------------------------------------------------------------------------
/theme/v2board/assets/static/Simple-Line-Icons.d2285965.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/Simple-Line-Icons.d2285965.ttf
--------------------------------------------------------------------------------
/theme/v2board/assets/static/Simple-Line-Icons.f33df365.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/Simple-Line-Icons.f33df365.eot
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-brands-400.14c590d1.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-brands-400.14c590d1.eot
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-brands-400.3e1b2a65.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-brands-400.3e1b2a65.woff2
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-brands-400.5e8aa9ea.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-brands-400.5e8aa9ea.ttf
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-brands-400.df02c782.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-brands-400.df02c782.woff
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-regular-400.285a9d2a.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-regular-400.285a9d2a.ttf
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-regular-400.5623624d.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-regular-400.5623624d.woff
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-regular-400.aa66d0e0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-regular-400.aa66d0e0.eot
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-regular-400.ac21cac3.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-regular-400.ac21cac3.woff2
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-solid-900.3ded831d.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-solid-900.3ded831d.woff
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-solid-900.42e1fbd2.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-solid-900.42e1fbd2.eot
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-solid-900.896e20e2.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-solid-900.896e20e2.ttf
--------------------------------------------------------------------------------
/theme/v2board/assets/static/fa-solid-900.d6d8d5da.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cedar2025/Xboard/895a870dfcb6a48bfeacb1253c390d7dab9f4d96/theme/v2board/assets/static/fa-solid-900.d6d8d5da.woff2
--------------------------------------------------------------------------------
/theme/v2board/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v2board",
3 | "description": "v2board",
4 | "version": "1.7.2",
5 | "images": "https://images.unsplash.com/photo-1515405295579-ba7b45403062?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2160&q=80",
6 | "configs": [{
7 | "label": "主题色",
8 | "placeholder": "请选择主题颜色",
9 | "field_name": "theme_color",
10 | "field_type": "select",
11 | "select_options": {
12 | "default": "默认(蓝色)",
13 | "green": "奶绿色",
14 | "black": "黑色",
15 | "darkblue": "暗蓝色"
16 | },
17 | "default_value": "default"
18 | }, {
19 | "label": "背景",
20 | "placeholder": "请输入背景图片URL",
21 | "field_name": "background_url",
22 | "field_type": "input"
23 | }, {
24 | "label": "边栏风格",
25 | "placeholder": "请选择边栏风格",
26 | "field_name": "theme_sidebar",
27 | "field_type": "select",
28 | "select_options": {
29 | "light": "亮",
30 | "dark": "暗"
31 | },
32 | "default_value": "light"
33 | }, {
34 | "label": "顶部风格",
35 | "placeholder": "请选择顶部风格",
36 | "field_name": "theme_header",
37 | "field_type": "select",
38 | "select_options": {
39 | "light": "亮",
40 | "dark": "暗"
41 | },
42 | "default_value": "dark"
43 | }, {
44 | "label": "自定义页脚HTML",
45 | "placeholder": "可以实现客服JS代码的加入等",
46 | "field_name": "custom_html",
47 | "field_type": "textarea"
48 | }]
49 | }
50 |
--------------------------------------------------------------------------------
/update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ ! -d ".git" ]; then
4 | echo "Please deploy using Git."
5 | exit 1
6 | fi
7 |
8 | if ! command -v git &> /dev/null; then
9 | echo "Git is not installed! Please install git and try again."
10 | exit 1
11 | fi
12 |
13 | git config --global --add safe.directory $(pwd)
14 | git fetch --all && git reset --hard origin/master && git pull origin master
15 | rm -rf composer.lock composer.phar
16 | wget https://github.com/composer/composer/releases/latest/download/composer.phar -O composer.phar
17 | php composer.phar update -vvv
18 | php artisan xboard:update
19 |
20 | if [ -f "/etc/init.d/bt" ] || [ -f "/.dockerenv" ]; then
21 | chown -R www:www $(pwd);
22 | fi
23 |
--------------------------------------------------------------------------------