├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── next.svg ├── next_1000.png ├── next_2000.png └── next_3000.png ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── predefine.php └── routes.php ├── composer.json ├── composer.lock ├── config ├── .config.example.php ├── appprofile.example.php ├── clients.json └── settings.json ├── db └── migrations │ ├── 2023020100-init.php │ ├── 2023021600-drop_user_token.php │ ├── 2023030500-add_paylist_invoice_id.php │ ├── 2023031700-add_user_money_log.php │ ├── 2023031701-add_paylist_gateway.php │ ├── 2023032600-online_log_per_user-ip.php │ ├── 2023050800-add_user_transfer_today.php │ ├── 2023053000-add_user_coupon_use_time.php │ ├── 2023060300-add_user_locale_and_update_data_type.php │ ├── 2023061800-update_new_shop_data_type.php │ ├── 2023063000-add_user_is_inactive.php │ ├── 2023071000-drop_temp_tables.php │ ├── 2023071600-drop_stream_media.php │ ├── 2023071700-add_user_is_shadow_banned.php │ ├── 2023072000-add_sub_log.php │ ├── 2023080900-update_user_im_type.php │ ├── 2023081800-add_user_contact_method.php │ ├── 2023082000-remove_user_expire_in.php │ ├── 2023102200-add_node_dynamic_rate.php │ ├── 2023111700-remove_node_info_status.php │ ├── 2023111800-remove_user_invite_code_at.php │ ├── 2023111801-remove_detect_ban_log_user_info.php │ ├── 2023120700-add_node_dynamic_rate.php │ ├── 2024012000-add_payback_invoice_id.php │ ├── 2024012300-remove_user_invite_num.php │ ├── 2024012700-add_hourly_usage.php │ ├── 2024021900-add_missing_indexes.php │ ├── 2024030300-update_node_ip.php │ ├── 2024031000-remove_user_forbidden.php │ ├── 2024031700-update_data_type.php │ ├── 2024040500-add_invoice_type.php │ ├── 2024041000-add_syslog.php │ ├── 2024052400-add_ann_docs_status_sort.php │ └── 2024061600-update_price_type.php ├── phpinsights.php ├── phpunit.xml ├── public ├── assets │ └── js │ │ ├── fuck.js │ │ └── fuck.min.js ├── clients │ └── .gitkeep ├── favicon.ico ├── images │ ├── alipay.svg │ ├── next-logo.svg │ ├── qq.svg │ ├── tether.svg │ └── wechat.svg ├── index.php └── theme │ └── tabler │ └── .gitkeep ├── resources ├── email │ ├── finance.tpl │ ├── footer.tpl │ ├── header.tpl │ ├── new_user.tpl │ ├── password_reset.tpl │ ├── test.tpl │ ├── traffic_report.tpl │ ├── verify_code.tpl │ └── warn.tpl ├── locale │ ├── en_US.php │ ├── ja_JP.php │ ├── zh_CN.php │ └── zh_TW.php └── views │ └── tabler │ ├── 404.tpl │ ├── 405.tpl │ ├── 500.tpl │ ├── admin │ ├── announcement │ │ ├── create.tpl │ │ ├── edit.tpl │ │ └── index.tpl │ ├── coupon.tpl │ ├── detect.tpl │ ├── docs │ │ ├── create.tpl │ │ ├── edit.tpl │ │ └── index.tpl │ ├── footer.tpl │ ├── giftcard.tpl │ ├── header.tpl │ ├── index.tpl │ ├── invoice │ │ ├── index.tpl │ │ └── view.tpl │ ├── log │ │ ├── detect.tpl │ │ ├── detect_ban.tpl │ │ ├── gateway.tpl │ │ ├── login.tpl │ │ ├── money.tpl │ │ ├── online.tpl │ │ ├── payback.tpl │ │ └── sub.tpl │ ├── node │ │ ├── create.tpl │ │ ├── edit.tpl │ │ └── index.tpl │ ├── order │ │ ├── index.tpl │ │ └── view.tpl │ ├── product │ │ ├── create.tpl │ │ ├── edit.tpl │ │ └── index.tpl │ ├── setting │ │ ├── billing.tpl │ │ ├── captcha.tpl │ │ ├── cron.tpl │ │ ├── email.tpl │ │ ├── feature.tpl │ │ ├── im.tpl │ │ ├── llm.tpl │ │ ├── ref.tpl │ │ ├── reg.tpl │ │ ├── sub.tpl │ │ └── support.tpl │ ├── syslog │ │ ├── index.tpl │ │ └── view.tpl │ ├── system.tpl │ ├── ticket │ │ ├── index.tpl │ │ └── view.tpl │ └── user │ │ ├── edit.tpl │ │ └── index.tpl │ ├── auth │ ├── login.tpl │ └── register.tpl │ ├── captcha │ ├── ajax.tpl │ ├── div.tpl │ └── js.tpl │ ├── datatable.tpl │ ├── footer.tpl │ ├── gateway │ ├── epay.tpl │ ├── f2f.tpl │ ├── paypal.tpl │ └── stripe.tpl │ ├── header.tpl │ ├── index.tpl │ ├── live_chat.tpl │ ├── password │ ├── reset.tpl │ └── token.tpl │ ├── staff.tpl │ ├── tinymce.tpl │ ├── tos.tpl │ └── user │ ├── announcement.tpl │ ├── banned.tpl │ ├── detect │ ├── index.tpl │ └── log.tpl │ ├── docs │ ├── index.tpl │ └── view.tpl │ ├── edit.tpl │ ├── footer.tpl │ ├── header.tpl │ ├── index.tpl │ ├── invite.tpl │ ├── invoice │ ├── index.tpl │ └── view.tpl │ ├── money.tpl │ ├── order │ ├── create.tpl │ ├── index.tpl │ └── view.tpl │ ├── product.tpl │ ├── profile.tpl │ ├── rate.tpl │ ├── server.tpl │ └── ticket │ ├── index.tpl │ └── view.tpl ├── src ├── Command │ ├── ClientDownload.php │ ├── Command.php │ ├── Cron.php │ ├── Migration.php │ ├── Test.php │ ├── Tool.php │ └── Update.php ├── Controllers │ ├── Admin │ │ ├── AnnController.php │ │ ├── CouponController.php │ │ ├── DetectBanLogController.php │ │ ├── DetectLogController.php │ │ ├── DetectRuleController.php │ │ ├── DocsController.php │ │ ├── GiftCardController.php │ │ ├── InvoiceController.php │ │ ├── LoginLogController.php │ │ ├── MoneyLogController.php │ │ ├── NodeController.php │ │ ├── OnlineLogController.php │ │ ├── OrderController.php │ │ ├── PaybackController.php │ │ ├── PaylistController.php │ │ ├── ProductController.php │ │ ├── Setting │ │ │ ├── BillingController.php │ │ │ ├── CaptchaController.php │ │ │ ├── CronController.php │ │ │ ├── EmailController.php │ │ │ ├── FeatureController.php │ │ │ ├── ImController.php │ │ │ ├── LlmController.php │ │ │ ├── RefController.php │ │ │ ├── RegController.php │ │ │ ├── SubController.php │ │ │ └── SupportController.php │ │ ├── SubLogController.php │ │ ├── SysLogController.php │ │ ├── SystemController.php │ │ ├── TicketController.php │ │ └── UserController.php │ ├── AdminController.php │ ├── Api │ │ ├── AdminApiV1Controller.php │ │ ├── NodeApiV1Controller.php │ │ └── UserApiV1Controller.php │ ├── AuthController.php │ ├── BaseController.php │ ├── CallbackController.php │ ├── HomeController.php │ ├── OAuthController.php │ ├── PasswordController.php │ ├── SubController.php │ ├── User │ │ ├── ClientController.php │ │ ├── CouponController.php │ │ ├── DetectLogController.php │ │ ├── DetectRuleController.php │ │ ├── DocsController.php │ │ ├── InfoController.php │ │ ├── InviteController.php │ │ ├── InvoiceController.php │ │ ├── MFAController.php │ │ ├── MoneyController.php │ │ ├── OrderController.php │ │ ├── ProductController.php │ │ ├── ProfileController.php │ │ ├── RateController.php │ │ ├── ServerController.php │ │ └── TicketController.php │ ├── UserController.php │ └── WebAPI │ │ ├── FuncController.php │ │ ├── NodeController.php │ │ └── UserController.php ├── Interfaces │ └── MigrationInterface.php ├── Middleware │ ├── Admin.php │ ├── AdminApi.php │ ├── ErrorHandler.php │ ├── Guest.php │ ├── NodeApi.php │ ├── NodeToken.php │ ├── User.php │ └── UserApi.php ├── Models │ ├── Ann.php │ ├── Config.php │ ├── DetectBanLog.php │ ├── DetectLog.php │ ├── DetectRule.php │ ├── Docs.php │ ├── EmailQueue.php │ ├── GiftCard.php │ ├── HourlyUsage.php │ ├── InviteCode.php │ ├── Invoice.php │ ├── Link.php │ ├── LoginIp.php │ ├── Model.php │ ├── Node.php │ ├── OnlineLog.php │ ├── Order.php │ ├── Payback.php │ ├── Paylist.php │ ├── Product.php │ ├── SubscribeLog.php │ ├── SysLog.php │ ├── Ticket.php │ ├── User.php │ ├── UserCoupon.php │ └── UserMoneyLog.php ├── Services │ ├── Analytics.php │ ├── Auth.php │ ├── Auth │ │ ├── Base.php │ │ └── Cookie.php │ ├── Boot.php │ ├── Bot │ │ ├── Discord │ │ │ └── .gitkeep │ │ ├── Slack │ │ │ └── .gitkeep │ │ └── Telegram │ │ │ ├── Callback.php │ │ │ ├── Commands │ │ │ ├── CheckinCommand.php │ │ │ ├── DcCommand.php │ │ │ ├── HelpCommand.php │ │ │ ├── MenuCommand.php │ │ │ ├── MyCommand.php │ │ │ ├── PingCommand.php │ │ │ ├── StartCommand.php │ │ │ └── UnbindCommand.php │ │ │ ├── Message.php │ │ │ └── Telegram.php │ ├── Cache.php │ ├── Captcha.php │ ├── Cloudflare.php │ ├── Cron.php │ ├── DB.php │ ├── Detect.php │ ├── DynamicRate.php │ ├── Exchange.php │ ├── Factory.php │ ├── Filter.php │ ├── Gateway │ │ ├── AlipayF2F.php │ │ ├── Base.php │ │ ├── Epay.php │ │ ├── Epay │ │ │ ├── EpayNotify.php │ │ │ ├── EpaySubmit.php │ │ │ └── EpayTool.php │ │ ├── PayPal.php │ │ └── Stripe.php │ ├── GeoIP2.php │ ├── I18n.php │ ├── IM.php │ ├── IM │ │ ├── Base.php │ │ ├── Discord.php │ │ ├── Slack.php │ │ └── Telegram.php │ ├── LLM.php │ ├── LLM │ │ ├── Anthropic.php │ │ ├── AwsBedrock.php │ │ ├── Base.php │ │ ├── CloudflareWorkersAI.php │ │ ├── GoogleAI.php │ │ ├── HuggingFace.php │ │ ├── OpenAI.php │ │ └── VertexAI.php │ ├── MFA.php │ ├── Mail.php │ ├── Mail │ │ ├── AlibabaCloud.php │ │ ├── Base.php │ │ ├── Mailchimp.php │ │ ├── Mailgun.php │ │ ├── NullMail.php │ │ ├── Postal.php │ │ ├── Resend.php │ │ ├── SendGrid.php │ │ ├── Ses.php │ │ └── Smtp.php │ ├── Notification.php │ ├── Password.php │ ├── Payment.php │ ├── RateLimit.php │ ├── Reward.php │ ├── Subscribe.php │ ├── Subscribe │ │ ├── Base.php │ │ ├── Clash.php │ │ ├── Json.php │ │ ├── SIP002.php │ │ ├── SIP008.php │ │ ├── SS.php │ │ ├── SingBox.php │ │ ├── Trojan.php │ │ ├── V2Ray.php │ │ └── V2RayJson.php │ ├── SysLog │ │ └── DBHandler.php │ └── View.php └── Utils │ ├── ClassHelper.php │ ├── Cookie.php │ ├── Hash.php │ ├── ResponseHelper.php │ └── Tools.php ├── storage └── framework │ ├── smarty │ ├── cache │ │ └── .gitkeep │ └── compile │ │ └── .gitkeep │ └── twig │ └── cache │ └── .gitkeep ├── tests ├── App │ ├── Services │ │ ├── CacheTest.php │ │ ├── DBTest.php │ │ ├── DynamicRateTest.php │ │ ├── FilterTest.php │ │ ├── I18nTest.php │ │ ├── MFATest.php │ │ └── ViewTest.php │ └── Utils │ │ ├── ClassHelperTest.php │ │ ├── CookieTest.php │ │ ├── HashTest.php │ │ ├── ResponseHelperTest.php │ │ └── ToolsTest.php └── testDir │ ├── emptyDir │ └── .gitkeep │ ├── file1 │ ├── file2 │ └── file3 ├── update.sh └── xcat /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: "[BUG] 在这里填入你所遇到问题的概述" 5 | labels: bug-user-report 6 | 7 | --- 8 | 9 | **Environment 环境** 10 | 11 | OS: 12 | HTTPS enabled: 13 | PHP version: 14 | DB version: 15 | Redis version: 16 | Commit: 17 | 18 | **Bug Info** 19 | 20 | Describe the issue you run into. 请描述你遇到的问题。 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new idea for this project 4 | title: "[Feature Request] 在这里填入你所希望添加功能的概述" 5 | labels: feature-request 6 | 7 | --- 8 | 9 | **Feature 功能请求** 10 | 11 | Describe the feature you want. 请描述你想要的功能。 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/next_1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/.github/next_1000.png -------------------------------------------------------------------------------- /.github/next_2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/.github/next_2000.png -------------------------------------------------------------------------------- /.github/next_3000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/.github/next_3000.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea 3 | .htaccess 4 | */.DS_Store 5 | .DS_Store 6 | .env 7 | 8 | vendor 9 | 10 | .vs/ 11 | .vscode 12 | .editorconfig 13 | 14 | .stylelintrc.json 15 | .stylelintignore 16 | 17 | php_errors.log 18 | composer.phar 19 | 20 | 404.html 21 | index.html 22 | 23 | config/.config.php 24 | config/.config.php.bak 25 | config/appprofile.php 26 | 27 | public/clients/*.* 28 | !public/clients/.gitkeep 29 | 30 | storage/framework/smarty/cache/* 31 | storage/framework/smarty/compile/* 32 | storage/framework/twig/cache/* 33 | !storage/framework/smarty/cache/.gitkeep 34 | !storage/framework/smarty/compile/.gitkeep 35 | !storage/framework/twig/cache/.gitkeep 36 | 37 | .user.ini 38 | public/.user.ini 39 | 40 | .envrc.local 41 | .envrc 42 | flake.lock 43 | flake.nix 44 | .direnv/ 45 | 46 | node_modules 47 | package.json 48 | package-lock.json 49 | 50 | *.cache 51 | 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Contact cat@sspanel.org or use Github Security Advisories feature to report a vulnerability, do not use issue tracker. 2 | -------------------------------------------------------------------------------- /app/predefine.php: -------------------------------------------------------------------------------- 1 | exec('DROP TABLE IF EXISTS `user_token`'); 12 | 13 | return 2023021600; 14 | } 15 | 16 | public function down(): int 17 | { 18 | DB::getPdo()->exec( 19 | 'CREATE TABLE IF NOT EXISTS `user_token` ( 20 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 21 | `token` varchar(255) DEFAULT NULL, 22 | `user_id` bigint(20) unsigned DEFAULT NULL, 23 | `create_time` bigint(20) unsigned DEFAULT NULL, 24 | `expire_time` bigint(20) DEFAULT NULL, 25 | PRIMARY KEY (`id`), 26 | KEY `user_id` (`user_id`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;' 28 | ); 29 | 30 | return 2023020100; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /db/migrations/2023030500-add_paylist_invoice_id.php: -------------------------------------------------------------------------------- 1 | exec('ALTER TABLE paylist ADD COLUMN IF NOT EXISTS `invoice_id` int(11) DEFAULT 0;'); 12 | 13 | return 2023030500; 14 | } 15 | 16 | public function down(): int 17 | { 18 | DB::getPdo()->exec('ALTER TABLE paylist DROP COLUMN IF EXISTS `invoice_id`;'); 19 | 20 | return 2023021600; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /db/migrations/2023031700-add_user_money_log.php: -------------------------------------------------------------------------------- 1 | exec("CREATE TABLE IF NOT EXISTS `user_money_log` ( 12 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 13 | `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '用户ID', 14 | `before` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '用户变动前账户余额', 15 | `after` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '用户变动后账户余额', 16 | `amount` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '变动总额', 17 | `remark` text NOT NULL DEFAULT '' COMMENT '备注', 18 | `create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间', 19 | PRIMARY KEY (`id`), 20 | KEY `user_id` (`user_id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"); 22 | 23 | return 2023031700; 24 | } 25 | 26 | public function down(): int 27 | { 28 | DB::getPdo()->exec('DROP TABLE IF EXISTS `user_money_log`;'); 29 | 30 | return 2023030500; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /db/migrations/2023031701-add_paylist_gateway.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE paylist ADD COLUMN IF NOT EXISTS `gateway` varchar(255) NOT NULL DEFAULT '';"); 12 | 13 | return 2023031701; 14 | } 15 | 16 | public function down(): int 17 | { 18 | DB::getPdo()->exec('ALTER TABLE paylist DROP COLUMN IF EXISTS `gateway`;'); 19 | 20 | return 2023031700; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /db/migrations/2023032600-online_log_per_user-ip.php: -------------------------------------------------------------------------------- 1 | exec(' 13 | CREATE TABLE IF NOT EXISTS online_log ( 14 | id INT UNSIGNED NOT NULL AUTO_INCREMENT, 15 | user_id INT UNSIGNED NOT NULL, 16 | ip INET6 NOT NULL, 17 | node_id INT UNSIGNED NOT NULL, 18 | first_time INT UNSIGNED NOT NULL, 19 | last_time INT UNSIGNED NOT NULL, 20 | PRIMARY KEY (id), 21 | UNIQUE KEY (user_id, ip), 22 | KEY (last_time) 23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 24 | '); 25 | 26 | $pdo->exec('DROP TABLE IF EXISTS alive_ip'); 27 | 28 | return 2023032600; 29 | } 30 | 31 | public function down(): int 32 | { 33 | $pdo = DB::getPdo(); 34 | $pdo->exec(' 35 | CREATE TABLE IF NOT EXISTS alive_ip ( 36 | id BIGINT(20) NOT NULL AUTO_INCREMENT, 37 | nodeid INT(11) DEFAULT NULL, 38 | userid INT(11) DEFAULT NULL, 39 | ip VARCHAR(255) DEFAULT NULL, 40 | datetime BIGINT(20) DEFAULT NULL, 41 | PRIMARY KEY (id) 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 43 | '); 44 | 45 | $pdo->exec('DROP TABLE IF EXISTS online_log'); 46 | 47 | return 2023031701; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /db/migrations/2023050800-add_user_transfer_today.php: -------------------------------------------------------------------------------- 1 | exec('ALTER TABLE user DROP COLUMN IF EXISTS `last_day_t`;'); 12 | DB::getPdo()->exec("ALTER TABLE user ADD COLUMN IF NOT EXISTS `transfer_today` bigint(20) unsigned DEFAULT 0 COMMENT '账户今日所用流量';"); 13 | 14 | return 2023050800; 15 | } 16 | 17 | public function down(): int 18 | { 19 | DB::getPdo()->exec("ALTER TABLE user ADD COLUMN IF NOT EXISTS `last_day_t` bigint(20) DEFAULT 0 COMMENT '今天之前已使用的流量';"); 20 | DB::getPdo()->exec('ALTER TABLE user DROP COLUMN IF EXISTS `transfer_today`;'); 21 | 22 | return 2023032600; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /db/migrations/2023053000-add_user_coupon_use_time.php: -------------------------------------------------------------------------------- 1 | exec("ALTER TABLE user_coupon ADD COLUMN IF NOT EXISTS `use_count` int(11) NOT NULL DEFAULT 0 COMMENT '累计使用数';"); 12 | 13 | return 2023053000; 14 | } 15 | 16 | public function down(): int 17 | { 18 | DB::getPdo()->exec('ALTER TABLE user_coupon DROP COLUMN IF EXISTS `use_count`;'); 19 | 20 | return 2023050800; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /db/migrations/2023063000-add_user_is_inactive.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE user DROP COLUMN IF EXISTS `t`; 13 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `is_inactive` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否处于闲置状态'; 14 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `last_use_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '最后使用时间'; 15 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `last_login_time` int(11) unsigned DEFAULT 0 COMMENT '最后登录时间'; 16 | ALTER TABLE user ADD KEY IF NOT EXISTS `is_inactive` (`is_inactive`); 17 | "); 18 | 19 | return 2023063000; 20 | } 21 | 22 | public function down(): int 23 | { 24 | DB::getPdo()->exec(" 25 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `t` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '最后使用时间'; 26 | ALTER TABLE user DROP COLUMN IF EXISTS `is_inactive`; 27 | ALTER TABLE user DROP COLUMN IF EXISTS `last_use_time`; 28 | ALTER TABLE user DROP COLUMN IF EXISTS `last_login_time`; 29 | ALTER TABLE user DROP KEY IF EXISTS `is_inactive`; 30 | "); 31 | 32 | return 2023061800; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /db/migrations/2023071600-drop_stream_media.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | DROP TABLE IF EXISTS `stream_media`; 13 | '); 14 | 15 | return 2023071600; 16 | } 17 | 18 | public function down(): int 19 | { 20 | DB::getPdo()->exec( 21 | "CREATE TABLE IF NOT EXISTS `stream_media` ( 22 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID', 23 | `node_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '节点ID', 24 | `result` text NOT NULL DEFAULT '' COMMENT '检测结果', 25 | `created_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间', 26 | PRIMARY KEY (`id`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;" 28 | ); 29 | 30 | return 2023071000; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /db/migrations/2023071700-add_user_is_shadow_banned.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `is_shadow_banned` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否处于账户异常状态'; 13 | ALTER TABLE user MODIFY COLUMN `is_dark_mode` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否启用暗黑模式'; 14 | ALTER TABLE user MODIFY COLUMN `is_inactive` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否处于闲置状态'; 15 | ALTER TABLE user DROP COLUMN IF EXISTS `use_new_shop`; 16 | "); 17 | 18 | return 2023071700; 19 | } 20 | 21 | public function down(): int 22 | { 23 | DB::getPdo()->exec(" 24 | ALTER TABLE user DROP COLUMN IF EXISTS `is_shadow_banned`; 25 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `use_new_shop` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT '是否启用新商店', 26 | "); 27 | 28 | return 2023071600; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /db/migrations/2023080900-update_user_im_type.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE user MODIFY COLUMN `im_type` smallint(6) unsigned NOT NULL DEFAULT 0 COMMENT '联系方式类型'; 13 | UPDATE user SET `im_value` = '' WHERE `im_value` = '0'; 14 | "); 15 | 16 | return 2023080900; 17 | } 18 | 19 | public function down(): int 20 | { 21 | DB::getPdo()->exec(" 22 | ALTER TABLE user MODIFY COLUMN `im_type` smallint(6) unsigned NOT NULL DEFAULT 1 COMMENT '联系方式类型'; 23 | "); 24 | 25 | return 2023072000; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /db/migrations/2023081800-add_user_contact_method.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `contact_method` smallint(6) NOT NULL DEFAULT 1 COMMENT '偏好的联系方式'; 13 | "); 14 | 15 | return 2023081800; 16 | } 17 | 18 | public function down(): int 19 | { 20 | DB::getPdo()->exec(' 21 | ALTER TABLE user DROP COLUMN IF EXISTS `contact_method`; 22 | '); 23 | 24 | return 2023080900; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /db/migrations/2023082000-remove_user_expire_in.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | ALTER TABLE user DROP COLUMN IF EXISTS `expire_in`; 13 | '); 14 | 15 | return 2023082000; 16 | } 17 | 18 | public function down(): int 19 | { 20 | DB::getPdo()->exec(" 21 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `expire_in` datetime NOT NULL DEFAULT '2199-01-01 00:00:00' COMMENT '账户过期时间'; 22 | "); 23 | 24 | return 2023081800; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /db/migrations/2023102200-add_node_dynamic_rate.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `is_dynamic_rate` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否启用动态流量倍率'; 13 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `dynamic_rate_config` longtext NOT NULL DEFAULT '{}' COMMENT '动态流量倍率配置' CHECK (json_valid(`custom_config`)); 14 | "); 15 | 16 | return 2023102200; 17 | } 18 | 19 | public function down(): int 20 | { 21 | DB::getPdo()->exec(' 22 | ALTER TABLE node DROP COLUMN IF EXISTS `is_dynamic_rate`; 23 | ALTER TABLE node DROP COLUMN IF EXISTS `dynamic_rate_config`; 24 | '); 25 | 26 | return 2023082000; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /db/migrations/2023111700-remove_node_info_status.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | ALTER TABLE node DROP COLUMN IF EXISTS `info`; 13 | ALTER TABLE node DROP COLUMN IF EXISTS `status`; 14 | '); 15 | 16 | return 2023111700; 17 | } 18 | 19 | public function down(): int 20 | { 21 | DB::getPdo()->exec(" 22 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `info` varchar(255) NOT NULL DEFAULT '' COMMENT '节点信息'; 23 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `status` varchar(255) NOT NULL DEFAULT '' COMMENT '节点状态'; 24 | "); 25 | 26 | return 2023102200; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /db/migrations/2023111800-remove_user_invite_code_at.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | ALTER TABLE user_invite_code DROP COLUMN IF EXISTS `created_at`; 13 | ALTER TABLE user_invite_code DROP COLUMN IF EXISTS `updated_at`; 14 | '); 15 | 16 | return 2023111800; 17 | } 18 | 19 | public function down(): int 20 | { 21 | DB::getPdo()->exec(" 22 | ALTER TABLE user_invite_code ADD COLUMN IF NOT EXISTS `created_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '创建时间'; 23 | ALTER TABLE user_invite_code ADD COLUMN IF NOT EXISTS `updated_at` timestamp NOT NULL DEFAULT '1989-06-04 00:05:00' COMMENT '更新时间'; 24 | "); 25 | 26 | return 2023111700; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /db/migrations/2023111801-remove_detect_ban_log_user_info.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | ALTER TABLE detect_ban_log DROP COLUMN IF EXISTS `user_name`; 13 | ALTER TABLE detect_ban_log DROP COLUMN IF EXISTS `email`; 14 | '); 15 | 16 | return 2023111801; 17 | } 18 | 19 | public function down(): int 20 | { 21 | DB::getPdo()->exec(" 22 | ALTER TABLE detect_ban_log ADD COLUMN IF NOT EXISTS `user_name` varchar(255) NOT NULL DEFAULT '' COMMENT '用户名'; 23 | ALTER TABLE detect_ban_log ADD COLUMN IF NOT EXISTS `email` varchar(255) NOT NULL DEFAULT '' COMMENT '用户邮箱'; 24 | "); 25 | 26 | return 2023111800; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /db/migrations/2023120700-add_node_dynamic_rate.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `dynamic_rate_type` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '动态流量倍率计算方式'; 13 | "); 14 | 15 | return 2023120700; 16 | } 17 | 18 | public function down(): int 19 | { 20 | DB::getPdo()->exec(' 21 | ALTER TABLE node DROP COLUMN IF EXISTS `dynamic_rate_type`; 22 | '); 23 | 24 | return 2023111801; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /db/migrations/2024012000-add_payback_invoice_id.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE payback ADD COLUMN IF NOT EXISTS `invoice_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '账单ID'; 13 | "); 14 | 15 | return 2024012000; 16 | } 17 | 18 | public function down(): int 19 | { 20 | DB::getPdo()->exec(' 21 | ALTER TABLE payback DROP COLUMN IF EXISTS `invoice_id`; 22 | '); 23 | 24 | return 2023120700; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /db/migrations/2024012300-remove_user_invite_num.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | ALTER TABLE user DROP COLUMN IF EXISTS `invite_num`; 13 | '); 14 | 15 | return 2024012300; 16 | } 17 | 18 | public function down(): int 19 | { 20 | DB::getPdo()->exec(" 21 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `invite_num` int(11) NOT NULL DEFAULT 0 COMMENT '可用邀请次数'; 22 | "); 23 | 24 | return 2024012000; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /db/migrations/2024012700-add_hourly_usage.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | CREATE TABLE IF NOT EXISTS `hourly_usage` ( 13 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID', 14 | `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '用户ID', 15 | `date` date NOT NULL DEFAULT 0 COMMENT '记录日期', 16 | `usage` longtext NOT NULL DEFAULT '{}' COMMENT '流量用量' CHECK (json_valid(`usage`)), 17 | PRIMARY KEY (`id`), 18 | KEY `user_id` (`user_id`) 19 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 20 | DROP TABLE IF EXISTS `user_hourly_usage`; 21 | "); 22 | 23 | return 2024012700; 24 | } 25 | 26 | public function down(): int 27 | { 28 | DB::getPdo()->exec(" 29 | CREATE TABLE IF NOT EXISTS `user_hourly_usage` ( 30 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID', 31 | `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '用户ID', 32 | `traffic` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '当前总流量', 33 | `hourly_usage` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '过去一小时流量', 34 | `datetime` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '记录时间', 35 | PRIMARY KEY (`id`), 36 | KEY `user_id` (`user_id`) 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 38 | DROP TABLE IF EXISTS `hourly_usage`; 39 | "); 40 | 41 | return 2024012300; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /db/migrations/2024021900-add_missing_indexes.php: -------------------------------------------------------------------------------- 1 | exec(' 12 | ALTER TABLE config ADD KEY IF NOT EXISTS `item` (`item`); 13 | ALTER TABLE config ADD KEY IF NOT EXISTS `class` (`class`); 14 | ALTER TABLE config ADD KEY IF NOT EXISTS `is_public` (`is_public`); 15 | ALTER TABLE hourly_usage ADD KEY IF NOT EXISTS `date` (`date`); 16 | ALTER TABLE node ADD UNIQUE KEY IF NOT EXISTS `password` (`password`); 17 | ALTER TABLE node ADD KEY IF NOT EXISTS `is_dynamic_rate` (`is_dynamic_rate`); 18 | ALTER TABLE node ADD KEY IF NOT EXISTS `bandwidthlimit_resetday` (`bandwidthlimit_resetday`); 19 | ALTER TABLE online_log ADD KEY IF NOT EXISTS `node_id` (`node_id`); 20 | ALTER TABLE payback ADD KEY IF NOT EXISTS `userid` (`userid`); 21 | ALTER TABLE payback ADD KEY IF NOT EXISTS `ref_by` (`ref_by`); 22 | ALTER TABLE payback ADD KEY IF NOT EXISTS `invoice_id` (`invoice_id`); 23 | ALTER TABLE paylist ADD UNIQUE KEY IF NOT EXISTS `tradeno` (`tradeno`); 24 | ALTER TABLE paylist ADD KEY IF NOT EXISTS `status` (`status`); 25 | ALTER TABLE paylist ADD KEY IF NOT EXISTS `invoice_id` (`invoice_id`); 26 | ALTER TABLE subscribe_log ADD KEY IF NOT EXISTS `type` (`type`); 27 | ALTER TABLE ticket ADD KEY IF NOT EXISTS `type` (`type`); 28 | ALTER TABLE user ADD KEY IF NOT EXISTS `is_shadow_banned` (`is_shadow_banned`);'); 29 | 30 | return 2024021900; 31 | } 32 | 33 | public function down(): int 34 | { 35 | return 2024021900; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /db/migrations/2024030300-update_node_ip.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `ipv4` inet4 NOT NULL DEFAULT '127.0.0.1' COMMENT 'IPv4地址'; 13 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `ipv6` inet6 NOT NULL DEFAULT '::1' COMMENT 'IPv6地址'; 14 | ALTER TABLE node DROP COLUMN IF EXISTS `node_ip`; 15 | "); 16 | 17 | return 2024030300; 18 | } 19 | 20 | public function down(): int 21 | { 22 | DB::getPdo()->exec(" 23 | ALTER TABLE node ADD COLUMN IF NOT EXISTS `node_ip` varchar(255) NOT NULL DEFAULT '' COMMENT '节点IP'; 24 | ALTER TABLE node DROP COLUMN IF EXISTS `ipv4`; 25 | ALTER TABLE node DROP COLUMN IF EXISTS `ipv6`; 26 | "); 27 | 28 | return 2024021900; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /db/migrations/2024031000-remove_user_forbidden.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE user DROP COLUMN IF EXISTS `forbidden_ip`; 13 | ALTER TABLE user DROP COLUMN IF EXISTS `forbidden_port`; 14 | ALTER TABLE user MODIFY COLUMN `api_token` varchar(255) NOT NULL DEFAULT '' COMMENT 'API Token'; 15 | ALTER TABLE user MODIFY COLUMN `uuid` uuid NOT NULL COMMENT 'UUID'; 16 | ALTER TABLE user ADD UNIQUE KEY IF NOT EXISTS `passwd` (`passwd`); 17 | "); 18 | 19 | return 2024031000; 20 | } 21 | 22 | public function down(): int 23 | { 24 | DB::getPdo()->exec(" 25 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `forbidden_ip` varchar(255) NOT NULL DEFAULT '' COMMENT '禁止访问IP'; 26 | ALTER TABLE user ADD COLUMN IF NOT EXISTS `forbidden_port` varchar(255) NOT NULL DEFAULT '' COMMENT '禁止访问端口'; 27 | ALTER TABLE user MODIFY COLUMN `api_token` char(36) NOT NULL DEFAULT '' COMMENT 'API 密钥'; 28 | ALTER TABLE user MODIFY COLUMN `uuid` char(36) NOT NULL COMMENT 'UUID'; 29 | "); 30 | 31 | return 2024030300; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /db/migrations/2024040500-add_invoice_type.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE invoice ADD COLUMN IF NOT EXISTS `type` varchar(255) NOT NULL DEFAULT 'product' COMMENT '类型'; 13 | ALTER TABLE invoice ADD KEY IF NOT EXISTS `type` (`type`); 14 | ALTER TABLE invoice MODIFY COLUMN `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID'; 15 | ALTER TABLE invoice MODIFY COLUMN `order_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '订单ID'; 16 | ALTER TABLE invoice MODIFY COLUMN `content` longtext NOT NULL DEFAULT '{}' COMMENT '账单内容' CHECK (json_valid(`content`)); 17 | ALTER TABLE invoice MODIFY COLUMN `price` double unsigned NOT NULL DEFAULT 0 COMMENT '账单金额'; 18 | ALTER TABLE invoice MODIFY COLUMN `status` varchar(255) NOT NULL DEFAULT '' COMMENT '账单状态'; 19 | ALTER TABLE invoice MODIFY COLUMN `create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间'; 20 | ALTER TABLE invoice MODIFY COLUMN `update_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间'; 21 | ALTER TABLE invoice MODIFY COLUMN `pay_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '支付时间'; 22 | ALTER TABLE `order` ADD KEY IF NOT EXISTS `product_type` (`product_type`); 23 | ALTER TABLE node MODIFY COLUMN `traffic_rate` double unsigned NOT NULL DEFAULT 1 COMMENT '流量倍率'; 24 | "); 25 | 26 | return 2024040500; 27 | } 28 | 29 | public function down(): int 30 | { 31 | DB::getPdo()->exec(' 32 | ALTER TABLE invoice DROP COLUMN IF EXISTS `type`; 33 | ALTER TABLE invoice DROP KEY IF EXISTS `type`; 34 | '); 35 | 36 | return 2024031700; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /db/migrations/2024041000-add_syslog.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | CREATE TABLE IF NOT EXISTS `syslog` ( 13 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录ID', 14 | `user_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '触发用户', 15 | `ip` varchar(255) NOT NULL DEFAULT '' COMMENT '触发IP', 16 | `message` varchar(1024) NOT NULL DEFAULT '' COMMENT '日志内容', 17 | `level` tinyint(3) unsigned NOT NULL DEFAULT 100 COMMENT '日志等级', 18 | `context` longtext NOT NULL DEFAULT '{}' COMMENT '日志内容' CHECK (json_valid(`context`)), 19 | `channel` varchar(255) NOT NULL DEFAULT '' COMMENT '日志类别', 20 | `datetime` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '记录时间', 21 | PRIMARY KEY (`id`), 22 | KEY `user_id` (`user_id`), 23 | KEY `ip` (`ip`), 24 | KEY `message` (`message`), 25 | KEY `level` (`level`), 26 | KEY `channel` (`channel`), 27 | KEY `datetime` (`datetime`) 28 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 29 | "); 30 | 31 | return 2024041000; 32 | } 33 | 34 | public function down(): int 35 | { 36 | DB::getPdo()->exec(' 37 | DROP TABLE IF EXISTS `syslog`; 38 | '); 39 | 40 | return 2024040500; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /db/migrations/2024052400-add_ann_docs_status_sort.php: -------------------------------------------------------------------------------- 1 | exec(" 12 | ALTER TABLE announcement ADD COLUMN IF NOT EXISTS `status` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT '公告状态'; 13 | ALTER TABLE announcement ADD COLUMN IF NOT EXISTS `sort` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '公告排序'; 14 | ALTER TABLE announcement ADD KEY IF NOT EXISTS `status` (`status`); 15 | ALTER TABLE announcement ADD KEY IF NOT EXISTS `sort` (`sort`); 16 | ALTER TABLE docs ADD COLUMN IF NOT EXISTS `status` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT '文档状态'; 17 | ALTER TABLE docs ADD COLUMN IF NOT EXISTS `sort` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '文档排序'; 18 | ALTER TABLE docs ADD KEY IF NOT EXISTS `status` (`status`); 19 | ALTER TABLE docs ADD KEY IF NOT EXISTS `sort` (`sort`); 20 | "); 21 | 22 | return 2024052400; 23 | } 24 | 25 | public function down(): int 26 | { 27 | return 2024052400; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | 20 | src 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/assets/js/fuck.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){const c=b.userAgent;let d=!1;const e=function(a,c){const d=b.mimeTypes;for(const b in d)if(d[b][a]===c)return!0;return!1},f=function(a){return-1 2 | 10 | 12 | 15 | 18 | 22 | 23 | 26 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/images/next-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 35 | 38 | 39 | -------------------------------------------------------------------------------- /public/images/tether.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 12 | 18 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/images/wechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | add(new ErrorHandler()); 33 | 34 | $routes = require __DIR__ . '/../app/routes.php'; 35 | $routes($app); 36 | 37 | $request = ServerRequest::fromGlobals(); 38 | $request = new Slim\Http\ServerRequest($request); 39 | 40 | $app->run($request); 41 | -------------------------------------------------------------------------------- /public/theme/tabler/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/public/theme/tabler/.gitkeep -------------------------------------------------------------------------------- /resources/email/finance.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 | {$title} 13 |

14 |
15 |
16 |
17 |
18 |

19 | {$text} 20 |

21 |
22 |
23 |
24 |
25 | 26 | {include file='footer.tpl'} 27 | -------------------------------------------------------------------------------- /resources/email/footer.tpl: -------------------------------------------------------------------------------- 1 |
2 |
3 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/email/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 44 | 45 | -------------------------------------------------------------------------------- /resources/email/new_user.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 | 账户已经生成 13 |

14 |
15 |
16 |
17 |
18 |

19 | {$text} 20 |

21 |
22 |
23 |
24 |
25 | 26 | {include file='footer.tpl'} 27 | -------------------------------------------------------------------------------- /resources/email/password_reset.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |

13 | 密码重置 14 |

15 |
16 |
17 |
18 |
19 |

20 | 你收到此邮件是因为你在 {$config['appName']} 系统申请了密码重置,如果非本人申请,请忽略此邮件。 21 |

22 | 点击此链接重置密码 23 |

24 |
25 |
26 |
27 |
28 | 29 | {include file='footer.tpl'} 30 | -------------------------------------------------------------------------------- /resources/email/test.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 | 邮件发送测试 13 |

14 |
15 |
16 |
17 |
18 |

19 | 这是一封测试邮件。如果你能收到,说明邮件发送配置有效,可以正常工作。 20 |

21 |
22 |
23 |
24 |
25 | 26 | {include file='footer.tpl'} 27 | -------------------------------------------------------------------------------- /resources/email/verify_code.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 | 邮箱验证 13 |

14 |
15 |
16 |
17 |
18 |

19 | 你请求的邮箱验证代码为:{$code}
20 | 本验证代码在 {$expire} 前有效。
21 | 如果此验证码非你本人申请,请忽视此邮件。
22 |

23 |
24 |
25 |
26 |
27 | 28 | {include file='footer.tpl'} 29 | -------------------------------------------------------------------------------- /resources/email/warn.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 | 系统提示 13 |

14 |
15 |
16 |
17 |
18 |

19 | {$text} 20 |

21 |
22 |
23 |
24 |
25 | 26 | {include file='footer.tpl'} 27 | -------------------------------------------------------------------------------- /resources/locale/en_US.php: -------------------------------------------------------------------------------- 1 | 'en_US', 8 | 'lang_name' => 'English(Simplified)', 9 | 'lang_code' => 'en', 10 | 'bot' => [ 11 | 'daily_job_run' => 'Successful execution of daily tasks', 12 | 'detect_rule_added' => 'Detect rule %rule_name% has been added', 13 | 'diary' => 'Number of people checking in today: %checkin_user%' . PHP_EOL . 'Total traffic used today: %lastday_total%', 14 | 'node_added' => '%node_name% has been added', 15 | 'node_deleted' => '%node_name% was deleted', 16 | 'node_gfwed' => '%node_name% is blocked', 17 | 'node_offline' => '%node_name% has gone offline', 18 | 'node_online' => '%node_name% is back online', 19 | 'node_ungfwed' => '%node_name% recovered', 20 | 'node_updated' => '%node_name% has been modified', 21 | 'order_created' => 'Order #%order_id% has been created' . PHP_EOL . 'Link: %order_link%', 22 | 'test_message' => 'Test message', 23 | 'ticket_created' => 'Ticket #%ticket_id% has been created' . PHP_EOL . 'Link: %ticket_link%', 24 | 'user_join_welcome_free' => 'Welcome %user_name%', 25 | 'user_join_welcome_paid' => 'Welcome VIP%user_class% user %user_name% to join the group', 26 | 'user_not_bind' => 'You have not bound your account to this website. You can enter the **Edit Account** of the website and bind your account in the lower right corner.', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /resources/locale/ja_JP.php: -------------------------------------------------------------------------------- 1 | 'ja_JP', 8 | 'lang_name' => '日本語', 9 | 'lang_code' => 'ja', 10 | 'bot' => [ 11 | 'daily_job_run' => '日次タスクの正常な実行', 12 | 'detect_rule_added' => '追加された監査ルール %rule_name%', 13 | 'diary' => '今日チェックインした人の数: %checkin_user%' . PHP_EOL . '今日使用された合計トラフィック: %lastday_total%', 14 | 'node_added' => '%node_name% が追加されました', 15 | 'node_deleted' => '%node_name% が削除されました', 16 | 'node_gfwed' => '%node_name% はブロックされています', 17 | 'node_offline' => '%node_name% にはいくつかの障害があります', 18 | 'node_online' => '%node_name% はオンラインに戻りました', 19 | 'node_ungfwed' => '%node_name% が回復しました', 20 | 'node_updated' => '%node_name% が変更されました', 21 | 'order_created' => 'オーダー #%order_id% が作成されました' . PHP_EOL . 'リンク: %order_link%', 22 | 'test_message' => 'テストメッセージ', 23 | 'ticket_created' => 'チケット #%ticket_id% が作成されました' . PHP_EOL . 'リンク: %ticket_link%', 24 | 'user_join_welcome_free' => 'ようこそ %user_name%', 25 | 'user_join_welcome_paid' => 'VIP%user_class% ユーザー %user_name% のグループへの参加を歓迎します', 26 | 'user_not_bind' => 'アカウントをこの Web サイトにバインドしていません。Web サイトの **データ編集** に入り、右下隅でアカウントをバインドできます。', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /resources/locale/zh_CN.php: -------------------------------------------------------------------------------- 1 | 'zh_CN', 8 | 'lang_name' => '中文', 9 | 'lang_code' => 'zh', 10 | 'bot' => [ 11 | 'daily_job_run' => '成功执行每日任务', 12 | 'detect_rule_added' => '新增了审计规则 %rule_name%', 13 | 'diary' => '今日签到人数:%checkin_user%' . PHP_EOL . '今日使用总流量:%lastday_total%', 14 | 'node_added' => '%node_name% 已被添加', 15 | 'node_deleted' => '%node_name% 被删除了', 16 | 'node_gfwed' => '%node_name% 节点被墙了', 17 | 'node_offline' => '%node_name% 节点出现了一些故障', 18 | 'node_online' => '%node_name% 节点恢复上线', 19 | 'node_ungfwed' => '%node_name% 节点恢复了', 20 | 'node_updated' => '%node_name% 已被修改', 21 | 'order_created' => '订单 #%order_id% 已创建' . PHP_EOL . '链接:%order_link%', 22 | 'test_message' => '测试消息', 23 | 'ticket_created' => '工单 #%ticket_id% 已创建' . PHP_EOL . '链接:%ticket_link%', 24 | 'user_join_welcome_free' => '欢迎 %user_name%', 25 | 'user_join_welcome_paid' => '欢迎 VIP%user_class% 用户 %user_name% 加入群组', 26 | 'user_not_bind' => '你未绑定本站账号,你可以进入网站的 **资料编辑**,在右下方绑定你的账号。', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /resources/locale/zh_TW.php: -------------------------------------------------------------------------------- 1 | 'zh_TW', 8 | 'lang_name' => '正體中文', 9 | 'lang_code' => 'zh', 10 | 'bot' => [ 11 | 'daily_job_run' => '成功執行每日任務', 12 | 'detect_rule_added' => '新增了稽核規則 %rule_name%', 13 | 'diary' => '今日簽到人數:%checkin_user%' . PHP_EOL . '今日使用總流量:%lastday_total%', 14 | 'node_added' => '%node_name% 已被加', 15 | 'node_deleted' => '%node_name% 被刪除了', 16 | 'node_gfwed' => '%node_name% 被牆了', 17 | 'node_offline' => '%node_name% 出現了一些故障', 18 | 'node_online' => '%node_name% 恢復上線', 19 | 'node_ungfwed' => '%node_name% 恢復了', 20 | 'node_updated' => '%node_name% 已被修改', 21 | 'order_created' => '訂單 #%order_id% 已建立' . PHP_EOL . '連結:%order_link%', 22 | 'test_message' => '測試訊息', 23 | 'ticket_created' => '工單 #%ticket_id% 已建立' . PHP_EOL . '連結:%ticket_link%', 24 | 'user_join_welcome_free' => '歡迎 %user_name%', 25 | 'user_join_welcome_paid' => '歡迎 VIP%user_class% 使用者 %user_name% 加入群組', 26 | 'user_not_bind' => '你未綁定本站帳號,你可以進入網站的 **資料編輯**,在右下方綁定你的帳號。', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /resources/views/tabler/404.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
404
8 |

你所尝试访问的页面不存在

9 |

10 | Take me home, country roads, to the place, I belong... 11 |

12 | 18 |
19 |
20 |
21 | 22 | {include file='footer.tpl'} 23 | -------------------------------------------------------------------------------- /resources/views/tabler/405.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
405
8 |

不允许的请求方式

9 |

10 | Wanna try something stupid I see, now go back to the home page. 11 |

12 | 18 |
19 |
20 |
21 | 22 | {include file='footer.tpl'} -------------------------------------------------------------------------------- /resources/views/tabler/500.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |
500
8 |

服务器内部错误

9 |

10 | Take me home, country roads, to the place, I belong... Oh wait the server is down. 11 |

12 | 18 |
19 |
20 |
21 | 22 | {include file='footer.tpl'} -------------------------------------------------------------------------------- /resources/views/tabler/captcha/ajax.tpl: -------------------------------------------------------------------------------- 1 | {if $public_setting['captcha_provider'] === 'turnstile'} 2 | turnstile: document.querySelector("[name=cf-turnstile-response]").value, 3 | {/if} 4 | {if $public_setting['captcha_provider'] === 'geetest'} 5 | geetest: geetest_result, 6 | {/if} 7 | {if $public_setting['captcha_provider'] === 'hcaptcha'} 8 | hcaptcha: hcaptcha.getResponse(), 9 | {/if} 10 | {if $public_setting['captcha_provider'] === 'recaptcha_enterprise'} 11 | recaptcha_enterprise: grecaptcha.enterprise.getResponse(), 12 | {/if} 13 | -------------------------------------------------------------------------------- /resources/views/tabler/captcha/div.tpl: -------------------------------------------------------------------------------- 1 | {if $public_setting['captcha_provider'] === 'turnstile'} 2 |
3 | {/if} 4 | {if $public_setting['captcha_provider'] === 'geetest'} 5 |
6 | {/if} 7 | {if $public_setting['captcha_provider'] === 'hcaptcha'} 8 |
9 | {/if} 10 | {if $public_setting['captcha_provider'] === 'recaptcha_enterprise'} 11 |
12 | {/if} 13 | -------------------------------------------------------------------------------- /resources/views/tabler/captcha/js.tpl: -------------------------------------------------------------------------------- 1 | {if $public_setting['captcha_provider'] === 'turnstile'} 2 | 3 | {/if} 4 | {if $public_setting['captcha_provider'] === 'geetest'} 5 | 6 | 20 | {/if} 21 | {if $public_setting['captcha_provider'] === 'hcaptcha'} 22 | 23 | {/if} 24 | {if $public_setting['captcha_provider'] === 'recaptcha_enterprise'} 25 | 26 | 33 | {/if} 34 | -------------------------------------------------------------------------------- /resources/views/tabler/gateway/f2f.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | 支付宝当面付 6 |

7 |

8 |
9 | 12 |
13 | 14 | 47 | -------------------------------------------------------------------------------- /resources/views/tabler/gateway/paypal.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | PayPal 6 |

7 |

8 |
9 |
10 | 11 | 32 | -------------------------------------------------------------------------------- /resources/views/tabler/gateway/stripe.tpl: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 |

6 | Stripe 7 |

8 |

9 |

可以使用带有 10 | 11 | 12 | 13 | 等标识的信用卡或借记卡

14 |
15 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /resources/views/tabler/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {$config['appName']} 11 | 12 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /resources/views/tabler/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {$config['appName']} 7 | {if $user->isLogin} 8 | 11 | {else} 12 | 15 | {/if} 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/views/tabler/password/token.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 | 8 | Next Panel Logo 9 | 10 |
11 |
12 |
13 |

设置新密码

14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 | 33 |
34 |
35 |
36 | 已有账户? 点击登录 37 |
38 |
39 |
40 | 41 | {include file='footer.tpl'} 42 | -------------------------------------------------------------------------------- /resources/views/tabler/tinymce.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | -------------------------------------------------------------------------------- /resources/views/tabler/tos.tpl: -------------------------------------------------------------------------------- 1 | {include file='header.tpl'} 2 | 3 | 4 |
5 |
6 |
7 |

用户服务协议(Terms of Service)

8 |

{$config['appName']},以下简称本站。

9 |
10 |

隐私安全

11 |

邮箱为本站服务的唯一凭证,请妥善保管。

12 |

用户密码均为密文储存,无法解密,但出于安全起见还是请使用高强度密码或使用密码管理器。

13 |
14 |

使用条款

15 |

在使用服务时,需遵循站点和节点所在国家的法律。

16 |

对于免费用户,本站有权在不通知的情况下删除账户。

17 |

任何违反使用条款的用户,我们将会删除违规账户并收回使用本站服务的权利。

18 |
19 |
20 |
21 | 22 | {include file='footer.tpl'} 23 | -------------------------------------------------------------------------------- /resources/views/tabler/user/banned.tpl: -------------------------------------------------------------------------------- 1 | {include file='user/header.tpl'} 2 | 3 |
4 |
5 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 | {if $banned_reason === 'DetectBan'} 28 |

审计封禁

29 |

你的账户因为触发审计规则而被系统自动封禁

30 | {else} 31 |

以下是你被封禁的理由

32 |

{$banned_reason}

33 | {/if} 34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | {include file='user/footer.tpl'} 42 | -------------------------------------------------------------------------------- /resources/views/tabler/user/docs/view.tpl: -------------------------------------------------------------------------------- 1 | {include file='user/header.tpl'} 2 | 3 |
4 |
5 | 14 |
15 |
16 |
17 |
18 |
19 |
20 | {$doc->content} 21 |
22 |
23 |
24 |
25 |
26 | 27 | {include file='user/footer.tpl'} -------------------------------------------------------------------------------- /src/Command/Command.php: -------------------------------------------------------------------------------- 1 | argv = $argv; 19 | } 20 | 21 | abstract public function boot(): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Command/Test.php: -------------------------------------------------------------------------------- 1 | argv) === 2) { 26 | echo $this->description; 27 | } else { 28 | $methodName = $this->argv[2]; 29 | if (method_exists($this, $methodName)) { 30 | $this->$methodName(); 31 | } else { 32 | echo '方法不存在' . PHP_EOL; 33 | } 34 | } 35 | } 36 | 37 | public function sendFinanceMail(): void 38 | { 39 | if (count($this->argv) === 3) { 40 | $type = 'daily'; 41 | } else { 42 | [, , , $type] = $this->argv; 43 | } 44 | 45 | $cron = new Cron(); 46 | 47 | switch ($type) { 48 | case 'daily': 49 | $cron->sendDailyFinanceMail(); 50 | break; 51 | case 'weekly': 52 | $cron->sendWeeklyFinanceMail(); 53 | break; 54 | case 'monthly': 55 | $cron->sendMonthlyFinanceMail(); 56 | break; 57 | default: 58 | echo '请输入正确的参数' . PHP_EOL; 59 | } 60 | } 61 | 62 | public function generateSysLog(): void 63 | { 64 | $message = '测试日志'; 65 | $context = [ 66 | 'user_id' => 1, 67 | 'ip' => '127.0.0.1', 68 | ]; 69 | 70 | $logger = new Logger('admin'); 71 | $logger->pushHandler(new DBHandler()); 72 | $logger->debug($message, $context); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Controllers/Admin/MoneyLogController.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'id' => '事件ID', 21 | 'user_id' => '用户ID', 22 | 'before' => '变动前余额', 23 | 'after' => '变动后余额', 24 | 'amount' => '变动金额', 25 | 'remark' => '备注', 26 | 'create_time' => '变动时间', 27 | ], 28 | ]; 29 | 30 | /** 31 | * 后台用户余额记录页面 32 | * 33 | * @throws Exception 34 | */ 35 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 36 | { 37 | return $response->write( 38 | $this->view() 39 | ->assign('details', self::$details) 40 | ->fetch('admin/log/money.tpl') 41 | ); 42 | } 43 | 44 | /** 45 | * 后台用户余额记录页面 AJAX 46 | */ 47 | public function ajax(ServerRequest $request, Response $response, array $args): ResponseInterface 48 | { 49 | $money_logs = (new UserMoneyLog())->orderBy('id', 'desc')->get(); 50 | 51 | foreach ($money_logs as $money_log) { 52 | $money_log->create_time = Tools::toDateTime((int) $money_log->create_time); 53 | } 54 | 55 | return $response->withJson([ 56 | 'money_logs' => $money_logs, 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Controllers/Admin/PaybackController.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'id' => '事件ID', 21 | 'total' => '原始金额', 22 | 'userid' => '发起用户ID', 23 | 'user_name' => '发起用户名', 24 | 'ref_by' => '获利用户ID', 25 | 'ref_user_name' => '获利用户名', 26 | 'ref_get' => '获利金额', 27 | 'invoice_id' => '账单ID', 28 | 'datetime' => '时间', 29 | ], 30 | ]; 31 | 32 | /** 33 | * 后台邀请记录页面 34 | * 35 | * @throws Exception 36 | */ 37 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 38 | { 39 | return $response->write( 40 | $this->view() 41 | ->assign('details', self::$details) 42 | ->fetch('admin/log/payback.tpl') 43 | ); 44 | } 45 | 46 | /** 47 | * 后台登录记录页面 AJAX 48 | */ 49 | public function ajax(ServerRequest $request, Response $response, array $args): ResponseInterface 50 | { 51 | $paybacks = (new Payback())->orderBy('id', 'desc')->get(); 52 | 53 | foreach ($paybacks as $payback) { 54 | $payback->datetime = Tools::toDateTime((int) $payback->datetime); 55 | $payback->user_name = $payback->getAttributes(); 56 | $payback->ref_user_name = $payback->getAttributes(); 57 | } 58 | 59 | return $response->withJson([ 60 | 'paybacks' => $paybacks, 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Controllers/Admin/PaylistController.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'id' => '事件ID', 21 | 'userid' => '用户ID', 22 | 'total' => '金额', 23 | 'status' => '状态', 24 | 'gateway' => '支付网关', 25 | 'tradeno' => '网关单号', 26 | 'datetime' => '支付时间', 27 | 'invoice_id' => '关联账单ID', 28 | ], 29 | ]; 30 | 31 | /** 32 | * 后台网关记录页面 33 | * 34 | * @throws Exception 35 | */ 36 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 37 | { 38 | return $response->write( 39 | $this->view() 40 | ->assign('details', self::$details) 41 | ->fetch('admin/log/gateway.tpl') 42 | ); 43 | } 44 | 45 | /** 46 | * 后台网关记录页面 AJAX 47 | */ 48 | public function ajax(ServerRequest $request, Response $response, array $args): ResponseInterface 49 | { 50 | $paylists = (new Paylist())->orderBy('id', 'desc')->get(); 51 | 52 | foreach ($paylists as $paylist) { 53 | $paylist->status = $paylist->status(); 54 | $paylist->datetime = Tools::toDateTime((int) $paylist->datetime); 55 | } 56 | 57 | return $response->withJson([ 58 | 'paylists' => $paylists, 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/CaptchaController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('captcha'); 23 | $this->settings = Config::getClass('captcha'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/captcha.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/FeatureController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('feature'); 23 | $this->settings = Config::getClass('feature'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/feature.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/LlmController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('llm'); 23 | $this->settings = Config::getClass('llm'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/llm.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/RefController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('ref'); 23 | $this->settings = Config::getClass('ref'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/ref.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/RegController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('reg'); 23 | $this->settings = Config::getClass('reg'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/reg.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/SubController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('subscribe'); 23 | $this->settings = Config::getClass('subscribe'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/sub.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Admin/Setting/SupportController.php: -------------------------------------------------------------------------------- 1 | update_field = Config::getItemListByClass('support'); 23 | $this->settings = Config::getClass('support'); 24 | } 25 | 26 | /** 27 | * @throws Exception 28 | */ 29 | public function index(ServerRequest $request, Response $response, array $args): ResponseInterface 30 | { 31 | return $response->write( 32 | $this->view() 33 | ->assign('update_field', $this->update_field) 34 | ->assign('settings', $this->settings) 35 | ->fetch('admin/setting/support.tpl') 36 | ); 37 | } 38 | 39 | public function save(ServerRequest $request, Response $response, array $args): ResponseInterface 40 | { 41 | foreach ($this->update_field as $item) { 42 | if (! Config::set($item, $request->getParam($item))) { 43 | return $response->withJson([ 44 | 'ret' => 0, 45 | 'msg' => '保存 ' . $item . ' 时出错', 46 | ]); 47 | } 48 | } 49 | 50 | return $response->withJson([ 51 | 'ret' => 1, 52 | 'msg' => '保存成功', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controllers/Api/AdminApiV1Controller.php: -------------------------------------------------------------------------------- 1 | $this->telegram($request, $response, $args), 28 | default => $response->withStatus(404)->write($this->view()->fetch('404.tpl')), 29 | }; 30 | } 31 | 32 | /** 33 | * @throws TelegramSDKException 34 | * @throws InvalidDatabaseException|GuzzleException 35 | */ 36 | public function telegram(ServerRequest $request, Response $response, array $args): ResponseInterface 37 | { 38 | $token = $request->getQueryParam('token'); 39 | 40 | if (Config::obtain('telegram_token') !== '' && $token === Config::obtain('telegram_webhook_token')) { 41 | Telegram::process($request); 42 | 43 | return $response->withStatus(204); 44 | } 45 | 46 | return $response->withStatus(400); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Controllers/User/ClientController.php: -------------------------------------------------------------------------------- 1 | antiXss->xss_clean($args['name']); 19 | 20 | if (! $_ENV['enable_r2_client_download'] || $clientName === '' || $clientName === null) { 21 | return $response->withStatus(404); 22 | } 23 | 24 | $clients = [ 25 | 'Clash.Nyanpasu.exe', 26 | 'Clash.Nyanpasu.AppImage', 27 | 'Clash.Nyanpasu_aarch64.dmg', 28 | 'CMFA.apk', 29 | 'SFA.apk', 30 | 'SFM.zip', 31 | 'Hiddify.apk', 32 | 'Hiddify.AppImage', 33 | 'Hiddify.dmg', 34 | 'Hiddify.exe', 35 | ]; 36 | 37 | if (! in_array($clientName, $clients)) { 38 | return $response->withStatus(404); 39 | } 40 | 41 | $presignedUrl = Cloudflare::genR2PresignedUrl($clientName); 42 | 43 | return $response->withHeader('Location', $presignedUrl)->withStatus(302); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Controllers/User/DetectLogController.php: -------------------------------------------------------------------------------- 1 | withRedirect('/user'); 27 | } 28 | 29 | $logs = (new DetectLog())->orderBy('id', 'desc')->where('user_id', $this->user->id)->get(); 30 | 31 | foreach ($logs as $log) { 32 | $log->node_name = $log->nodeName(); 33 | $log->rule = $log->rule(); 34 | $log->rule->type = $log->rule()->type(); 35 | $log->datetime = Tools::toDateTime($log->datetime); 36 | } 37 | 38 | return $response->write($this->view() 39 | ->assign('logs', $logs) 40 | ->fetch('user/detect/log.tpl')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controllers/User/DetectRuleController.php: -------------------------------------------------------------------------------- 1 | get(); 22 | 23 | return $response->write($this->view() 24 | ->assign('rules', $rules) 25 | ->fetch('user/detect/index.tpl')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controllers/User/DocsController.php: -------------------------------------------------------------------------------- 1 | user->class === 0)) { 24 | return $response->withRedirect('/user'); 25 | } 26 | 27 | $docs = (new Docs())->where('status', 1) 28 | ->orderBy('sort') 29 | ->orderBy('id', 'desc')->get(); 30 | 31 | return $response->write( 32 | $this->view() 33 | ->assign('docs', $docs) 34 | ->fetch('user/docs/index.tpl') 35 | ); 36 | } 37 | 38 | /** 39 | * @throws Exception 40 | */ 41 | public function detail(ServerRequest $request, Response $response, array $args): ResponseInterface 42 | { 43 | if (! Config::obtain('display_docs') || 44 | (Config::obtain('display_docs_only_for_paid_user') && $this->user->class === 0)) { 45 | return $response->withRedirect('/user/docs'); 46 | } 47 | 48 | $doc = (new Docs())->where('status', 1)->where('id', $args['id'])->first(); 49 | 50 | if (! $doc) { 51 | return $response->withRedirect('/user/docs'); 52 | } 53 | 54 | return $response->write( 55 | $this->view() 56 | ->assign('doc', $doc) 57 | ->fetch('user/docs/view.tpl') 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controllers/User/ProductController.php: -------------------------------------------------------------------------------- 1 | where('status', '1') 23 | ->where('type', 'tabp') 24 | ->orderBy('id') 25 | ->get(); 26 | 27 | $bandwidths = (new Product())->where('status', '1') 28 | ->where('type', 'bandwidth') 29 | ->orderBy('id') 30 | ->get(); 31 | 32 | $times = (new Product())->where('status', '1') 33 | ->where('type', 'time') 34 | ->orderBy('id') 35 | ->get(); 36 | 37 | foreach ($tabps as $tabp) { 38 | $tabp->content = json_decode($tabp->content); 39 | } 40 | 41 | foreach ($bandwidths as $bandwidth) { 42 | $bandwidth->content = json_decode($bandwidth->content); 43 | } 44 | 45 | foreach ($times as $time) { 46 | $time->content = json_decode($time->content); 47 | } 48 | 49 | return $response->write( 50 | $this->view() 51 | ->assign('tabps', $tabps) 52 | ->assign('bandwidths', $bandwidths) 53 | ->assign('times', $times) 54 | ->fetch('user/product.tpl') 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Controllers/User/ServerController.php: -------------------------------------------------------------------------------- 1 | user, true); 23 | $node_list = []; 24 | 25 | foreach ($nodes as $node) { 26 | $node_list[] = [ 27 | 'id' => $node->id, 28 | 'name' => $node->name, 29 | 'class' => (int) $node->node_class, 30 | 'color' => $node->color, 31 | 'connection_type' => $node->connection_type, 32 | 'sort' => $node->sort(), 33 | 'online_user' => $node->online_user, 34 | 'online' => $node->getNodeOnlineStatus(), 35 | 'traffic_rate' => $node->traffic_rate, 36 | 'is_dynamic_rate' => $node->is_dynamic_rate, 37 | 'node_bandwidth' => Tools::autoBytes($node->node_bandwidth), 38 | 'node_bandwidth_limit' => $node->node_bandwidth_limit === 0 ? '无限制' : 39 | Tools::autoBytes($node->node_bandwidth_limit), 40 | ]; 41 | } 42 | 43 | return $response->write( 44 | $this->view() 45 | ->assign('servers', $node_list) 46 | ->fetch('user/server.tpl') 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Controllers/WebAPI/FuncController.php: -------------------------------------------------------------------------------- 1 | toArray(); 30 | 31 | return ResponseHelper::successWithDataEtag($request, $response, $rules); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Controllers/WebAPI/NodeController.php: -------------------------------------------------------------------------------- 1 | find($node_id); 27 | 28 | if ($node === null) { 29 | return ResponseHelper::error($response, 'Node not found.'); 30 | } 31 | 32 | if ($node->type === 0) { 33 | return ResponseHelper::error($response, 'Node is not enabled.'); 34 | } 35 | 36 | $data = [ 37 | 'node_speedlimit' => $node->node_speedlimit, 38 | 'sort' => $node->sort, 39 | 'server' => $node->server, 40 | 'custom_config' => json_decode($node->custom_config, true, JSON_UNESCAPED_SLASHES), 41 | 'type' => PANEL_NAME, 42 | 'version' => PANEL_VERSION, 43 | ]; 44 | 45 | return ResponseHelper::successWithDataEtag($request, $response, $data); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Interfaces/MigrationInterface.php: -------------------------------------------------------------------------------- 1 | isLogin) { 21 | return AppFactory::determineResponseFactory()->createResponse(302)->withHeader('Location', '/auth/login'); 22 | } 23 | 24 | if (! $user->is_admin) { 25 | return AppFactory::determineResponseFactory()->createResponse(302)->withHeader('Location', '/user'); 26 | } 27 | 28 | return $handler->handle($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Middleware/AdminApi.php: -------------------------------------------------------------------------------- 1 | isLogin) { 21 | $response = AppFactory::determineResponseFactory()->createResponse(302); 22 | 23 | return $response->withHeader('Location', '/user'); 24 | } 25 | 26 | return $handler->handle($request); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Middleware/NodeApi.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(); 24 | 25 | if (! $user->isLogin) { 26 | if (str_contains($path, '/user/order/create')) { 27 | Cookie::set(['redir' => $path . '?' . $request->getUri()->getQuery()], time() + 3600); 28 | } 29 | 30 | return AppFactory::determineResponseFactory()->createResponse(302)->withHeader('Location', '/auth/login'); 31 | } 32 | 33 | $bannedUserEnabledPages = ['/user/banned', '/user/logout']; 34 | 35 | if ($user->is_banned && ! in_array($path, $bannedUserEnabledPages)) { 36 | return AppFactory::determineResponseFactory()->createResponse(302)->withHeader('Location', '/user/banned'); 37 | } 38 | 39 | $request = $request->withAttribute('user', $user); 40 | 41 | return $handler->handle($request); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/UserApi.php: -------------------------------------------------------------------------------- 1 | status) { 26 | 0 => '未发布', 27 | 1 => '已发布', 28 | 2 => '置顶', 29 | default => '未知', 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/DetectBanLog.php: -------------------------------------------------------------------------------- 1 | end_time + $this->ban_time * 60); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Models/DetectLog.php: -------------------------------------------------------------------------------- 1 | find($this->node_id); 30 | } 31 | 32 | /** 33 | * 节点名 34 | */ 35 | public function nodeName(): string 36 | { 37 | return $this->node() === null ? '节点不存在' : $this->node()->name; 38 | } 39 | 40 | /** 41 | * 规则 42 | */ 43 | public function rule(): ?DetectRule 44 | { 45 | return (new DetectRule())->find($this->list_id); 46 | } 47 | 48 | /** 49 | * 规则名 50 | */ 51 | public function ruleName(): string 52 | { 53 | return $this->rule() === null ? '规则不存在' : $this->rule()->name; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Models/DetectRule.php: -------------------------------------------------------------------------------- 1 | type === 1 ? '数据包明文匹配' : '数据包 hex 匹配'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/Docs.php: -------------------------------------------------------------------------------- 1 | status) { 27 | 0 => '未发布', 28 | 1 => '已发布', 29 | default => '未知', 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/EmailQueue.php: -------------------------------------------------------------------------------- 1 | to_email = $to; 29 | $this->subject = $subject; 30 | $this->template = $template; 31 | $this->time = time(); 32 | $this->array = json_encode($array); 33 | $this->save(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Models/GiftCard.php: -------------------------------------------------------------------------------- 1 | status ? '已使用' : '未使用'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/HourlyUsage.php: -------------------------------------------------------------------------------- 1 | where('user_id', $user_id)->where('date', $date)->first(); 31 | 32 | if ($exist_usage === null) { 33 | $new_usage_array = array_fill(0, 24, 0); 34 | $new_usage_array[$hour] = $usage; 35 | $this->user_id = $user_id; 36 | $this->date = $date; 37 | $this->usage = json_encode($new_usage_array); 38 | $this->save(); 39 | } else { 40 | $exist_usage_array = json_decode($exist_usage->usage, true); 41 | $exist_usage_array[$hour] += $usage; 42 | $exist_usage->usage = json_encode($exist_usage_array); 43 | $exist_usage->save(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/InviteCode.php: -------------------------------------------------------------------------------- 1 | code = Tools::genRandomChar(10); 25 | $this->user_id = $user_id; 26 | $this->save(); 27 | 28 | return $this->code; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/Link.php: -------------------------------------------------------------------------------- 1 | find($this->attributes['userid']); 24 | } 25 | 26 | public function isValid(): bool 27 | { 28 | if ($this !== null && $this->user() !== null && $this->user()->is_banned === 0) { 29 | return true; 30 | } 31 | 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/Model.php: -------------------------------------------------------------------------------- 1 | ip`, this method would convert IPv4-mapped IPv6 Address to IPv4 Address. 29 | * 30 | * @return string Example: IPv4 Address: `1.1.1.1`; IPv6 Address: `2606:4700:4700::1111` 31 | */ 32 | public function ip(): string 33 | { 34 | $ip = $this->attributes['ip']; 35 | 36 | if (str_starts_with($ip, '::ffff:')) { 37 | $v4 = substr($ip, 7); 38 | // Mix hexadecimal and dot decimal notations: https://www.rfc-editor.org/rfc/rfc5952#section-5 39 | // 40 | // IPv4-translated address: https://www.rfc-editor.org/rfc/rfc2765.html#section-2.1 41 | if (str_contains($v4, '.') && ! str_contains($v4, ':')) { 42 | return $v4; 43 | } 44 | } 45 | 46 | return $ip; 47 | } 48 | 49 | public function nodeName(): string 50 | { 51 | return (new Node())->where('id', $this->node_id)->value('name'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Models/Order.php: -------------------------------------------------------------------------------- 1 | status) { 35 | 'pending_payment' => '等待中', 36 | 'pending_activation' => '待激活', 37 | 'activated' => '已激活', 38 | 'expired' => '已过期', 39 | 'cancelled' => '已取消', 40 | default => '未知', 41 | }; 42 | } 43 | 44 | /** 45 | * 订单商品类型 46 | */ 47 | public function productType(): string 48 | { 49 | return match ($this->product_type) { 50 | 'tabp' => '时间流量包', 51 | 'time' => '时间包', 52 | 'bandwidth' => '流量包', 53 | 'topup' => '充值', 54 | default => '其他', 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Models/Payback.php: -------------------------------------------------------------------------------- 1 | where('id', $this->userid)->first(); 29 | } 30 | 31 | public function getUserNameAttribute(): string 32 | { 33 | return (new User())->where('id', $this->userid)->first() === null ? '已注销' : 34 | (new User())->where('id', $this->userid)->first()->user_name; 35 | } 36 | 37 | public function refUser(): \Illuminate\Database\Eloquent\Model|User|null 38 | { 39 | return (new User())->where('id', $this->ref_by)->first(); 40 | } 41 | 42 | public function getRefUserNameAttribute(): string 43 | { 44 | return (new User())->where('id', $this->ref_by)->first() === null ? '已注销' : 45 | (new User())->where('id', $this->ref_by)->first()->user_name; 46 | } 47 | 48 | public function add(float $total, int $user_id, int $ref_by, float $ref_get, int $invoice_id): void 49 | { 50 | $this->total = $total; 51 | $this->userid = $user_id; 52 | $this->ref_by = $ref_by; 53 | $this->ref_get = $ref_get; 54 | $this->invoice_id = $invoice_id; 55 | $this->datetime = time(); 56 | $this->save(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Models/Paylist.php: -------------------------------------------------------------------------------- 1 | status) { 32 | 0 => '未支付', 33 | 1 => '已支付', 34 | default => '未知', 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/Product.php: -------------------------------------------------------------------------------- 1 | status ? '正常' : '下架'; 35 | } 36 | 37 | /** 38 | * 商品类型 39 | */ 40 | public function type(): string 41 | { 42 | return match ($this->type) { 43 | 'tabp' => '时间流量包', 44 | 'time' => '时间包', 45 | 'bandwidth' => '流量包', 46 | default => '其他', 47 | }; 48 | } 49 | 50 | /** 51 | * 商品库存 52 | */ 53 | public function stock(): string|int 54 | { 55 | return $this->stock < 0 ? '无限制' : $this->stock; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Models/SysLog.php: -------------------------------------------------------------------------------- 1 | level) { 29 | 200 => 'INFO', 30 | 250 => 'NOTICE', 31 | 300 => 'WARNING', 32 | 400 => 'ERROR', 33 | 500 => 'CRITICAL', 34 | 550 => 'ALERT', 35 | 600 => 'EMERGENCY', 36 | 999 => 'KABOOM', 37 | default => 'DEBUG', 38 | }; 39 | } 40 | 41 | public function channel(): string 42 | { 43 | return match ($this->channel) { 44 | 'cron' => '计划任务', 45 | 'sub' => '订阅', 46 | 'auth' => '认证', 47 | 'user' => '用户', 48 | 'admin' => '管理员', 49 | default => '未知', 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Models/Ticket.php: -------------------------------------------------------------------------------- 1 | type) { 31 | 'howto' => '使用', 32 | 'billing' => '财务', 33 | 'account' => '账户', 34 | default => '其他', 35 | }; 36 | } 37 | 38 | /** 39 | * 工单状态 40 | */ 41 | public function status(): string 42 | { 43 | return match ($this->status) { 44 | 'closed' => '已结单', 45 | 'open_wait_user' => '等待用户回复', 46 | 'open_wait_admin' => '进行中', 47 | default => '未知', 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Models/UserCoupon.php: -------------------------------------------------------------------------------- 1 | content)->type ?? null) { 33 | 'percentage' => '百分比', 34 | 'fixed' => '固定金额', 35 | default => '未知', 36 | }; 37 | } 38 | 39 | /** 40 | * 优惠码状态 41 | */ 42 | public function status(): string 43 | { 44 | return $this->expire_time < time() ? '已过期' : '激活'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/UserMoneyLog.php: -------------------------------------------------------------------------------- 1 | user_id = $user_id; 29 | $this->before = $before; 30 | $this->after = $after; 31 | $this->amount = $amount; 32 | $this->remark = $remark; 33 | $this->create_time = time(); 34 | $this->save(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Auth.php: -------------------------------------------------------------------------------- 1 | login($uid, $time); 16 | } 17 | 18 | public static function getUser(): User 19 | { 20 | global $user; 21 | 22 | if ($user === null) { 23 | $user = self::getDriver()->getUser(); 24 | } 25 | 26 | return $user; 27 | } 28 | 29 | public static function logout(): void 30 | { 31 | self::getDriver()->logout(); 32 | } 33 | 34 | private static function getDriver(): Auth\Cookie 35 | { 36 | return Factory::createAuth(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Services/Auth/Base.php: -------------------------------------------------------------------------------- 1 | $_ENV['sentry_dsn'], 29 | ]); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Services/Bot/Discord/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/src/Services/Bot/Discord/.gitkeep -------------------------------------------------------------------------------- /src/Services/Bot/Slack/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/src/Services/Bot/Slack/.gitkeep -------------------------------------------------------------------------------- /src/Services/Bot/Telegram/Commands/HelpCommand.php: -------------------------------------------------------------------------------- 1 | update; 32 | $message = $update->message; 33 | 34 | if (in_array($message->chat->type, ['group', 'supergroup']) && Config::obtain('telegram_group_quiet')) { 35 | return; 36 | } 37 | 38 | if (! preg_match('/^\/help\s?(@' . Config::obtain('telegram_bot') . ')?.*/i', $message->text) && 39 | ! Config::obtain('help_any_command')) { 40 | return; 41 | } 42 | 43 | $this->replyWithChatAction(['action' => Actions::TYPING]); 44 | $commands = $this->telegram->getCommands(); 45 | $text = '系统中可用的所有指令:'; 46 | $text .= PHP_EOL . PHP_EOL; 47 | 48 | foreach ($commands as $name => $handler) { 49 | $text .= '/' . $name . PHP_EOL . '` - ' . $handler->getDescription() . '`' . PHP_EOL; 50 | } 51 | 52 | $this->replyWithMessage( 53 | [ 54 | 'text' => $text, 55 | 'parse_mode' => 'Markdown', 56 | 'disable_web_page_preview' => false, 57 | 'reply_to_message_id' => $message->messageId, 58 | 'reply_markup' => null, 59 | ] 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Services/Bot/Telegram/Commands/PingCommand.php: -------------------------------------------------------------------------------- 1 | update; 32 | $message = $update->message; 33 | $chat_id = $message->chat->id; 34 | 35 | if ($message->chat->type === 'private') { 36 | // 发送 '输入中' 会话状态 37 | $this->replyWithChatAction(['action' => Actions::TYPING]); 38 | 39 | $text = [ 40 | 'Pong!', 41 | 'User ID is ' . $chat_id, 42 | ]; 43 | // 回送信息 44 | $this->replyWithMessage( 45 | [ 46 | 'text' => implode(PHP_EOL, $text), 47 | 'parse_mode' => 'Markdown', 48 | ] 49 | ); 50 | } elseif (in_array($message->chat->type, ['group', 'supergroup']) && 51 | ! Config::obtain('telegram_group_quiet')) { 52 | // 发送 '输入中' 会话状态 53 | $this->replyWithChatAction(['action' => Actions::TYPING]); 54 | 55 | $text = [ 56 | 'Pong!', 57 | 'User ID is ' . $message->from->id, 58 | 'Group ID is ' . $chat_id, 59 | ]; 60 | // 回送信息 61 | $this->replyWithMessage( 62 | [ 63 | 'text' => implode(PHP_EOL, $text), 64 | ] 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Services/Bot/Telegram/Commands/StartCommand.php: -------------------------------------------------------------------------------- 1 | update; 30 | $message = $update->message; 31 | 32 | if ($message->chat->type === 'private') { 33 | // 发送 '输入中' 会话状态 34 | $this->replyWithChatAction(['action' => Actions::TYPING]); 35 | // 回送信息 36 | $this->replyWithMessage( 37 | [ 38 | 'text' => '发送 /help 获取帮助', 39 | 'parse_mode' => 'Markdown', 40 | ] 41 | ); 42 | } elseif (in_array($message->chat->type, ['group', 'supergroup']) && ! Config::obtain('telegram_group_quiet')) { 43 | // 发送 '输入中' 会话状态 44 | $this->replyWithChatAction(['action' => Actions::TYPING]); 45 | // 回送信息 46 | $this->replyWithMessage( 47 | [ 48 | 'text' => '?', 49 | 'parse_mode' => 'Markdown', 50 | 'reply_to_message_id' => $message->messageId, 51 | ] 52 | ); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Services/Bot/Telegram/Telegram.php: -------------------------------------------------------------------------------- 1 | addCommands([ 23 | new Commands\MyCommand(), 24 | new Commands\HelpCommand(), 25 | new Commands\MenuCommand(), 26 | new Commands\PingCommand(), 27 | new Commands\DcCommand(), 28 | new Commands\StartCommand(), 29 | new Commands\UnbindCommand(), 30 | new Commands\CheckinCommand(), 31 | ]); 32 | 33 | $bot->commandsHandler(true, $request); 34 | $bot->setConnectTimeOut(1); 35 | $update = $bot->getWebhookUpdate(); 36 | 37 | if ($update->has('callback_query')) { 38 | new Callback($bot, $update->callbackQuery); 39 | } elseif ($update->has('message')) { 40 | new Message($bot, $update->message); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/Cache.php: -------------------------------------------------------------------------------- 1 | $_ENV['redis_host'], 20 | 'port' => $_ENV['redis_port'], 21 | 'connectTimeout' => $_ENV['redis_connect_timeout'], 22 | 'readTimeout' => $_ENV['redis_read_timeout'], 23 | ]; 24 | 25 | if ($_ENV['redis_username'] !== '') { 26 | $config['auth']['user'] = $_ENV['redis_username']; 27 | } 28 | 29 | if ($_ENV['redis_password'] !== '') { 30 | $config['auth']['pass'] = $_ENV['redis_password']; 31 | } 32 | 33 | if ($_ENV['redis_ssl']) { 34 | $config['ssl'] = $_ENV['redis_ssl_context']; 35 | } 36 | 37 | return $config; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Cloudflare.php: -------------------------------------------------------------------------------- 1 | 'auto', 19 | 'endpoint' => 'https://' . $_ENV['r2_account_id'] . '.r2.cloudflarestorage.com', 20 | 'version' => 'latest', 21 | 'credentials' => $credentials, 22 | ]; 23 | 24 | return new S3Client($options); 25 | } 26 | 27 | public static function uploadR2($name, $file): void 28 | { 29 | $r2 = self::initR2(); 30 | 31 | try { 32 | $r2->upload( 33 | $_ENV['r2_bucket_name'], 34 | $name, 35 | $file, 36 | ); 37 | } catch (Exception $e) { 38 | echo $e->getMessage() . PHP_EOL; 39 | } 40 | } 41 | 42 | public static function genR2PresignedUrl($fileName): string 43 | { 44 | $r2 = self::initR2(); 45 | 46 | $cmd = $r2->getCommand('GetObject', [ 47 | 'Bucket' => $_ENV['r2_bucket_name'], 48 | 'Key' => $fileName, 49 | ]); 50 | 51 | return (string) $r2->createPresignedRequest( 52 | $cmd, 53 | '+' . $_ENV['r2_client_download_timeout'] . ' minutes' 54 | )->getUri(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/Exchange.php: -------------------------------------------------------------------------------- 1 | getExchangeRate($from, $to), 2); 22 | } 23 | 24 | /** 25 | * @throws GuzzleException 26 | * @throws RedisException 27 | */ 28 | public function getExchangeRate(string $from, string $to): float 29 | { 30 | $redis = (new Cache())->initRedis(); 31 | $rate = $redis->get('exchange_rate:' . $from . '_' . $to); 32 | 33 | if (! $rate) { 34 | $client = new Client(); 35 | $response = $client->get('https://cdn.moneyconvert.net/api/latest.json'); 36 | $data = json_decode($response->getBody()->getContents(), true); 37 | $rate = $data['rates'][$to] / $data['rates'][$from]; 38 | $redis->setex('exchange_rate:' . $from . '_' . $to, 3600, $rate); 39 | } 40 | 41 | return (float) $rate; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/Factory.php: -------------------------------------------------------------------------------- 1 | epay_config = $epay_config; 16 | } 17 | 18 | public function verifyNotify(): bool 19 | { 20 | if (is_null($_GET)) {//判断POST来的数组是否为空 21 | return false; 22 | } 23 | 24 | if ($this->getSignVeryfy($_GET, $_GET['sign'])) { 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | public function getSignVeryfy($para_temp, $sign): bool 32 | { 33 | //除去待签名参数数组中的空值和签名参数 34 | $para_filter = EpayTool::paraFilter($para_temp); 35 | //对待签名参数数组排序 36 | $para_sort = EpayTool::argSort($para_filter); 37 | //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 38 | $prestr = EpayTool::createLinkstring($para_sort); 39 | 40 | return EpayTool::verify($prestr, $sign, $this->epay_config['key']); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/Gateway/Epay/EpaySubmit.php: -------------------------------------------------------------------------------- 1 | epay_config = $epay_config; 15 | $this->epay_gateway = $this->epay_config['apiurl'] . 'submit.php?'; 16 | } 17 | 18 | public function buildRequestMysign($para_sort): string 19 | { 20 | //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 21 | $prestr = EpayTool::createLinkstring($para_sort); 22 | 23 | return EpayTool::sign($prestr, $this->epay_config['key']); 24 | } 25 | 26 | public function buildRequestPara($para_temp) 27 | { 28 | //除去待签名参数数组中的空值和签名参数 29 | $para_filter = EpayTool::paraFilter($para_temp); 30 | //对待签名参数数组排序 31 | $para_sort = EpayTool::argSort($para_filter); 32 | //生成签名结果 33 | $mysign = $this->buildRequestMysign($para_sort); 34 | //签名结果与签名方式加入请求提交参数组中 35 | $para_sort['sign'] = $mysign; 36 | $para_sort['sign_type'] = strtoupper(trim($this->epay_config['sign_type'])); 37 | 38 | return $para_sort; 39 | } 40 | 41 | public function buildRequestForm($para_temp, $method = 'POST', $button_name = '正在跳转'): string 42 | { 43 | //待请求参数数组 44 | $para = $this->buildRequestPara($para_temp); 45 | $html = "
"; 47 | 48 | foreach ($para as $key => $val) { 49 | $html .= ""; 50 | } 51 | //submit按钮控件请不要含有name属性 52 | $html .= "
"; 53 | $html .= ""; 54 | 55 | return $html; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Services/Gateway/Epay/EpayTool.php: -------------------------------------------------------------------------------- 1 | $val) { 33 | $arg .= $key . '=' . $val . '&'; 34 | } 35 | //去掉最后一个&字符 36 | $arg = substr($arg, 0, strlen($arg) - 1); 37 | //如果存在转义字符,那么去掉转义 38 | return stripslashes($arg); 39 | } 40 | 41 | public static function paraFilter($para): array 42 | { 43 | $para_filter = []; 44 | 45 | foreach ($para as $key => $val) { 46 | if ($key === 'sign' || $key === 'sign_type' || $val === '') { 47 | continue; 48 | } 49 | $para_filter[$key] = $val; 50 | } 51 | 52 | return $para_filter; 53 | } 54 | 55 | public static function argSort($para) 56 | { 57 | ksort($para); 58 | 59 | return $para; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Services/GeoIP2.php: -------------------------------------------------------------------------------- 1 | city_reader = new Reader(BASE_PATH . '/storage/GeoLite2-City/GeoLite2-City.mmdb'); 23 | $this->country_reader = new Reader(BASE_PATH . '/storage/GeoLite2-Country/GeoLite2-Country.mmdb'); 24 | } 25 | 26 | /** 27 | * @throws AddressNotFoundException 28 | * @throws InvalidDatabaseException 29 | */ 30 | public function getCity(string $ip): ?string 31 | { 32 | $record = $this?->city_reader?->city($ip); 33 | return $record?->city?->names[$_ENV['geoip_locale']] ?? $record?->city?->name; 34 | } 35 | 36 | /** 37 | * @throws AddressNotFoundException 38 | * @throws InvalidDatabaseException 39 | */ 40 | public function getCountry(string $ip): ?string 41 | { 42 | $record = $this?->country_reader?->country($ip); 43 | return $record?->country?->names[$_ENV['geoip_locale']] ?? $record?->country?->name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Services/I18n.php: -------------------------------------------------------------------------------- 1 | trans($key); 21 | } 22 | 23 | public static function getLocaleList(): array 24 | { 25 | $locales = []; 26 | $files = glob(BASE_PATH . '/resources/locale/*.php'); 27 | 28 | foreach ($files as $file) { 29 | $locales[] = basename($file, '.php'); 30 | } 31 | 32 | return $locales; 33 | } 34 | 35 | public static function getTranslator($lang = 'en_US'): Translator 36 | { 37 | $translator = new Translator($lang); 38 | $translator->addLoader('php', new PhpFileLoader()); 39 | $translator->addResource( 40 | 'php', 41 | BASE_PATH . '/resources/locale/' . $lang . '.php', 42 | $lang 43 | ); 44 | 45 | return $translator; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Services/IM.php: -------------------------------------------------------------------------------- 1 | new Discord(), 22 | 2 => new Slack(), 23 | default => new Telegram(), 24 | }; 25 | } 26 | 27 | /** 28 | * @throws GuzzleException 29 | * @throws TelegramSDKException 30 | */ 31 | public static function send(int $to, string $msg, int $type): void 32 | { 33 | self::getClient($type)->send($to, $msg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Services/IM/Base.php: -------------------------------------------------------------------------------- 1 | token = Config::obtain('slack_token'); 20 | $this->client = new Client(); 21 | } 22 | 23 | /** 24 | * @throws GuzzleException 25 | * @throws Exception 26 | */ 27 | public function send(int $to, string $msg): void 28 | { 29 | $url = 'https://slack.com/api/chat.postMessage'; 30 | 31 | $headers = [ 32 | 'Authorization' => 'Bearer '.$this->token, 33 | 'Content-Type' => 'application/json', 34 | ]; 35 | 36 | $body = [ 37 | 'channel' => $to, 38 | 'text' => $msg, 39 | ]; 40 | 41 | $response = $this->client->post($url, [ 42 | 'headers' => $headers, 43 | 'json' => $body, 44 | ]); 45 | 46 | if ($response->getStatusCode() !== 200) { 47 | throw new Exception($response->getBody()->getContents()); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Services/LLM.php: -------------------------------------------------------------------------------- 1 | new GoogleAI(), 22 | 'vertex-ai' => new VertexAI(), 23 | 'huggingface' => new HuggingFace(), 24 | 'cf-workers-ai' => new CloudflareWorkersAI(), 25 | 'anthropic' => new Anthropic(), 26 | 'aws-bedrock' => new AwsBedrock(), 27 | default => new OpenAI(), 28 | }; 29 | } 30 | 31 | public static function genTextResponse(string $q): string 32 | { 33 | if (Config::obtain('llm_backend') === '') { 34 | return 'No LLM backend configured'; 35 | } 36 | 37 | if ($q === '') { 38 | return 'No question provided'; 39 | } 40 | 41 | return self::getBackend()->textPrompt($q); 42 | } 43 | 44 | public static function genTextResponseWithContext(array $context = []): string 45 | { 46 | if (Config::obtain('llm_backend') === '') { 47 | return 'No LLM backend configured'; 48 | } 49 | 50 | if ($context === []) { 51 | return 'No context provided'; 52 | } 53 | 54 | return self::getBackend()->textPromptWithContext($context); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/LLM/Anthropic.php: -------------------------------------------------------------------------------- 1 | makeRequest([ 16 | [ 17 | 'role' => 'user', 18 | 'content' => $q, 19 | ], 20 | ]); 21 | } 22 | 23 | public function textPromptWithContext(array $context): string 24 | { 25 | $conversation = []; 26 | 27 | if (count($context) > 0) { 28 | foreach ($context as $role => $content) { 29 | $conversation[] = [ 30 | 'role' => $role === 'user' ? 'user' : 'assistant', 31 | 'content' => $content, 32 | ]; 33 | } 34 | } 35 | 36 | return $this->makeRequest($conversation); 37 | } 38 | 39 | private function makeRequest(array $conversation): string 40 | { 41 | if (Config::obtain('anthropic_api_key') === '') { 42 | return 'Anthropic API key not set'; 43 | } 44 | 45 | $api_url = 'https://api.anthropic.com/v1/messages'; 46 | 47 | $headers = [ 48 | 'x-api-key' => Config::obtain('anthropic_api_key'), 49 | 'content-type' => 'application/json', 50 | ]; 51 | 52 | $data = [ 53 | 'model' => Config::obtain('anthropic_model_id'), 54 | 'temperature' => 1, 55 | 'messages' => $conversation, 56 | ]; 57 | 58 | try { 59 | $response = json_decode($this->client->post($api_url, [ 60 | 'headers' => $headers, 61 | 'json' => $data, 62 | 'timeout' => 30, 63 | ])->getBody()->getContents()); 64 | } catch (GuzzleException $e) { 65 | return ''; 66 | } 67 | 68 | return $response->content[0]->text; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Services/LLM/Base.php: -------------------------------------------------------------------------------- 1 | client = new Client(); 16 | } 17 | 18 | abstract public function textPrompt(string $q): string; 19 | 20 | abstract public function textPromptWithContext(array $context): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/LLM/CloudflareWorkersAI.php: -------------------------------------------------------------------------------- 1 | makeRequest([ 16 | [ 17 | 'role' => 'user', 18 | 'content' => $q, 19 | ], 20 | ]); 21 | } 22 | 23 | public function textPromptWithContext(array $context): string 24 | { 25 | return 'This service does not support context'; 26 | } 27 | 28 | private function makeRequest(array $conversation): string 29 | { 30 | if (Config::obtain('cf_workers_ai_account_id') === '' || 31 | Config::obtain('cf_workers_ai_api_token') === '') { 32 | return 'Cloudflare Workers AI Account ID or API Token not set'; 33 | } 34 | 35 | $api_url = 'https://api.cloudflare.com/client/v4/accounts/' . 36 | Config::obtain('cf_workers_ai_account_id') . '/ai/run/' . Config::obtain('cf_workers_ai_model_id'); 37 | 38 | $headers = [ 39 | 'Authorization' => 'Bearer ' . Config::obtain('cf_workers_ai_api_token'), 40 | 'Content-Type' => 'application/json', 41 | ]; 42 | 43 | $data = [ 44 | 'prompt' => $conversation[0]['content'], 45 | ]; 46 | 47 | try { 48 | $response = json_decode($this->client->post($api_url, [ 49 | 'headers' => $headers, 50 | 'json' => $data, 51 | 'timeout' => 30, 52 | ])->getBody()->getContents()); 53 | } catch (GuzzleException $e) { 54 | return ''; 55 | } 56 | 57 | return $response->result->response; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Services/LLM/HuggingFace.php: -------------------------------------------------------------------------------- 1 | makeRequest([ 16 | [ 17 | 'role' => 'user', 18 | 'content' => $q, 19 | ], 20 | ]); 21 | } 22 | 23 | public function textPromptWithContext(array $context): string 24 | { 25 | return 'This service does not support context'; 26 | } 27 | 28 | private function makeRequest(array $conversation): string 29 | { 30 | if (Config::obtain('huggingface_api_key') === '' || 31 | Config::obtain('huggingface_endpoint_url') === '') { 32 | return 'Hugging Face API key or Endpoint URL not set'; 33 | } 34 | 35 | $headers = [ 36 | 'Authorization' => 'Bearer ' . Config::obtain('huggingface_api_key'), 37 | 'Content-Type' => 'application/json', 38 | ]; 39 | 40 | $data = [ 41 | 'inputs' => [ 42 | 'question' => $conversation[0]['content'], 43 | ], 44 | ]; 45 | 46 | try { 47 | $response = json_decode($this->client->post(Config::obtain('huggingface_endpoint_url'), [ 48 | 'headers' => $headers, 49 | 'json' => $data, 50 | 'timeout' => 30, 51 | ])->getBody()->getContents()); 52 | } catch (GuzzleException $e) { 53 | return ''; 54 | } 55 | 56 | return $response->answer; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Services/LLM/OpenAI.php: -------------------------------------------------------------------------------- 1 | makeRequest([ 15 | [ 16 | 'role' => 'user', 17 | 'content' => $q, 18 | ], 19 | ]); 20 | } 21 | 22 | public function textPromptWithContext(array $context): string 23 | { 24 | $conversation = [ 25 | [ 26 | 'role' => 'system', 27 | 'content' => 'You are a helpful assistant.', 28 | ], 29 | ]; 30 | 31 | if (count($context) > 0) { 32 | foreach ($context as $role => $content) { 33 | $conversation[] = [ 34 | 'role' => $role === 'user' ? 'user' : 'assistant', 35 | 'content' => $content, 36 | ]; 37 | } 38 | } 39 | 40 | return $this->makeRequest($conversation); 41 | } 42 | 43 | private function makeRequest(array $conversation): string 44 | { 45 | if (Config::obtain('openai_api_key') === '') { 46 | return 'OpenAI API key not set'; 47 | } 48 | 49 | $client = OpenAISDK::client(Config::obtain('openai_api_key')); 50 | 51 | $response = $client->chat()->create([ 52 | 'model' => Config::obtain('openai_model_id'), 53 | 'temperature' => 1, 54 | 'messages' => $conversation, 55 | ]); 56 | 57 | return $response->choices[0]->message->content; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Services/MFA.php: -------------------------------------------------------------------------------- 1 | createSecret(); 19 | } 20 | 21 | public static function verifyGa($user, string $code): bool 22 | { 23 | $ga = new GoogleAuthenticator(); 24 | return $ga->verifyCode($user->ga_token, $code); 25 | } 26 | 27 | public static function getGaUrl($user): string 28 | { 29 | return 'otpauth://totp/' . 30 | rawurlencode($_ENV['appName'] . ' (' . $user->email . ')') . '?secret=' . $user->ga_token; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Services/Mail/AlibabaCloud.php: -------------------------------------------------------------------------------- 1 | account_name = $configs['alibabacloud_dm_account_name']; 23 | $this->from_alias = $configs['alibabacloud_dm_from_alias']; 24 | 25 | $this->client = new Dm(new OpenApiConfig([ 26 | 'accessKeyId' => $configs['alibabacloud_dm_access_key_id'], 27 | 'accessKeySecret' => $configs['alibabacloud_dm_access_key_secret'], 28 | 'endpoint' => $configs['alibabacloud_dm_endpoint'], 29 | ])); 30 | } 31 | 32 | public function send($to, $subject, $body): void 33 | { 34 | $request = new SingleSendMailRequest([ 35 | 'accountName' => $this->account_name, 36 | 'addressType' => 1, 37 | 'replyToAddress' => false, 38 | 'toAddress' => $to, 39 | 'subject' => $subject, 40 | 'htmlBody' => $body, 41 | 'fromAlias' => $this->from_alias, 42 | ]); 43 | 44 | $this->client->singleSendMail($request); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Mail/Base.php: -------------------------------------------------------------------------------- 1 | mc = new ApiClient(); 21 | $this->mc->setApiKey($configs['mailchimp_key']); 22 | $this->from_email = $configs['mailchimp_from_email']; 23 | $this->from_name = $configs['mailchimp_from_name']; 24 | } 25 | 26 | public function send($to, $subject, $body): void 27 | { 28 | $this->mc->messages->send([ 29 | 'message' => [ 30 | 'html' => $body, 31 | 'subject' => $subject, 32 | 'from_email' => $this->from_email, 33 | 'from_name' => $this->from_name, 34 | 'to' => [['email' => $to]], 35 | ], 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Services/Mail/Mailgun.php: -------------------------------------------------------------------------------- 1 | mg = MG::create($configs['mailgun_key']); 23 | $this->domain = $configs['mailgun_domain']; 24 | $this->sender = $configs['mailgun_sender_name'] . ' <' . $configs['mailgun_sender'] . '>'; 25 | } 26 | 27 | /** 28 | * @throws Exception 29 | * @throws ClientExceptionInterface 30 | */ 31 | public function send($to, $subject, $body): void 32 | { 33 | $this->mg->messages()->send($this->domain, [ 34 | 'from' => $this->sender, 35 | 'to' => $to, 36 | 'subject' => $subject, 37 | 'html' => $body, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/Mail/NullMail.php: -------------------------------------------------------------------------------- 1 | client = new Client($configs['postal_host'], $configs['postal_key']); 21 | $this->message = new Message(); 22 | $this->message->sender($configs['postal_sender']); # 发件邮箱 23 | $this->message->from($configs['postal_name'] . ' <' . $configs['postal_sender'] . '>'); # 发件人 24 | $this->message->replyTo($configs['postal_sender']); 25 | } 26 | 27 | public function send($to, $subject, $body): void 28 | { 29 | $this->message->subject($subject); 30 | $this->message->to($to); 31 | $this->message->plainBody($body); 32 | $this->message->htmlBody($body); 33 | 34 | $this->client->send->message($this->message); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Mail/Resend.php: -------------------------------------------------------------------------------- 1 | resend = RS::client($configs['resend_api_key']); 19 | $this->from = $configs['resend_from']; 20 | } 21 | 22 | public function send($to, $subject, $body): void 23 | { 24 | $this->resend->emails->send([ 25 | 'from' => $this->from, 26 | 'to' => [$to], 27 | 'subject' => $subject, 28 | 'html' => $body, 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/Mail/SendGrid.php: -------------------------------------------------------------------------------- 1 | sg = new SG($configs['sendgrid_key']); 25 | $this->email = new Mail(); 26 | $this->email->setFrom($configs['sendgrid_sender'], $configs['sendgrid_name']); 27 | } 28 | 29 | /** 30 | * @throws TypeException 31 | */ 32 | public function send($to, $subject, $body): void 33 | { 34 | $this->email->setSubject($subject); 35 | $this->email->addTo($to); 36 | $this->email->addContent('text/html', $body); 37 | 38 | $this->sg->send($this->email); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/Mail/Ses.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'key' => $configs['aws_ses_access_key_id'], 21 | 'secret' => $configs['aws_ses_access_key_secret'], 22 | ], 23 | 'region' => $configs['aws_ses_region'], 24 | 'version' => 'latest', 25 | ]); 26 | 27 | $this->ses = $ses; 28 | } 29 | 30 | public function send($to, $subject, $body): void 31 | { 32 | $ses = $this->ses; 33 | $char_set = 'UTF-8'; 34 | 35 | $ses->sendEmail([ 36 | 'Destination' => [ 37 | 'ToAddresses' => [$to], 38 | ], 39 | 'Source' => Config::obtain('aws_ses_sender'), 40 | 'Message' => [ 41 | 'Body' => [ 42 | 'Html' => [ 43 | 'Charset' => $char_set, 44 | 'Data' => $body, 45 | ], 46 | ], 47 | 'Subject' => [ 48 | 'Charset' => $char_set, 49 | 'Data' => $subject, 50 | ], 51 | ], 52 | 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Services/Mail/Smtp.php: -------------------------------------------------------------------------------- 1 | isSMTP(); 24 | $mail->Host = $configs['smtp_host']; 25 | $mail->Port = $configs['smtp_port']; 26 | $mail->SMTPAuth = ! ($configs['smtp_username'] === '' && $configs['smtp_password'] === ''); 27 | $mail->CharSet = 'UTF-8'; 28 | $mail->Username = $configs['smtp_username']; 29 | $mail->Password = $configs['smtp_password']; 30 | $mail->setFrom($configs['smtp_sender'], $configs['smtp_name']); 31 | 32 | if ($configs['smtp_ssl']) { 33 | $mail->SMTPSecure = ($configs['smtp_port'] === '587' ? 'tls' : 'ssl'); 34 | } 35 | 36 | if ($configs['smtp_bbc'] !== '') { 37 | $mail->addBCC($configs['smtp_bbc']); 38 | } 39 | 40 | $this->mail = $mail; 41 | } 42 | 43 | /** 44 | * @throws Exception 45 | */ 46 | public function send($to, $subject, $body): void 47 | { 48 | $mail = $this->mail; 49 | $mail->addAddress($to); // Add a recipient 50 | $mail->isHTML(); 51 | $mail->Subject = $subject; 52 | $mail->Body = $body; 53 | 54 | $mail->send(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/Password.php: -------------------------------------------------------------------------------- 1 | initRedis(); 21 | $token = Tools::genRandomChar(64); 22 | 23 | $redis->setex('password_reset:' . $token, Config::obtain('email_password_reset_ttl'), $email); 24 | 25 | $subject = $_ENV['appName'] . '-重置密码'; 26 | $resetUrl = $_ENV['baseUrl'] . '/password/token/' . $token; 27 | 28 | Mail::send( 29 | $email, 30 | $subject, 31 | 'password_reset.tpl', 32 | [ 33 | 'resetUrl' => $resetUrl, 34 | ] 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Services/Subscribe/Base.php: -------------------------------------------------------------------------------- 1 | 4, 17 | 'sub_name' => $_ENV['appName'], 18 | 'email' => $user->email, 19 | 'user_name' => $user->user_name, 20 | 'class' => $user->class, 21 | 'class_expire_date' => $user->class_expire, 22 | 'total_traffic' => $user->transfer_enable, 23 | 'used_upload_traffic' => $user->u, 24 | 'used_download_traffic' => $user->d, 25 | 'sub_url' => [ 26 | 'sing-box' => $sub_url . '/singbox', 27 | 'clash' => $sub_url . '/clash', 28 | ], 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/Subscribe/SIP002.php: -------------------------------------------------------------------------------- 1 | custom_config, true); 26 | 27 | if ((int) $node_raw->sort === 0) { 28 | $plugin = $node_custom_config['plugin'] ?? ''; 29 | $plugin_option = $node_custom_config['plugin_option'] ?? ''; 30 | 31 | $links .= $user->method . ':' . $user->passwd . '@' . $node_raw->server . ':' . 32 | $user->port . '/?plugin=' . $plugin . '&' . $plugin_option . '#' . 33 | $node_raw->name . PHP_EOL; 34 | } 35 | } 36 | 37 | return $links; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Subscribe/SIP008.php: -------------------------------------------------------------------------------- 1 | custom_config, true); 26 | 27 | if ((int) $node_raw->sort === 0) { 28 | $plugin = $node_custom_config['plugin'] ?? ''; 29 | $plugin_option = $node_custom_config['plugin_option'] ?? ''; 30 | $node = [ 31 | 'id' => $node_raw->id, 32 | 'remarks' => $node_raw->name, 33 | 'server' => $node_raw->server, 34 | 'server_port' => (int) $user->port, 35 | 'password' => $user->passwd, 36 | 'method' => $user->method, 37 | 'plugin' => $plugin, 38 | 'plugin_opts' => $plugin_option, 39 | ]; 40 | } 41 | 42 | $nodes[] = $node; 43 | } 44 | 45 | return json_encode([ 46 | 'version' => 1, 47 | 'servers' => $nodes, 48 | 'bytes_used' => $user->u + $user->d, 49 | 'bytes_remaining' => $user->transfer_enable - $user->u - $user->d, 50 | ]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Services/Subscribe/SS.php: -------------------------------------------------------------------------------- 1 | sort === 0) { 26 | $links .= base64_encode($user->method . ':' . $user->passwd . '@' . $node_raw->server . ':' . $user->port) . '#' . 27 | $node_raw->name . PHP_EOL; 28 | } 29 | } 30 | 31 | return $links; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/SysLog/DBHandler.php: -------------------------------------------------------------------------------- 1 | insert([ 17 | 'user_id' => $record->context['user_id'] ?? 0, 18 | 'ip' => $record->context['ip'] ?? '', 19 | 'message' => $record->message, 20 | 'level' => $record->level, 21 | 'context' => json_encode($record->context), 22 | 'channel' => $record->channel, 23 | 'datetime' => $record->datetime->format('U'), 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Utils/ClassHelper.php: -------------------------------------------------------------------------------- 1 | getClassMap()); 25 | } 26 | } 27 | 28 | public function getClassesByNamespace($namespace): array 29 | { 30 | if (! str_starts_with($namespace, '\\')) { 31 | $namespace = '\\' . $namespace; 32 | } 33 | 34 | $termUpper = strtoupper($namespace); 35 | 36 | return array_filter($this->getClasses(), static function ($class) use ($termUpper) { 37 | $className = strtoupper($class); 38 | if (str_starts_with($className, $termUpper) && 39 | ! str_contains($className, strtoupper('Abstract')) && 40 | ! str_contains($className, strtoupper('Interface')) 41 | ) { 42 | return $class; 43 | } 44 | return false; 45 | }); 46 | } 47 | 48 | public function getClasses(): array 49 | { 50 | $allClasses = []; 51 | 52 | if (! is_null(self::$classes)) { 53 | foreach (self::$classes as $class) { 54 | $allClasses[] = '\\' . $class; 55 | } 56 | } 57 | 58 | return $allClasses; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Utils/Cookie.php: -------------------------------------------------------------------------------- 1 | $value) { 12 | setcookie($key, $value, $time, path: '/', secure: true, httponly: true); 13 | } 14 | } 15 | 16 | public static function setWithDomain(array $arg, int $time, string $domain): void 17 | { 18 | foreach ($arg as $key => $value) { 19 | setcookie($key, $value, $time, path: '/', domain: $domain, secure: true, httponly: true); 20 | } 21 | } 22 | 23 | public static function get(string $key): string 24 | { 25 | return $_COOKIE[$key] ?? ''; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Utils/Hash.php: -------------------------------------------------------------------------------- 1 | self::sha256WithSalt($pass), 48 | 'sha3' => self::sha3WithSalt($pass), 49 | 'argon2i' => password_hash($pass, PASSWORD_ARGON2I), 50 | 'argon2id' => password_hash($pass, PASSWORD_ARGON2ID), 51 | default => password_hash($pass, PASSWORD_BCRYPT), 52 | }; 53 | } 54 | 55 | public static function sha256WithSalt($pwd): string 56 | { 57 | return hash('sha256', $pwd . $_ENV['salt']); 58 | } 59 | 60 | public static function sha3WithSalt($pwd): string 61 | { 62 | return hash('sha3-256', $pwd . $_ENV['salt']); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /storage/framework/smarty/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/storage/framework/smarty/cache/.gitkeep -------------------------------------------------------------------------------- /storage/framework/smarty/compile/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/storage/framework/smarty/compile/.gitkeep -------------------------------------------------------------------------------- /storage/framework/twig/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/storage/framework/twig/cache/.gitkeep -------------------------------------------------------------------------------- /tests/App/Services/CacheTest.php: -------------------------------------------------------------------------------- 1 | 'localhost', 28 | 'port' => '6379', 29 | 'connectTimeout' => '1.0', 30 | 'readTimeout' => '1.0', 31 | 'auth' => [ 32 | 'user' => 'username', 33 | 'pass' => 'password', 34 | ], 35 | 'ssl' => [], 36 | ]; 37 | 38 | $result1 = Cache::getRedisConfig(); 39 | $this->assertEquals($expected1, $result1); 40 | 41 | // Scenario 2: Optional parameters are not set 42 | $_ENV['redis_username'] = ''; 43 | $_ENV['redis_password'] = ''; 44 | $_ENV['redis_ssl'] = false; 45 | 46 | $expected2 = [ 47 | 'host' => 'localhost', 48 | 'port' => '6379', 49 | 'connectTimeout' => '1.0', 50 | 'readTimeout' => '1.0', 51 | ]; 52 | 53 | $result2 = Cache::getRedisConfig(); 54 | $this->assertEquals($expected2, $result2); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/App/Services/FilterTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Filter::checkEmailFilter('test@example.com')); 19 | $_ENV['mail_filter'] = 2; 20 | $_ENV['mail_filter_list'] = ['example.com']; 21 | $this->assertFalse(Filter::checkEmailFilter('test@example.com')); 22 | $this->assertFalse(Filter::checkEmailFilter('invalid_email')); 23 | $_ENV['mail_filter'] = 0; 24 | $this->assertTrue(Filter::checkEmailFilter('test@example.com')); 25 | $_ENV['mail_filter'] = 1; 26 | $_ENV['mail_filter_list'] = ['example.com']; 27 | $this->assertFalse(Filter::checkEmailFilter('test@notexample.com')); 28 | $_ENV['mail_filter'] = 2; 29 | $_ENV['mail_filter_list'] = ['example.com']; 30 | $this->assertTrue(Filter::checkEmailFilter('test@notexample.com')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/App/Services/I18nTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expectedTranslation, $translation); 27 | // non-existing locale 28 | $key = 'non_existent_key'; 29 | 30 | $translation = I18n::trans($key, $lang); 31 | 32 | $this->assertSame($key, $translation); 33 | } 34 | 35 | /** 36 | * @covers App\Services\I18n::getLocaleList 37 | */ 38 | public function testGetLocaleList(): void 39 | { 40 | $expectedLocales = ['en_US', 'ja_JP', 'zh_CN', 'zh_TW']; 41 | 42 | $locales = I18n::getLocaleList(); 43 | 44 | $this->assertSame($expectedLocales, $locales); 45 | } 46 | 47 | /** 48 | * @covers App\Services\I18n::getTranslator 49 | */ 50 | public function testGetTranslatorr(): void 51 | { 52 | $lang = 'en_US'; 53 | 54 | $translator = I18n::getTranslator($lang); 55 | 56 | $this->assertInstanceOf(Translator::class, $translator); 57 | $this->assertSame($lang, $translator->getLocale()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/App/Services/MFATest.php: -------------------------------------------------------------------------------- 1 | assertIsString($token); 21 | $this->assertGreaterThan(0, strlen($token)); 22 | $this->assertEquals(16, strlen($token)); 23 | } 24 | 25 | /** 26 | * @covers App\Services\MFA::verifyGa 27 | */ 28 | public function testVerifyGa() 29 | { 30 | $user = (object) ['ga_token' => 'SECRET']; 31 | $this->assertFalse(MFA::verifyGa($user, '000000')); 32 | $this->assertFalse(MFA::verifyGa($user, 'test')); 33 | $this->assertFalse(MFA::verifyGa($user, '0')); 34 | } 35 | 36 | /** 37 | * @covers App\Services\MFA::getGaUrl 38 | */ 39 | public function testGetGaUrl() 40 | { 41 | $_ENV['appName'] = 'Test'; 42 | $user = (object) ['email' => 'test@example.com', 'ga_token' => 'SECRET']; 43 | $url = MFA::getGaUrl($user); 44 | $this->assertStringContainsString('otpauth://totp/', $url); 45 | $this->assertStringContainsString(rawurlencode('Test' . ' (' . $user->email . ')'), $url); 46 | $this->assertStringContainsString('secret=' . $user->ga_token, $url); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/App/Utils/ClassHelperTest.php: -------------------------------------------------------------------------------- 1 | classHelper = new ClassHelper(); 16 | } 17 | 18 | /** 19 | * @covers App\Utils\ClassHelper::getClassesByNamespace 20 | */ 21 | public function testGetClassesByNamespace(): void 22 | { 23 | $namespace = 'App\\Utils'; 24 | $classes = $this->classHelper->getClassesByNamespace($namespace); 25 | 26 | $this->assertIsArray($classes); 27 | $this->assertContains('\App\Utils\ClassHelper', $classes); 28 | } 29 | 30 | /** 31 | * @covers App\Utils\ClassHelper::getClasses 32 | */ 33 | public function testGetClasses(): void 34 | { 35 | $classes = $this->classHelper->getClasses(); 36 | 37 | $this->assertIsArray($classes); 38 | $this->assertContains('\App\Utils\ClassHelper', $classes); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/App/Utils/CookieTest.php: -------------------------------------------------------------------------------- 1 | 'testValue']; 17 | $time = time() + 3600; 18 | 19 | Cookie::set($data, $time); 20 | 21 | $this->assertEquals('testValue', $_COOKIE['testKey']); 22 | } 23 | 24 | /** 25 | * @covers App\Utils\Cookie::setWithDomain 26 | */ 27 | public function testSetWithDomain(): void 28 | { 29 | $data = ['testKey' => 'testValue']; 30 | $time = time() + 3600; 31 | $domain = 'localhost'; 32 | 33 | Cookie::setWithDomain($data, $time, $domain); 34 | 35 | $this->assertEquals('testValue', $_COOKIE['testKey']); 36 | } 37 | 38 | /** 39 | * @covers App\Utils\Cookie::get 40 | */ 41 | public function testGet(): void 42 | { 43 | $data = ['testKey' => 'testValue']; 44 | $time = time() + 3600; 45 | 46 | Cookie::set($data, $time); 47 | 48 | $this->assertEquals('testValue', Cookie::get('testKey')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/testDir/emptyDir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/tests/testDir/emptyDir/.gitkeep -------------------------------------------------------------------------------- /tests/testDir/file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/tests/testDir/file1 -------------------------------------------------------------------------------- /tests/testDir/file2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/tests/testDir/file2 -------------------------------------------------------------------------------- /tests/testDir/file3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekQuerxy/SSPanel-Uim/a9e14cfdce63f6f315749cda77da05de43c185f5/tests/testDir/file3 -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | cat << "EOF" 4 | Usage: 5 | ./update.sh dev --> Upgrade to the latest development version 6 | ./update.sh release $release_version $db_version --> Upgrade to the release version with the specified database version 7 | ./update.sh release-nogit --> Upgrade to the current release version without git(You will need to manually download the latest release version) 8 | EOF 9 | 10 | do_update_sspanel_dev(){ 11 | git pull origin dev 12 | git reset --hard origin/dev 13 | git fetch --prune --prune-tags 14 | rm -r storage/framework/smarty/compile/* 15 | php composer.phar install --no-dev 16 | php composer.phar selfupdate 17 | php xcat Update 18 | php xcat Tool importSetting 19 | php xcat Migration latest 20 | } 21 | 22 | do_update_sspanel_release(){ 23 | tag=$1 24 | db_version=$2 25 | git pull --tags 26 | git reset --hard $tag 27 | rm -r storage/framework/smarty/compile/* 28 | php composer.phar install --no-dev 29 | php composer.phar selfupdate 30 | php xcat Update 31 | php xcat Tool importSetting 32 | php xcat Migration $db_version 33 | } 34 | 35 | do_update_sspanel_release_nogit(){ 36 | rm -r storage/framework/smarty/compile/* 37 | php composer.phar install --no-dev 38 | php composer.phar selfupdate 39 | php xcat Update 40 | php xcat Tool importSetting 41 | php xcat Migration latest 42 | } 43 | 44 | if [[ $1 == "dev" ]]; then 45 | do_update_sspanel_dev 46 | exit 0 47 | fi 48 | 49 | if [[ $1 == "release" ]]; then 50 | if [[ $2 == "" ]]; then 51 | echo "Error: The release version cannot be empty!" 52 | exit 1 53 | fi 54 | 55 | if [[ $3 == "" ]]; then 56 | echo "Error: The database version cannot be empty!" 57 | exit 1 58 | fi 59 | 60 | do_update_sspanel_release $2 $3 61 | exit 0 62 | fi 63 | 64 | if [[ $1 == "release-nogit" ]]; then 65 | do_update_sspanel_release_nogit 66 | exit 0 67 | fi 68 | -------------------------------------------------------------------------------- /xcat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | description) . PHP_EOL; 43 | } 44 | return; 45 | } 46 | 47 | $classPath = '\\App\\Command\\' . $argv[1]; 48 | if (class_exists($classPath)) { 49 | $trigger = new $classPath($argv); 50 | $trigger->boot(); 51 | } else { 52 | echo 'Unable to load class: ' . $classPath . PHP_EOL; 53 | } 54 | --------------------------------------------------------------------------------