├── .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 | 39 | 40 | 41 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 |
{{$name}}
网站通知
19 | 尊敬的用户您好! 20 |
21 |
22 | {!! nl2br($content) !!} 23 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 |
返回{{$name}}
38 |
42 |
43 | -------------------------------------------------------------------------------- /resources/views/mail/default/remindTraffic.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 39 | 40 | 41 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 |
{{$name}}
流量通知
19 | 尊敬的用户您好! 20 |
21 |
22 | 你的流量已经使用80%。为了不造成使用上的影响请合理安排流量的使用。 23 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 |
返回{{$name}}
38 |
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 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------