From 194287554ebb776431ebc34c8145c7b92b8c4048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Wed, 18 Sep 2024 01:43:14 +0800 Subject: [PATCH] refactor: migrate to chi framework (#165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 重构部分完成 * fix: 添加.gitkeep * fix: build * fix: lint * fix: lint * chore(deps): Update module github.com/go-playground/validator/v10 to v10.22.1 (#162) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update module gorm.io/gorm to v1.25.12 (#161) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update module golang.org/x/net to v0.29.0 (#159) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * workflow: 更新工作流 * workflow: test new download * feat: merge frontend project * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: fix frontend build * workflow: update to ubuntu-24.04 * workflow: rename build-* * workflow: 修改fetch-depth * chore(deps): Update dependency eslint to v9 (#164) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(frontend): update dependences * chore(frontend): fix lint * chore(frontend): fix lint * workflow: add govulncheck * workflow: disable nilaway * feat: 使用新的压缩解压库 * fix: 测试 * fix: 测试 * fix: 测试 * feat: 添加ntp包 * chore(deps): Lock file maintenance (#168) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update module github.com/go-resty/resty/v2 to v2.15.0 (#167) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update dependency @iconify/json to v2.2.249 (#169) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * feat: 添加限流器 * feat: 调整登录限流 * feat: 证书 * fix: lint * feat: 证书dns * feat: 证书acme账号 * fix: 修改UserID导致的一系列问题 * feat: 低配版任务队列 * feat: 队列完成 * fix: lint * fix: lint * fix: swagger和前端路由 * fix: 去掉ntp测试 * feat: 完成插件接口 * feat: 完成cron * feat: 完成safe * chore(deps): Update dependency vue to v3.5.6 (#170) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update dependency @vueuse/core to v11.1.0 (#171) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update dependency vite to v5.4.6 (#173) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): Update unocss monorepo to v0.62.4 (#172) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: update renovate config * feat: 新的firewall客户端 * fix: lint * feat: firewall完成 * feat: ssh完成 * feat: 容器完成1/2 * feat: 容器完成 * feat: 文件完成 * feat: systemctl及设置 * fix: windows编译 * fix: session not work * fix: migrate not work * feat: 前端路由 * feat: 初步支持cli --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .air.toml | 69 + .github/workflows/backend.yml | 55 + .github/workflows/build.yml | 32 - .github/workflows/codecov.yml | 13 +- .github/workflows/frontend.yml | 42 + .github/workflows/goreleaser.yml | 27 +- .github/workflows/issue-auto-reply.yml | 9 +- .github/workflows/lint.yml | 71 +- .github/workflows/test.yml | 19 +- .gitignore | 21 +- .gitlab-ci.yml | 90 - .goreleaser.yaml | 18 +- README.md | 2 +- app/console/commands/cert_renew.go | 78 - app/console/commands/monitoring.go | 91 - app/console/commands/panel.go | 732 -- app/console/commands/panel_task.go | 79 - app/console/kernel.go | 29 - app/http/controllers/cert_controller.go | 706 -- app/http/controllers/container_controller.go | 1011 --- app/http/controllers/cron_controller.go | 296 - app/http/controllers/file_controller.go | 519 -- app/http/controllers/info_controller.go | 361 - app/http/controllers/plugin_controller.go | 207 - app/http/controllers/safe_controller.go | 377 -- app/http/controllers/setting_controller.go | 290 - app/http/controllers/ssh_controller.go | 157 - app/http/controllers/swagger_controller.go | 31 - app/http/controllers/system_controller.go | 211 - app/http/controllers/task_controller.go | 88 - app/http/controllers/user_controller.go | 123 - app/http/controllers/website_controller.go | 646 -- app/http/kernel.go | 23 - app/http/middleware/entrance.go | 39 - app/http/middleware/log.go | 20 - app/http/middleware/must_install.go | 93 - app/http/middleware/session.go | 53 - app/http/middleware/static.go | 16 - app/http/middleware/status.go | 40 - app/http/requests/cert/cert_deploy.go | 41 - .../requests/cert/cert_show_and_destroy.go | 36 - app/http/requests/cert/cert_store.go | 50 - app/http/requests/cert/cert_update.go | 52 - .../requests/cert/dns_show_and_destroy.go | 36 - app/http/requests/cert/dns_store.go | 47 - app/http/requests/cert/dns_update.go | 50 - app/http/requests/cert/obtain.go | 36 - app/http/requests/cert/renew.go | 36 - .../requests/cert/user_show_and_destroy.go | 36 - app/http/requests/cert/user_store.go | 44 - app/http/requests/cert/user_update.go | 46 - app/http/requests/common/paginate.go | 41 - .../requests/container/container_create.go | 85 - .../requests/container/container_rename.go | 38 - .../requests/container/container_update.go | 74 - app/http/requests/container/id.go | 36 - app/http/requests/container/image_pull.go | 42 - .../container/network_connect_disconnect.go | 38 - app/http/requests/container/network_create.go | 48 - app/http/requests/container/volume_create.go | 44 - app/http/requests/file/archive.go | 39 - app/http/requests/file/copy.go | 38 - app/http/requests/file/exist.go | 36 - app/http/requests/file/move.go | 38 - app/http/requests/file/not_exist.go | 36 - app/http/requests/file/permission.go | 42 - app/http/requests/file/save.go | 38 - app/http/requests/file/search.go | 38 - app/http/requests/file/un_archive.go | 38 - app/http/requests/file/upload.go | 40 - app/http/requests/plugins/frp/service.go | 36 - .../requests/plugins/frp/update_config.go | 38 - .../requests/plugins/gitea/update_config.go | 36 - .../plugins/podman/update_registry_config.go | 36 - .../plugins/podman/update_storage_config.go | 36 - app/http/requests/plugins/rsync/create.go | 46 - app/http/requests/plugins/rsync/update.go | 46 - .../requests/plugins/rsync/update_config.go | 36 - app/http/requests/setting/https.go | 40 - app/http/requests/setting/update.go | 57 - app/http/requests/user/login.go | 42 - app/http/requests/website/add.go | 54 - app/http/requests/website/delete.go | 42 - app/http/requests/website/delete_backup.go | 36 - app/http/requests/website/id.go | 38 - app/http/requests/website/restore_backup.go | 40 - app/http/requests/website/save_config.go | 100 - app/jobs/process_task.go | 74 - app/models/cert.go | 22 - app/models/cert_dns.go | 16 - app/models/cert_user.go | 15 - app/models/cron.go | 13 - app/models/database.go | 14 - app/models/monitor.go | 12 - app/models/plugin.go | 11 - app/models/setting.go | 23 - app/models/task.go | 18 - app/models/user.go | 10 - app/models/website.go | 15 - app/plugins/README.md | 3 - app/plugins/fail2ban/controller.go | 342 - app/plugins/fail2ban/main.go | 39 - app/plugins/frp_controller.go | 72 - app/plugins/gitea_controller.go | 63 - app/plugins/loader/loader.go | 17 - app/plugins/main.go | 6 - app/plugins/mysql_controller.go | 506 -- app/plugins/openresty/controller.go | 187 - app/plugins/openresty/main.go | 37 - app/plugins/php_controller.go | 246 - app/plugins/phpmyadmin_controller.go | 127 - app/plugins/podman_controller.go | 109 - app/plugins/postgresql_controller.go | 491 -- app/plugins/pureftpd_controller.go | 179 - app/plugins/redis_controller.go | 93 - app/plugins/rsync_controller.go | 299 - app/plugins/s3fs_controller.go | 181 - app/plugins/supervisor_controller.go | 336 - app/plugins/toolbox_controller.go | 241 - app/providers/app_service_provider.go | 16 - app/providers/auth_service_provider.go | 16 - app/providers/console_service_provider.go | 21 - app/providers/database_service_provider.go | 22 - app/providers/event_service_provider.go | 21 - app/providers/plugin_service_provider.go | 20 - app/providers/queue_service_provider.go | 28 - app/providers/route_service_provider.go | 48 - app/providers/validation_service_provider.go | 31 - app/rules/exists.go | 55 - app/rules/not_exists.go | 55 - app/rules/path_exist.go | 37 - app/rules/path_not_exist.go | 37 - bootstrap/app.go | 25 - cmd/README.md | 3 + scripts/panel.sh => cmd/app/main.go | 25 +- scripts/frp/uninstall.sh => cmd/cli/main.go | 51 +- config/README.md | 5 + config/app.go | 123 - config/auth.go | 36 - config/cache.go | 37 - config/config.example.yml | 12 + config/cors.go | 25 - config/database.go | 79 - config/filesystems.go | 32 - config/hashing.go | 40 - config/http.go | 47 - config/jwt.go | 41 - config/logging.go | 50 - config/mail.go | 43 - config/panel.go | 15 - config/queue.go | 23 - config/session.go | 85 - docs/README.md | 3 + docs/docs.go | 5859 +---------------- docs/swagger.json | 5859 +---------------- docs/swagger.yaml | 3631 +--------- embed/frontend/.gitignore | 1 - go.mod | 242 +- go.sum | 884 +-- internal/app/global.go | 29 + internal/backup.go | 18 - internal/biz/cert.go | 40 + internal/biz/cert_account.go | 29 + internal/biz/cert_dns.go | 27 + internal/biz/container.go | 28 + internal/biz/container_image.go | 17 + internal/biz/container_network.go | 18 + internal/biz/container_volume.go | 16 + internal/biz/cron.go | 30 + internal/biz/database.go | 16 + internal/biz/firewall.go | 4 + internal/biz/monitor.go | 22 + internal/biz/plugin.go | 30 + internal/biz/safe.go | 8 + internal/biz/setting.go | 39 + internal/biz/ssh.go | 8 + internal/biz/task.go | 30 + internal/biz/user.go | 22 + internal/biz/website.go | 36 + internal/bootstrap/app.go | 21 + internal/bootstrap/conf.go | 24 + internal/bootstrap/db.go | 40 + internal/bootstrap/http.go | 33 + internal/bootstrap/queue.go | 11 + internal/bootstrap/session.go | 31 + internal/bootstrap/validator.go | 26 + internal/cert.go | 27 - internal/container.go | 54 - internal/cron.go | 8 - internal/data/cert.go | 273 + internal/data/cert_account.go | 154 + internal/data/cert_dns.go | 57 + internal/data/container.go | 235 + internal/data/container_image.go | 97 + internal/data/container_network.go | 104 + internal/data/container_volume.go | 74 + internal/data/cron.go | 253 + internal/data/monitor.go | 67 + internal/data/plugin.go | 226 + internal/data/safe.go | 120 + internal/data/setting.go | 68 + internal/data/ssh.go | 54 + internal/data/task.go | 39 + internal/data/user.go | 51 + internal/data/website.go | 784 +++ {embed => internal/embed}/embed.go | 0 .../embed/frontend}/.gitignore | 0 {embed => internal/embed}/website/404.html | 0 {embed => internal/embed}/website/index.html | 0 internal/http/middleware/middleware.go | 23 + internal/http/middleware/must_login.go | 47 + internal/http/middleware/throttle.go | 27 + internal/http/request/cert.go | 25 + internal/http/request/cert_account.go | 18 + internal/http/request/cert_dns.go | 16 + internal/http/request/common.go | 5 + internal/http/request/container.go | 33 + internal/http/request/container_image.go | 12 + internal/http/request/container_network.go | 21 + internal/http/request/container_volume.go | 14 + internal/http/request/cron.go | 24 + internal/http/request/file.go | 54 + internal/http/request/firewall.go | 10 + internal/http/request/monitor.go | 11 + internal/http/request/paginate.go | 32 + internal/http/request/plugin.go | 10 + internal/http/request/request.go | 21 + internal/http/request/safe.go | 10 + internal/http/request/setting.go | 16 + internal/http/request/ssh.go | 8 + internal/http/request/systemctl.go | 5 + internal/http/request/user.go | 14 + internal/http/request/website.go | 76 + internal/job/process_task.go | 52 + internal/migration/migration.go | 5 + internal/migration/v1.go | 44 + internal/php.go | 25 - internal/plugin.go | 16 - internal/plugin/init.go | 12 + internal/plugin/openresty/init.go | 22 + internal/plugin/openresty/service.go | 1 + internal/route/http.go | 300 + internal/service/backup.go | 34 + internal/service/base.go | 138 + internal/service/cert.go | 341 + internal/service/cert_account.go | 102 + internal/service/cert_dns.go | 102 + internal/service/container.go | 279 + internal/service/container_image.go | 122 + internal/service/container_network.go | 170 + internal/service/container_volume.go | 133 + internal/service/cron.go | 132 + internal/service/file.go | 355 + internal/service/file_windows.go | 352 + internal/service/firewall.go | 104 + internal/service/info.go | 387 ++ .../service/monitor.go | 114 +- internal/service/plugin.go | 160 + internal/service/safe.go | 73 + internal/service/setting.go | 44 + internal/service/ssh.go | 127 + internal/service/systemctl.go | 138 + internal/service/task.go | 116 + internal/service/user.go | 117 + internal/service/website.go | 301 + internal/services/backup.go | 331 - internal/services/cert.go | 508 -- internal/services/container.go | 374 -- internal/services/cron.go | 48 - internal/services/php.go | 356 - internal/services/plugin.go | 247 - internal/services/setting.go | 50 - internal/services/task.go | 42 - internal/services/user.go | 35 - internal/services/website.go | 630 -- internal/setting.go | 7 - internal/task.go | 6 - internal/user.go | 8 - internal/website.go | 17 - lang/en.json | 204 - lang/zh_CN.json | 204 - main.go | 70 - panel-example.conf | 10 - pkg/acme/acme.go | 2 +- pkg/acme/client.go | 2 +- pkg/acme/solvers.go | 4 +- pkg/arch/arch.go | 21 + pkg/db/mysql.go | 2 +- pkg/db/mysql_tools.go | 4 +- pkg/db/postgres.go | 12 +- pkg/firewall/consts.go | 24 + pkg/firewall/firewall.go | 218 + pkg/h/request.go | 29 - pkg/h/response.go | 68 - pkg/io/file.go | 149 +- pkg/io/io_test.go | 208 + pkg/io/path.go | 13 +- pkg/migrate/migrate.go | 22 - pkg/migrate/migrations.go | 42 - pkg/ntp/ntp.go | 119 + pkg/ntp/ntp_test.go | 49 + pkg/pluginloader/plugin.go | 44 + pkg/queue/job.go | 16 + pkg/queue/queue.go | 92 + pkg/queue/queue_test.go | 140 + pkg/shell/exec.go | 12 +- pkg/ssh/ssh.go | 2 +- pkg/systemctl/service.go | 2 +- pkg/tools/tools.go | 186 +- pkg/tools/tools_test.go | 4 +- pkg/types/common.go | 25 + pkg/types/plugin.go | 22 +- renovate.json | 13 +- routes/api.go | 234 - routes/plugin.go | 172 - scripts/calculate_j.sh | 59 - scripts/fail2ban/install.sh | 83 - scripts/fail2ban/uninstall.sh | 41 - scripts/fail2ban/update.sh | 42 - scripts/frp/install.sh | 102 - scripts/frp/update.sh | 82 - scripts/gitea/install.sh | 143 - scripts/gitea/uninstall.sh | 35 - scripts/gitea/update.sh | 72 - scripts/install_panel.sh | 357 - scripts/mysql/install.sh | 352 - scripts/mysql/uninstall.sh | 39 - scripts/mysql/update.sh | 139 - scripts/openresty/install.sh | 685 -- scripts/openresty/uninstall.sh | 32 - scripts/panel.service | 20 - scripts/php/install.sh | 280 - scripts/php/uninstall.sh | 42 - scripts/php_extensions/Swow.sh | 99 - scripts/php_extensions/Zend OPcache.sh | 69 - scripts/php_extensions/igbinary.sh | 99 - scripts/php_extensions/imagick.sh | 111 - scripts/php_extensions/ionCube Loader.sh | 80 - scripts/php_extensions/official.sh | 162 - scripts/php_extensions/redis.sh | 99 - scripts/php_extensions/swoole.sh | 99 - scripts/phpmyadmin/install.sh | 135 - scripts/phpmyadmin/uninstall.sh | 31 - scripts/podman/install.sh | 49 - scripts/podman/uninstall.sh | 43 - scripts/podman/update.sh | 42 - scripts/postgresql/install.sh | 200 - scripts/postgresql/uninstall.sh | 38 - scripts/postgresql/update.sh | 113 - scripts/pureftpd/install.sh | 131 - scripts/pureftpd/uninstall.sh | 34 - scripts/pureftpd/update.sh | 86 - scripts/redis/install.sh | 132 - scripts/redis/uninstall.sh | 39 - scripts/redis/update.sh | 135 - scripts/rsync/install.sh | 84 - scripts/rsync/uninstall.sh | 43 - scripts/rsync/update.sh | 34 - scripts/s3fs/install.sh | 39 - scripts/s3fs/uninstall.sh | 34 - scripts/s3fs/update.sh | 34 - scripts/supervisor/install.sh | 44 - scripts/supervisor/uninstall.sh | 38 - scripts/supervisor/update.sh | 34 - scripts/uninstall_panel.sh | 98 - scripts/update_panel.sh | 154 - storage/.gitignore | 3 - storage/README.md | 3 + .../.gitignore => storage/logs/.gitkeep | 0 storage/sessions/.gitkeep | 0 storage/temp/.gitignore | 2 - tests/setting/setting_test.go | 48 - tests/test_case.go | 15 - tests/user/user_test.go | 49 - web/.env.development | 17 + web/.env.production | 20 + web/.eslintrc-auto-import.json | 76 + web/.eslintrc.cjs | 17 + web/.gitignore | 32 + web/.prettierrc.json | 8 + web/README.md | 11 + web/build/config/define.ts | 13 + web/build/config/index.ts | 2 + web/build/config/proxy.ts | 16 + web/build/plugins/html.ts | 16 + web/build/plugins/index.ts | 28 + web/build/plugins/unplugin.ts | 33 + web/build/utils.ts | 38 + web/env.d.ts | 10 + web/index.html | 31 + web/package.json | 72 + web/pnpm-lock.yaml | 4618 +++++++++++++ web/public/favicon.png | Bin 0 -> 1265 bytes web/public/loading/index.css | 91 + web/public/loading/index.js | 9 + web/public/loading/logo.png | Bin 0 -> 10253 bytes web/public/robots.txt | 2 + web/settings/.gitignore | 1 + web/settings/proxy-config.ts.example | 18 + web/settings/theme.json | 25 + web/src/App.vue | 11 + web/src/api/panel/cert/index.ts | 59 + web/src/api/panel/container/index.ts | 106 + web/src/api/panel/cron/index.ts | 22 + web/src/api/panel/file/index.ts | 59 + web/src/api/panel/info/index.ts | 28 + web/src/api/panel/monitor/index.ts | 18 + web/src/api/panel/plugin/index.ts | 23 + web/src/api/panel/safe/index.ts | 34 + web/src/api/panel/setting/index.ts | 15 + web/src/api/panel/ssh/index.ts | 14 + web/src/api/panel/system/service/index.ts | 29 + web/src/api/panel/task/index.ts | 15 + web/src/api/panel/user/index.ts | 17 + web/src/api/panel/website/index.ts | 50 + web/src/api/plugins/fail2ban/index.ts | 24 + web/src/api/plugins/frp/index.ts | 11 + web/src/api/plugins/gitea/index.ts | 10 + web/src/api/plugins/mysql/index.ts | 63 + web/src/api/plugins/openresty/index.ts | 16 + web/src/api/plugins/php/index.ts | 41 + web/src/api/plugins/phpmyadmin/index.ts | 15 + web/src/api/plugins/podman/index.ts | 15 + web/src/api/plugins/postgresql/index.ts | 57 + web/src/api/plugins/pureftpd/index.ts | 22 + web/src/api/plugins/redis/index.ts | 12 + web/src/api/plugins/rsync/index.ts | 22 + web/src/api/plugins/s3fs/index.ts | 12 + web/src/api/plugins/supervisor/index.ts | 48 + web/src/api/plugins/toolbox/index.ts | 28 + web/src/assets/images/404.webp | Bin 0 -> 14344 bytes web/src/assets/images/login_banner.png | Bin 0 -> 415646 bytes web/src/assets/images/login_bg.webp | Bin 0 -> 6944 bytes web/src/assets/images/logo.png | Bin 0 -> 7269 bytes web/src/components/common/AppFooter.vue | 48 + web/src/components/common/AppProvider.vue | 59 + web/src/components/common/CodeEditor.vue | 116 + web/src/components/common/CronSelect.vue | 417 ++ web/src/components/custom/TheIcon.vue | 18 + web/src/components/page/AppPage.vue | 18 + web/src/components/page/CommonPage.vue | 40 + web/src/i18n/en.json | 402 ++ web/src/i18n/i18n.ts | 27 + web/src/i18n/zh_CN.json | 402 ++ web/src/layout/AppMain.vue | 11 + web/src/layout/IndexView.vue | 58 + web/src/layout/header/IndexView.vue | 21 + .../layout/header/components/BreadCrumb.vue | 63 + .../layout/header/components/FullScreen.vue | 12 + .../layout/header/components/MenuCollapse.vue | 12 + .../layout/header/components/ReloadPage.vue | 15 + .../layout/header/components/ThemeMode.vue | 12 + .../layout/header/components/UserAvatar.vue | 46 + web/src/layout/sidebar/IndexView.vue | 9 + .../layout/sidebar/components/SideLogo.vue | 19 + .../layout/sidebar/components/SideMenu.vue | 137 + web/src/layout/tab/IndexView.vue | 115 + web/src/layout/tab/components/ContextMenu.vue | 120 + web/src/main.ts | 51 + web/src/router/guard/index.ts | 10 + web/src/router/guard/page-loading-guard.ts | 17 + web/src/router/guard/page-title-guard.ts | 12 + web/src/router/guard/plugin-install-guard.ts | 35 + web/src/router/index.ts | 55 + web/src/router/routes/index.ts | 43 + web/src/store/index.ts | 8 + web/src/store/modules/app/index.ts | 22 + web/src/store/modules/index.ts | 5 + web/src/store/modules/permission/helpers.ts | 32 + web/src/store/modules/permission/index.ts | 30 + web/src/store/modules/tab/helpers.ts | 6 + web/src/store/modules/tab/index.ts | 65 + web/src/store/modules/theme/helpers.ts | 82 + web/src/store/modules/theme/index.ts | 70 + web/src/store/modules/user/index.ts | 56 + web/src/styles/index.scss | 80 + web/src/styles/reset.css | 40 + web/src/utils/auth/index.ts | 1 + web/src/utils/auth/router.ts | 17 + web/src/utils/common/color.ts | 135 + web/src/utils/common/common.ts | 24 + web/src/utils/common/crypto.ts | 24 + web/src/utils/common/icon.ts | 13 + web/src/utils/common/index.ts | 6 + web/src/utils/common/is.ts | 96 + web/src/utils/common/naiveTools.ts | 19 + web/src/utils/event/index.ts | 35 + web/src/utils/file/index.ts | 332 + web/src/utils/http/helpers.ts | 41 + web/src/utils/http/index.ts | 19 + web/src/utils/http/interceptors.ts | 85 + web/src/utils/index.ts | 6 + web/src/utils/storage/index.ts | 2 + web/src/utils/storage/local.ts | 55 + web/src/utils/storage/session.ts | 23 + web/src/views/cert/CertView.vue | 682 ++ web/src/views/cert/DNSView.vue | 380 ++ web/src/views/cert/IndexView.vue | 23 + web/src/views/cert/UserView.vue | 353 + web/src/views/cert/route.ts | 25 + web/src/views/cert/types.ts | 56 + web/src/views/container/ContainerCreate.vue | 361 + web/src/views/container/ContainerView.vue | 496 ++ web/src/views/container/ImageView.vue | 234 + web/src/views/container/IndexView.vue | 27 + web/src/views/container/NetworkView.vue | 328 + web/src/views/container/VolumeView.vue | 227 + web/src/views/container/route.ts | 25 + web/src/views/container/types.ts | 85 + web/src/views/cron/IndexView.vue | 437 ++ web/src/views/cron/route.ts | 25 + web/src/views/cron/types.ts | 11 + web/src/views/error-page/NotFound.vue | 16 + web/src/views/file/ArchiveModal.vue | 100 + web/src/views/file/EditModal.vue | 35 + web/src/views/file/IndexView.vue | 39 + web/src/views/file/ListTable.vue | 412 ++ web/src/views/file/PathInput.vue | 155 + web/src/views/file/PermissionModal.vue | 128 + web/src/views/file/ToolBar.vue | 161 + web/src/views/file/UploadModal.vue | 54 + web/src/views/file/route.ts | 25 + web/src/views/file/types.ts | 5 + web/src/views/home/IndexView.vue | 645 ++ web/src/views/home/UpdateView.vue | 86 + web/src/views/home/route.ts | 38 + web/src/views/home/types.ts | 187 + web/src/views/login/IndexView.vue | 140 + web/src/views/monitor/IndexView.vue | 541 ++ web/src/views/monitor/route.ts | 25 + web/src/views/monitor/types.ts | 37 + web/src/views/plugin/IndexView.vue | 251 + web/src/views/plugin/route.ts | 25 + web/src/views/plugin/types.ts | 11 + web/src/views/plugins/fail2ban/IndexView.vue | 495 ++ web/src/views/plugins/fail2ban/route.ts | 23 + web/src/views/plugins/fail2ban/types.ts | 8 + web/src/views/plugins/frp/IndexView.vue | 229 + web/src/views/plugins/frp/route.ts | 23 + web/src/views/plugins/gitea/IndexView.vue | 148 + web/src/views/plugins/gitea/route.ts | 23 + web/src/views/plugins/mysql/IndexView.vue | 910 +++ web/src/views/plugins/mysql/types.ts | 14 + web/src/views/plugins/mysql57/route.ts | 23 + web/src/views/plugins/mysql80/route.ts | 23 + web/src/views/plugins/mysql84/route.ts | 23 + web/src/views/plugins/openresty/IndexView.vue | 278 + web/src/views/plugins/openresty/route.ts | 23 + web/src/views/plugins/openresty/types.ts | 9 + web/src/views/plugins/php/IndexView.vue | 409 ++ web/src/views/plugins/php74/route.ts | 24 + web/src/views/plugins/php80/route.ts | 24 + web/src/views/plugins/php81/route.ts | 24 + web/src/views/plugins/php82/route.ts | 24 + web/src/views/plugins/php83/route.ts | 24 + .../views/plugins/phpmyadmin/IndexView.vue | 157 + web/src/views/plugins/phpmyadmin/route.ts | 23 + web/src/views/plugins/podman/IndexView.vue | 194 + web/src/views/plugins/podman/route.ts | 23 + .../views/plugins/postgresql/IndexView.vue | 841 +++ web/src/views/plugins/postgresql/types.ts | 13 + web/src/views/plugins/postgresql15/route.ts | 23 + web/src/views/plugins/postgresql16/route.ts | 23 + web/src/views/plugins/pureftpd/IndexView.vue | 361 + web/src/views/plugins/pureftpd/route.ts | 23 + web/src/views/plugins/pureftpd/types.ts | 4 + web/src/views/plugins/redis/IndexView.vue | 175 + web/src/views/plugins/redis/route.ts | 23 + web/src/views/plugins/rsync/IndexView.vue | 458 ++ web/src/views/plugins/rsync/route.ts | 23 + web/src/views/plugins/rsync/types.ts | 9 + web/src/views/plugins/s3fs/IndexView.vue | 186 + web/src/views/plugins/s3fs/route.ts | 23 + web/src/views/plugins/s3fs/types.ts | 6 + .../views/plugins/supervisor/IndexView.vue | 585 ++ web/src/views/plugins/supervisor/route.ts | 23 + web/src/views/plugins/supervisor/types.ts | 6 + web/src/views/plugins/toolbox/IndexView.vue | 173 + web/src/views/plugins/toolbox/route.ts | 23 + web/src/views/safe/IndexView.vue | 294 + web/src/views/safe/route.ts | 25 + web/src/views/safe/types.ts | 4 + web/src/views/setting/IndexView.vue | 21 + web/src/views/setting/SettingBase.vue | 116 + web/src/views/setting/SettingHttps.vue | 46 + web/src/views/setting/route.ts | 25 + web/src/views/ssh/IndexView.vue | 164 + web/src/views/ssh/route.ts | 25 + web/src/views/task/IndexView.vue | 256 + web/src/views/task/route.ts | 25 + web/src/views/task/types.ts | 9 + web/src/views/website/EditView.vue | 428 ++ web/src/views/website/IndexView.vue | 783 +++ web/src/views/website/route.ts | 37 + web/src/views/website/types.ts | 47 + web/tsconfig.app.json | 42 + web/tsconfig.json | 11 + web/tsconfig.node.json | 21 + web/types/axios.d.ts | 12 + web/types/env.d.ts | 20 + web/types/global.d.ts | 6 + web/types/router.d.ts | 25 + web/types/shims.d.ts | 5 + web/types/theme.d.ts | 50 + web/uno.config.ts | 66 + web/vite.config.ts | 41 + 606 files changed, 36077 insertions(+), 38762 deletions(-) create mode 100644 .air.toml create mode 100644 .github/workflows/backend.yml delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/frontend.yml delete mode 100644 .gitlab-ci.yml delete mode 100644 app/console/commands/cert_renew.go delete mode 100644 app/console/commands/monitoring.go delete mode 100644 app/console/commands/panel.go delete mode 100644 app/console/commands/panel_task.go delete mode 100644 app/console/kernel.go delete mode 100644 app/http/controllers/cert_controller.go delete mode 100644 app/http/controllers/container_controller.go delete mode 100644 app/http/controllers/cron_controller.go delete mode 100644 app/http/controllers/file_controller.go delete mode 100644 app/http/controllers/info_controller.go delete mode 100644 app/http/controllers/plugin_controller.go delete mode 100644 app/http/controllers/safe_controller.go delete mode 100644 app/http/controllers/setting_controller.go delete mode 100644 app/http/controllers/ssh_controller.go delete mode 100644 app/http/controllers/swagger_controller.go delete mode 100644 app/http/controllers/system_controller.go delete mode 100644 app/http/controllers/task_controller.go delete mode 100644 app/http/controllers/user_controller.go delete mode 100644 app/http/controllers/website_controller.go delete mode 100644 app/http/kernel.go delete mode 100644 app/http/middleware/entrance.go delete mode 100644 app/http/middleware/log.go delete mode 100644 app/http/middleware/must_install.go delete mode 100644 app/http/middleware/session.go delete mode 100644 app/http/middleware/static.go delete mode 100644 app/http/middleware/status.go delete mode 100644 app/http/requests/cert/cert_deploy.go delete mode 100644 app/http/requests/cert/cert_show_and_destroy.go delete mode 100644 app/http/requests/cert/cert_store.go delete mode 100644 app/http/requests/cert/cert_update.go delete mode 100644 app/http/requests/cert/dns_show_and_destroy.go delete mode 100644 app/http/requests/cert/dns_store.go delete mode 100644 app/http/requests/cert/dns_update.go delete mode 100644 app/http/requests/cert/obtain.go delete mode 100644 app/http/requests/cert/renew.go delete mode 100644 app/http/requests/cert/user_show_and_destroy.go delete mode 100644 app/http/requests/cert/user_store.go delete mode 100644 app/http/requests/cert/user_update.go delete mode 100644 app/http/requests/common/paginate.go delete mode 100644 app/http/requests/container/container_create.go delete mode 100644 app/http/requests/container/container_rename.go delete mode 100644 app/http/requests/container/container_update.go delete mode 100644 app/http/requests/container/id.go delete mode 100644 app/http/requests/container/image_pull.go delete mode 100644 app/http/requests/container/network_connect_disconnect.go delete mode 100644 app/http/requests/container/network_create.go delete mode 100644 app/http/requests/container/volume_create.go delete mode 100644 app/http/requests/file/archive.go delete mode 100644 app/http/requests/file/copy.go delete mode 100644 app/http/requests/file/exist.go delete mode 100644 app/http/requests/file/move.go delete mode 100644 app/http/requests/file/not_exist.go delete mode 100644 app/http/requests/file/permission.go delete mode 100644 app/http/requests/file/save.go delete mode 100644 app/http/requests/file/search.go delete mode 100644 app/http/requests/file/un_archive.go delete mode 100644 app/http/requests/file/upload.go delete mode 100644 app/http/requests/plugins/frp/service.go delete mode 100644 app/http/requests/plugins/frp/update_config.go delete mode 100644 app/http/requests/plugins/gitea/update_config.go delete mode 100644 app/http/requests/plugins/podman/update_registry_config.go delete mode 100644 app/http/requests/plugins/podman/update_storage_config.go delete mode 100644 app/http/requests/plugins/rsync/create.go delete mode 100644 app/http/requests/plugins/rsync/update.go delete mode 100644 app/http/requests/plugins/rsync/update_config.go delete mode 100644 app/http/requests/setting/https.go delete mode 100644 app/http/requests/setting/update.go delete mode 100644 app/http/requests/user/login.go delete mode 100644 app/http/requests/website/add.go delete mode 100644 app/http/requests/website/delete.go delete mode 100644 app/http/requests/website/delete_backup.go delete mode 100644 app/http/requests/website/id.go delete mode 100644 app/http/requests/website/restore_backup.go delete mode 100644 app/http/requests/website/save_config.go delete mode 100644 app/jobs/process_task.go delete mode 100644 app/models/cert.go delete mode 100644 app/models/cert_dns.go delete mode 100644 app/models/cert_user.go delete mode 100644 app/models/cron.go delete mode 100644 app/models/database.go delete mode 100644 app/models/monitor.go delete mode 100644 app/models/plugin.go delete mode 100644 app/models/setting.go delete mode 100644 app/models/task.go delete mode 100644 app/models/user.go delete mode 100644 app/models/website.go delete mode 100644 app/plugins/README.md delete mode 100644 app/plugins/fail2ban/controller.go delete mode 100644 app/plugins/fail2ban/main.go delete mode 100644 app/plugins/frp_controller.go delete mode 100644 app/plugins/gitea_controller.go delete mode 100644 app/plugins/loader/loader.go delete mode 100644 app/plugins/main.go delete mode 100644 app/plugins/mysql_controller.go delete mode 100644 app/plugins/openresty/controller.go delete mode 100644 app/plugins/openresty/main.go delete mode 100644 app/plugins/php_controller.go delete mode 100644 app/plugins/phpmyadmin_controller.go delete mode 100644 app/plugins/podman_controller.go delete mode 100644 app/plugins/postgresql_controller.go delete mode 100644 app/plugins/pureftpd_controller.go delete mode 100644 app/plugins/redis_controller.go delete mode 100644 app/plugins/rsync_controller.go delete mode 100644 app/plugins/s3fs_controller.go delete mode 100644 app/plugins/supervisor_controller.go delete mode 100644 app/plugins/toolbox_controller.go delete mode 100644 app/providers/app_service_provider.go delete mode 100644 app/providers/auth_service_provider.go delete mode 100644 app/providers/console_service_provider.go delete mode 100644 app/providers/database_service_provider.go delete mode 100644 app/providers/event_service_provider.go delete mode 100644 app/providers/plugin_service_provider.go delete mode 100644 app/providers/queue_service_provider.go delete mode 100644 app/providers/route_service_provider.go delete mode 100644 app/providers/validation_service_provider.go delete mode 100644 app/rules/exists.go delete mode 100644 app/rules/not_exists.go delete mode 100644 app/rules/path_exist.go delete mode 100644 app/rules/path_not_exist.go delete mode 100644 bootstrap/app.go create mode 100644 cmd/README.md rename scripts/panel.sh => cmd/app/main.go (63%) rename scripts/frp/uninstall.sh => cmd/cli/main.go (53%) create mode 100644 config/README.md delete mode 100644 config/app.go delete mode 100644 config/auth.go delete mode 100644 config/cache.go create mode 100644 config/config.example.yml delete mode 100644 config/cors.go delete mode 100644 config/database.go delete mode 100644 config/filesystems.go delete mode 100644 config/hashing.go delete mode 100644 config/http.go delete mode 100644 config/jwt.go delete mode 100644 config/logging.go delete mode 100644 config/mail.go delete mode 100644 config/panel.go delete mode 100644 config/queue.go delete mode 100644 config/session.go create mode 100644 docs/README.md delete mode 100644 embed/frontend/.gitignore create mode 100644 internal/app/global.go delete mode 100644 internal/backup.go create mode 100644 internal/biz/cert.go create mode 100644 internal/biz/cert_account.go create mode 100644 internal/biz/cert_dns.go create mode 100644 internal/biz/container.go create mode 100644 internal/biz/container_image.go create mode 100644 internal/biz/container_network.go create mode 100644 internal/biz/container_volume.go create mode 100644 internal/biz/cron.go create mode 100644 internal/biz/database.go create mode 100644 internal/biz/firewall.go create mode 100644 internal/biz/monitor.go create mode 100644 internal/biz/plugin.go create mode 100644 internal/biz/safe.go create mode 100644 internal/biz/setting.go create mode 100644 internal/biz/ssh.go create mode 100644 internal/biz/task.go create mode 100644 internal/biz/user.go create mode 100644 internal/biz/website.go create mode 100644 internal/bootstrap/app.go create mode 100644 internal/bootstrap/conf.go create mode 100644 internal/bootstrap/db.go create mode 100644 internal/bootstrap/http.go create mode 100644 internal/bootstrap/queue.go create mode 100644 internal/bootstrap/session.go create mode 100644 internal/bootstrap/validator.go delete mode 100644 internal/cert.go delete mode 100644 internal/container.go delete mode 100644 internal/cron.go create mode 100644 internal/data/cert.go create mode 100644 internal/data/cert_account.go create mode 100644 internal/data/cert_dns.go create mode 100644 internal/data/container.go create mode 100644 internal/data/container_image.go create mode 100644 internal/data/container_network.go create mode 100644 internal/data/container_volume.go create mode 100644 internal/data/cron.go create mode 100644 internal/data/monitor.go create mode 100644 internal/data/plugin.go create mode 100644 internal/data/safe.go create mode 100644 internal/data/setting.go create mode 100644 internal/data/ssh.go create mode 100644 internal/data/task.go create mode 100644 internal/data/user.go create mode 100644 internal/data/website.go rename {embed => internal/embed}/embed.go (100%) rename {storage/logs => internal/embed/frontend}/.gitignore (100%) rename {embed => internal/embed}/website/404.html (100%) rename {embed => internal/embed}/website/index.html (100%) create mode 100644 internal/http/middleware/middleware.go create mode 100644 internal/http/middleware/must_login.go create mode 100644 internal/http/middleware/throttle.go create mode 100644 internal/http/request/cert.go create mode 100644 internal/http/request/cert_account.go create mode 100644 internal/http/request/cert_dns.go create mode 100644 internal/http/request/common.go create mode 100644 internal/http/request/container.go create mode 100644 internal/http/request/container_image.go create mode 100644 internal/http/request/container_network.go create mode 100644 internal/http/request/container_volume.go create mode 100644 internal/http/request/cron.go create mode 100644 internal/http/request/file.go create mode 100644 internal/http/request/firewall.go create mode 100644 internal/http/request/monitor.go create mode 100644 internal/http/request/paginate.go create mode 100644 internal/http/request/plugin.go create mode 100644 internal/http/request/request.go create mode 100644 internal/http/request/safe.go create mode 100644 internal/http/request/setting.go create mode 100644 internal/http/request/ssh.go create mode 100644 internal/http/request/systemctl.go create mode 100644 internal/http/request/user.go create mode 100644 internal/http/request/website.go create mode 100644 internal/job/process_task.go create mode 100644 internal/migration/migration.go create mode 100644 internal/migration/v1.go delete mode 100644 internal/php.go delete mode 100644 internal/plugin.go create mode 100644 internal/plugin/init.go create mode 100644 internal/plugin/openresty/init.go create mode 100644 internal/plugin/openresty/service.go create mode 100644 internal/route/http.go create mode 100644 internal/service/backup.go create mode 100644 internal/service/base.go create mode 100644 internal/service/cert.go create mode 100644 internal/service/cert_account.go create mode 100644 internal/service/cert_dns.go create mode 100644 internal/service/container.go create mode 100644 internal/service/container_image.go create mode 100644 internal/service/container_network.go create mode 100644 internal/service/container_volume.go create mode 100644 internal/service/cron.go create mode 100644 internal/service/file.go create mode 100644 internal/service/file_windows.go create mode 100644 internal/service/firewall.go create mode 100644 internal/service/info.go rename app/http/controllers/monitor_controller.go => internal/service/monitor.go (50%) create mode 100644 internal/service/plugin.go create mode 100644 internal/service/safe.go create mode 100644 internal/service/setting.go create mode 100644 internal/service/ssh.go create mode 100644 internal/service/systemctl.go create mode 100644 internal/service/task.go create mode 100644 internal/service/user.go create mode 100644 internal/service/website.go delete mode 100644 internal/services/backup.go delete mode 100644 internal/services/cert.go delete mode 100644 internal/services/container.go delete mode 100644 internal/services/cron.go delete mode 100644 internal/services/php.go delete mode 100644 internal/services/plugin.go delete mode 100644 internal/services/setting.go delete mode 100644 internal/services/task.go delete mode 100644 internal/services/user.go delete mode 100644 internal/services/website.go delete mode 100644 internal/setting.go delete mode 100644 internal/task.go delete mode 100644 internal/user.go delete mode 100644 internal/website.go delete mode 100644 lang/en.json delete mode 100644 lang/zh_CN.json delete mode 100644 main.go delete mode 100644 panel-example.conf create mode 100644 pkg/arch/arch.go create mode 100644 pkg/firewall/consts.go create mode 100644 pkg/firewall/firewall.go delete mode 100644 pkg/h/request.go delete mode 100644 pkg/h/response.go create mode 100644 pkg/io/io_test.go delete mode 100644 pkg/migrate/migrate.go delete mode 100644 pkg/migrate/migrations.go create mode 100644 pkg/ntp/ntp.go create mode 100644 pkg/ntp/ntp_test.go create mode 100644 pkg/pluginloader/plugin.go create mode 100644 pkg/queue/job.go create mode 100644 pkg/queue/queue.go create mode 100644 pkg/queue/queue_test.go delete mode 100644 routes/api.go delete mode 100644 routes/plugin.go delete mode 100644 scripts/calculate_j.sh delete mode 100644 scripts/fail2ban/install.sh delete mode 100644 scripts/fail2ban/uninstall.sh delete mode 100644 scripts/fail2ban/update.sh delete mode 100644 scripts/frp/install.sh delete mode 100644 scripts/frp/update.sh delete mode 100644 scripts/gitea/install.sh delete mode 100644 scripts/gitea/uninstall.sh delete mode 100644 scripts/gitea/update.sh delete mode 100644 scripts/install_panel.sh delete mode 100644 scripts/mysql/install.sh delete mode 100644 scripts/mysql/uninstall.sh delete mode 100644 scripts/mysql/update.sh delete mode 100644 scripts/openresty/install.sh delete mode 100644 scripts/openresty/uninstall.sh delete mode 100644 scripts/panel.service delete mode 100644 scripts/php/install.sh delete mode 100644 scripts/php/uninstall.sh delete mode 100644 scripts/php_extensions/Swow.sh delete mode 100644 scripts/php_extensions/Zend OPcache.sh delete mode 100644 scripts/php_extensions/igbinary.sh delete mode 100644 scripts/php_extensions/imagick.sh delete mode 100644 scripts/php_extensions/ionCube Loader.sh delete mode 100644 scripts/php_extensions/official.sh delete mode 100644 scripts/php_extensions/redis.sh delete mode 100644 scripts/php_extensions/swoole.sh delete mode 100644 scripts/phpmyadmin/install.sh delete mode 100644 scripts/phpmyadmin/uninstall.sh delete mode 100644 scripts/podman/install.sh delete mode 100644 scripts/podman/uninstall.sh delete mode 100644 scripts/podman/update.sh delete mode 100644 scripts/postgresql/install.sh delete mode 100644 scripts/postgresql/uninstall.sh delete mode 100644 scripts/postgresql/update.sh delete mode 100644 scripts/pureftpd/install.sh delete mode 100644 scripts/pureftpd/uninstall.sh delete mode 100644 scripts/pureftpd/update.sh delete mode 100644 scripts/redis/install.sh delete mode 100644 scripts/redis/uninstall.sh delete mode 100644 scripts/redis/update.sh delete mode 100644 scripts/rsync/install.sh delete mode 100644 scripts/rsync/uninstall.sh delete mode 100644 scripts/rsync/update.sh delete mode 100644 scripts/s3fs/install.sh delete mode 100644 scripts/s3fs/uninstall.sh delete mode 100644 scripts/s3fs/update.sh delete mode 100644 scripts/supervisor/install.sh delete mode 100644 scripts/supervisor/uninstall.sh delete mode 100644 scripts/supervisor/update.sh delete mode 100644 scripts/uninstall_panel.sh delete mode 100644 scripts/update_panel.sh delete mode 100644 storage/.gitignore create mode 100644 storage/README.md rename app/http/middleware/.gitignore => storage/logs/.gitkeep (100%) create mode 100644 storage/sessions/.gitkeep delete mode 100644 storage/temp/.gitignore delete mode 100644 tests/setting/setting_test.go delete mode 100644 tests/test_case.go delete mode 100644 tests/user/user_test.go create mode 100644 web/.env.development create mode 100644 web/.env.production create mode 100644 web/.eslintrc-auto-import.json create mode 100644 web/.eslintrc.cjs create mode 100644 web/.gitignore create mode 100644 web/.prettierrc.json create mode 100644 web/README.md create mode 100644 web/build/config/define.ts create mode 100644 web/build/config/index.ts create mode 100644 web/build/config/proxy.ts create mode 100644 web/build/plugins/html.ts create mode 100644 web/build/plugins/index.ts create mode 100644 web/build/plugins/unplugin.ts create mode 100644 web/build/utils.ts create mode 100644 web/env.d.ts create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/public/favicon.png create mode 100644 web/public/loading/index.css create mode 100644 web/public/loading/index.js create mode 100644 web/public/loading/logo.png create mode 100644 web/public/robots.txt create mode 100644 web/settings/.gitignore create mode 100644 web/settings/proxy-config.ts.example create mode 100644 web/settings/theme.json create mode 100644 web/src/App.vue create mode 100644 web/src/api/panel/cert/index.ts create mode 100644 web/src/api/panel/container/index.ts create mode 100644 web/src/api/panel/cron/index.ts create mode 100644 web/src/api/panel/file/index.ts create mode 100644 web/src/api/panel/info/index.ts create mode 100644 web/src/api/panel/monitor/index.ts create mode 100644 web/src/api/panel/plugin/index.ts create mode 100644 web/src/api/panel/safe/index.ts create mode 100644 web/src/api/panel/setting/index.ts create mode 100644 web/src/api/panel/ssh/index.ts create mode 100644 web/src/api/panel/system/service/index.ts create mode 100644 web/src/api/panel/task/index.ts create mode 100644 web/src/api/panel/user/index.ts create mode 100644 web/src/api/panel/website/index.ts create mode 100644 web/src/api/plugins/fail2ban/index.ts create mode 100644 web/src/api/plugins/frp/index.ts create mode 100644 web/src/api/plugins/gitea/index.ts create mode 100644 web/src/api/plugins/mysql/index.ts create mode 100644 web/src/api/plugins/openresty/index.ts create mode 100644 web/src/api/plugins/php/index.ts create mode 100644 web/src/api/plugins/phpmyadmin/index.ts create mode 100644 web/src/api/plugins/podman/index.ts create mode 100644 web/src/api/plugins/postgresql/index.ts create mode 100644 web/src/api/plugins/pureftpd/index.ts create mode 100644 web/src/api/plugins/redis/index.ts create mode 100644 web/src/api/plugins/rsync/index.ts create mode 100644 web/src/api/plugins/s3fs/index.ts create mode 100644 web/src/api/plugins/supervisor/index.ts create mode 100644 web/src/api/plugins/toolbox/index.ts create mode 100644 web/src/assets/images/404.webp create mode 100644 web/src/assets/images/login_banner.png create mode 100644 web/src/assets/images/login_bg.webp create mode 100644 web/src/assets/images/logo.png create mode 100644 web/src/components/common/AppFooter.vue create mode 100644 web/src/components/common/AppProvider.vue create mode 100644 web/src/components/common/CodeEditor.vue create mode 100644 web/src/components/common/CronSelect.vue create mode 100644 web/src/components/custom/TheIcon.vue create mode 100644 web/src/components/page/AppPage.vue create mode 100644 web/src/components/page/CommonPage.vue create mode 100644 web/src/i18n/en.json create mode 100644 web/src/i18n/i18n.ts create mode 100644 web/src/i18n/zh_CN.json create mode 100644 web/src/layout/AppMain.vue create mode 100644 web/src/layout/IndexView.vue create mode 100644 web/src/layout/header/IndexView.vue create mode 100644 web/src/layout/header/components/BreadCrumb.vue create mode 100644 web/src/layout/header/components/FullScreen.vue create mode 100644 web/src/layout/header/components/MenuCollapse.vue create mode 100644 web/src/layout/header/components/ReloadPage.vue create mode 100644 web/src/layout/header/components/ThemeMode.vue create mode 100644 web/src/layout/header/components/UserAvatar.vue create mode 100644 web/src/layout/sidebar/IndexView.vue create mode 100644 web/src/layout/sidebar/components/SideLogo.vue create mode 100644 web/src/layout/sidebar/components/SideMenu.vue create mode 100644 web/src/layout/tab/IndexView.vue create mode 100644 web/src/layout/tab/components/ContextMenu.vue create mode 100644 web/src/main.ts create mode 100644 web/src/router/guard/index.ts create mode 100644 web/src/router/guard/page-loading-guard.ts create mode 100644 web/src/router/guard/page-title-guard.ts create mode 100644 web/src/router/guard/plugin-install-guard.ts create mode 100644 web/src/router/index.ts create mode 100644 web/src/router/routes/index.ts create mode 100644 web/src/store/index.ts create mode 100644 web/src/store/modules/app/index.ts create mode 100644 web/src/store/modules/index.ts create mode 100644 web/src/store/modules/permission/helpers.ts create mode 100644 web/src/store/modules/permission/index.ts create mode 100644 web/src/store/modules/tab/helpers.ts create mode 100644 web/src/store/modules/tab/index.ts create mode 100644 web/src/store/modules/theme/helpers.ts create mode 100644 web/src/store/modules/theme/index.ts create mode 100644 web/src/store/modules/user/index.ts create mode 100644 web/src/styles/index.scss create mode 100644 web/src/styles/reset.css create mode 100644 web/src/utils/auth/index.ts create mode 100644 web/src/utils/auth/router.ts create mode 100644 web/src/utils/common/color.ts create mode 100644 web/src/utils/common/common.ts create mode 100644 web/src/utils/common/crypto.ts create mode 100644 web/src/utils/common/icon.ts create mode 100644 web/src/utils/common/index.ts create mode 100644 web/src/utils/common/is.ts create mode 100644 web/src/utils/common/naiveTools.ts create mode 100644 web/src/utils/event/index.ts create mode 100644 web/src/utils/file/index.ts create mode 100644 web/src/utils/http/helpers.ts create mode 100644 web/src/utils/http/index.ts create mode 100644 web/src/utils/http/interceptors.ts create mode 100644 web/src/utils/index.ts create mode 100644 web/src/utils/storage/index.ts create mode 100644 web/src/utils/storage/local.ts create mode 100644 web/src/utils/storage/session.ts create mode 100644 web/src/views/cert/CertView.vue create mode 100644 web/src/views/cert/DNSView.vue create mode 100644 web/src/views/cert/IndexView.vue create mode 100644 web/src/views/cert/UserView.vue create mode 100644 web/src/views/cert/route.ts create mode 100644 web/src/views/cert/types.ts create mode 100644 web/src/views/container/ContainerCreate.vue create mode 100644 web/src/views/container/ContainerView.vue create mode 100644 web/src/views/container/ImageView.vue create mode 100644 web/src/views/container/IndexView.vue create mode 100644 web/src/views/container/NetworkView.vue create mode 100644 web/src/views/container/VolumeView.vue create mode 100644 web/src/views/container/route.ts create mode 100644 web/src/views/container/types.ts create mode 100644 web/src/views/cron/IndexView.vue create mode 100644 web/src/views/cron/route.ts create mode 100644 web/src/views/cron/types.ts create mode 100644 web/src/views/error-page/NotFound.vue create mode 100644 web/src/views/file/ArchiveModal.vue create mode 100644 web/src/views/file/EditModal.vue create mode 100644 web/src/views/file/IndexView.vue create mode 100644 web/src/views/file/ListTable.vue create mode 100644 web/src/views/file/PathInput.vue create mode 100644 web/src/views/file/PermissionModal.vue create mode 100644 web/src/views/file/ToolBar.vue create mode 100644 web/src/views/file/UploadModal.vue create mode 100644 web/src/views/file/route.ts create mode 100644 web/src/views/file/types.ts create mode 100644 web/src/views/home/IndexView.vue create mode 100644 web/src/views/home/UpdateView.vue create mode 100644 web/src/views/home/route.ts create mode 100644 web/src/views/home/types.ts create mode 100644 web/src/views/login/IndexView.vue create mode 100644 web/src/views/monitor/IndexView.vue create mode 100644 web/src/views/monitor/route.ts create mode 100644 web/src/views/monitor/types.ts create mode 100644 web/src/views/plugin/IndexView.vue create mode 100644 web/src/views/plugin/route.ts create mode 100644 web/src/views/plugin/types.ts create mode 100644 web/src/views/plugins/fail2ban/IndexView.vue create mode 100644 web/src/views/plugins/fail2ban/route.ts create mode 100644 web/src/views/plugins/fail2ban/types.ts create mode 100644 web/src/views/plugins/frp/IndexView.vue create mode 100644 web/src/views/plugins/frp/route.ts create mode 100644 web/src/views/plugins/gitea/IndexView.vue create mode 100644 web/src/views/plugins/gitea/route.ts create mode 100644 web/src/views/plugins/mysql/IndexView.vue create mode 100644 web/src/views/plugins/mysql/types.ts create mode 100644 web/src/views/plugins/mysql57/route.ts create mode 100644 web/src/views/plugins/mysql80/route.ts create mode 100644 web/src/views/plugins/mysql84/route.ts create mode 100644 web/src/views/plugins/openresty/IndexView.vue create mode 100644 web/src/views/plugins/openresty/route.ts create mode 100644 web/src/views/plugins/openresty/types.ts create mode 100644 web/src/views/plugins/php/IndexView.vue create mode 100644 web/src/views/plugins/php74/route.ts create mode 100644 web/src/views/plugins/php80/route.ts create mode 100644 web/src/views/plugins/php81/route.ts create mode 100644 web/src/views/plugins/php82/route.ts create mode 100644 web/src/views/plugins/php83/route.ts create mode 100644 web/src/views/plugins/phpmyadmin/IndexView.vue create mode 100644 web/src/views/plugins/phpmyadmin/route.ts create mode 100644 web/src/views/plugins/podman/IndexView.vue create mode 100644 web/src/views/plugins/podman/route.ts create mode 100644 web/src/views/plugins/postgresql/IndexView.vue create mode 100644 web/src/views/plugins/postgresql/types.ts create mode 100644 web/src/views/plugins/postgresql15/route.ts create mode 100644 web/src/views/plugins/postgresql16/route.ts create mode 100644 web/src/views/plugins/pureftpd/IndexView.vue create mode 100644 web/src/views/plugins/pureftpd/route.ts create mode 100644 web/src/views/plugins/pureftpd/types.ts create mode 100644 web/src/views/plugins/redis/IndexView.vue create mode 100644 web/src/views/plugins/redis/route.ts create mode 100644 web/src/views/plugins/rsync/IndexView.vue create mode 100644 web/src/views/plugins/rsync/route.ts create mode 100644 web/src/views/plugins/rsync/types.ts create mode 100644 web/src/views/plugins/s3fs/IndexView.vue create mode 100644 web/src/views/plugins/s3fs/route.ts create mode 100644 web/src/views/plugins/s3fs/types.ts create mode 100644 web/src/views/plugins/supervisor/IndexView.vue create mode 100644 web/src/views/plugins/supervisor/route.ts create mode 100644 web/src/views/plugins/supervisor/types.ts create mode 100644 web/src/views/plugins/toolbox/IndexView.vue create mode 100644 web/src/views/plugins/toolbox/route.ts create mode 100644 web/src/views/safe/IndexView.vue create mode 100644 web/src/views/safe/route.ts create mode 100644 web/src/views/safe/types.ts create mode 100644 web/src/views/setting/IndexView.vue create mode 100644 web/src/views/setting/SettingBase.vue create mode 100644 web/src/views/setting/SettingHttps.vue create mode 100644 web/src/views/setting/route.ts create mode 100644 web/src/views/ssh/IndexView.vue create mode 100644 web/src/views/ssh/route.ts create mode 100644 web/src/views/task/IndexView.vue create mode 100644 web/src/views/task/route.ts create mode 100644 web/src/views/task/types.ts create mode 100644 web/src/views/website/EditView.vue create mode 100644 web/src/views/website/IndexView.vue create mode 100644 web/src/views/website/route.ts create mode 100644 web/src/views/website/types.ts create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/types/axios.d.ts create mode 100644 web/types/env.d.ts create mode 100644 web/types/global.d.ts create mode 100644 web/types/router.d.ts create mode 100644 web/types/shims.d.ts create mode 100644 web/types/theme.d.ts create mode 100644 web/uno.config.ts create mode 100644 web/vite.config.ts diff --git a/.air.toml b/.air.toml new file mode 100644 index 00000000..1352f5ce --- /dev/null +++ b/.air.toml @@ -0,0 +1,69 @@ +# Config file for [Air](https://github.com/air-verse/air) in TOML format + +# Working directory +# . or absolute path, please note that the directories following must be under root. +root = "." +tmp_dir = "storage/temp" + +[build] +# Array of commands to run before each build +pre_cmd = [] +# Just plain old shell command. You could use `make` as well. +cmd = "go build -o storage/temp/main.exe ./cmd/app" +# Array of commands to run after ^C +post_cmd = [] +# Binary file yields from `cmd`. +bin = "storage/temp/main.exe" +# Customize binary, can setup environment variables when run your app. +full_bin = "" +# Watch these filename extensions. +include_ext = ["go", "tpl", "tmpl", "html"] +# Ignore these filename extensions or directories. +exclude_dir = ["storage", "web"] +# Watch these directories if you specified. +include_dir = [] +# Watch these files. +include_file = [] +# Exclude files. +exclude_file = [] +# Exclude specific regular expressions. +exclude_regex = ["_test\\.go"] +# Exclude unchanged files. +exclude_unchanged = true +# Follow symlink for directories +follow_symlink = true +# This log file places in your tmp_dir. +log = "build-errors.log" +# It's not necessary to trigger build each time file changes if it's too frequent. +delay = 2000 +# Stop running old binary when build errors occur. +stop_on_error = true +# Send Interrupt signal before killing process (windows does not support this feature) +send_interrupt = false +# Delay after sending Interrupt signal +kill_delay = 500 # nanosecond +# Rerun binary or not +rerun = false +# Delay after each execution +rerun_delay = 500 + +[log] +# Show log time +time = false +# Only show main log (silences watcher, build, runner) +main_only = false + +[color] +# Customize each part's color. If no color found, use the raw app log. +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +# Delete tmp directory on exit +clean_on_exit = true + +[screen] +clear_on_rebuild = true +keep_scroll = true \ No newline at end of file diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..ae0fc941 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,55 @@ +name: Backend +on: + push: + branches: + - main + pull_request: +jobs: + build: + runs-on: ubuntu-24.04 + strategy: + matrix: + goarch: [ amd64, arm64 ] + fail-fast: true + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install dependencies + run: go mod tidy + - name: Wait for frontend build + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + check-name: 'build (frontend)' + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Download frontend + uses: dawidd6/action-download-artifact@v6 + with: + workflow: build-frontend.yml + name: frontend + path: internal/embed/frontend + check_artifacts: true + - name: Build ${{ matrix.goarch }} + env: + CGO_ENABLED: 0 + GOOS: linux + GOARCH: ${{ matrix.goarch }} + run: | + go build -ldflags '-s -w --extldflags "-static"' -o panel-${{ matrix.goarch }} ./cmd/app + go build -ldflags '-s -w --extldflags "-static"' -o cli-${{ matrix.goarch }} ./cmd/cli + - name: Compress ${{ matrix.goarch }} + run: | + upx --best --lzma panel-${{ matrix.goarch }} + upx --best --lzma cli-${{ matrix.goarch }} + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: panel-${{ matrix.goarch }} + path: | + panel-${{ matrix.goarch }} + cli-${{ matrix.goarch }} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index dfb345b8..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Build -on: - push: - branches: - - main - pull_request: -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - goarch: [ amd64, arm64 ] - fail-fast: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - cache: true - go-version: '1.22' - - name: Install dependencies - run: go mod tidy - - name: Build ${{ matrix.goarch }} - env: - CGO_ENABLED: 0 - GOOS: linux - GOARCH: ${{ matrix.goarch }} - run: go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel-${{ matrix.goarch }} - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: panel-${{ matrix.goarch }} - path: panel-${{ matrix.goarch }} diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index a4d8d00b..693bed8f 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -6,12 +6,15 @@ on: pull_request: jobs: codecov: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: '1.22' + cache: true + go-version: 'stable' - name: Install dependencies run: go mod tidy - name: Run tests with coverage @@ -20,4 +23,4 @@ jobs: uses: codecov/codecov-action@v4 with: file: ./coverage.out - token: ${{ secrets.CODECOV }} + token: ${{ secrets.CODECOV }} \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..65ddb6fd --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,42 @@ +name: Frontend +on: + push: + branches: + - main + pull_request: +jobs: + build: + name: build (frontend) + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + package_json_file: web/package.json + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: web/pnpm-lock.yaml + - name: Build frontend + # We need to run the dev server first to generate the auto-imports files + run: | + cp .env.production .env + cp settings/proxy-config.ts.example settings/proxy-config.ts + pnpm dev & + sleep 5 + kill %1 + pnpm build + - name: Upload frontend + uses: actions/upload-artifact@v4 + with: + name: frontend + path: web/dist/ # https://github.com/actions/upload-artifact/issues/541 \ No newline at end of file diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 48437327..198e876c 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -7,25 +7,34 @@ permissions: contents: write jobs: goreleaser: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Go + - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.22' - - name: Fetch Frontend - run: | - curl -sSL https://api.github.com/repos/TheTNB/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip - unzip frontend.zip - mv dist/* embed/frontend/ + cache: true + go-version: 'stable' + - name: Wait for frontend build + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + check-name: 'build (frontend)' + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Download frontend + uses: dawidd6/action-download-artifact@v6 + with: + workflow: build-frontend.yml + name: frontend + path: internal/embed/frontend + check_artifacts: true - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/issue-auto-reply.yml b/.github/workflows/issue-auto-reply.yml index 0ef9c37a..01b9346c 100644 --- a/.github/workflows/issue-auto-reply.yml +++ b/.github/workflows/issue-auto-reply.yml @@ -1,18 +1,15 @@ name: Issue Auto Reply - on: issues: types: [ labeled ] - permissions: contents: read - jobs: issue-reply: permissions: issues: write pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ✏️ Feature if: github.event.label.name == '✏️ Feature' @@ -27,7 +24,7 @@ jobs: 我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! - ![aoligei](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) + ![干](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) - name: ☢️ Bug if: github.event.label.name == '☢️ Bug' uses: actions-cool/issues-helper@v3 @@ -41,4 +38,4 @@ jobs: 我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! - ![aoligei](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) + ![干](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 30323898..1d04df72 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,18 +7,73 @@ on: permissions: contents: read jobs: - lint: - name: lint - runs-on: ubuntu-latest + golangci: + name: golanci-lint + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: '1.22' - cache: false - - name: Lint + cache: true + go-version: 'stable' + - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: skip-cache: true version: latest args: --timeout=30m ./... + nilaway: + runs-on: ubuntu-24.04 + if: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install dependencies + run: go mod tidy + - name: Install NilAway + run: go install go.uber.org/nilaway/cmd/nilaway@latest + - name: Run NilAway + run: nilaway -include-pkgs="github.com/TheTNB/panel" ./... + govulncheck: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install Govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + - name: Run Govulncheck + run: govulncheck ./... + frontend: + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + package_json_file: web/package.json + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: web/pnpm-lock.yaml + - name: Run pnpm lint + run: pnpm lint \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 744b8ad4..84abeb42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,19 +5,20 @@ on: - main pull_request: jobs: - test: - runs-on: ubuntu-latest + unit: + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: '1.22' + cache: true + go-version: 'stable' - name: Install dependencies run: sudo apt-get install -y curl jq - name: Set up environment run: | - cp panel-example.conf .env - echo "DB_FILE=$(pwd)/storage/panel.db" >> .env - go run . artisan key:generate + cp config/config.example.yml config/config.yml - name: Run tests - run: go test ./... + run: go test ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 32b14049..243b118a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,6 @@ -tmp -/.air.toml -/panel.conf - -# Golang # # `go test -c` 生成的二进制文件 *.test + # go coverage 工具 *.out *.prof @@ -14,16 +10,16 @@ _cgo_defun.c _cgo_gotypes.go _cgo_export.* -# 编译文件 # +# 编译文件 *.com *.class *.dll *.exe *.o *.so -/panel +# 在此添加你的项目名(如果需要) -# 压缩包 # +# 压缩包 # Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit *.7z *.dmg @@ -34,16 +30,17 @@ _cgo_export.* *.tar *.zip -# 日志文件和数据库 # +# 日志文件和数据库及配置 *.log *.sqlite *.db +config/config.yml -# 临时文件 # +# 临时文件 tmp/ .tmp/ -# 系统生成文件 # +# 系统生成文件 .DS_Store .DS_Store? .AppleDouble @@ -58,7 +55,7 @@ Thumbs.db .VolumeIcon.icns .com.apple.timemachine.donotpresent -# IDE 和编辑器 # +# IDE 和编辑器 .idea/ /go_build_* out/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6e3a813e..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,90 +0,0 @@ -image: golang:1.23-bookworm - -# 在每个任务执行前运行 -before_script: - - mkdir -p .go - - go version - - go env -w GO111MODULE=on - - go env -w GOPROXY=https://goproxy.cn,direct - -.go_cache: - variables: - GOPATH: $CI_PROJECT_DIR/.go - cache: - paths: - - .go/pkg/mod/ - -stages: - - prepare - - build - - release - -golangci_lint: - stage: prepare - image: golangci/golangci-lint:latest-alpine - extends: .go_cache - allow_failure: true - script: - - golangci-lint run --timeout 30m - -unit_test: - stage: prepare - extends: .go_cache - allow_failure: true - script: - - rm -rf /etc/apt/sources.list - - rm -rf /etc/apt/sources.list.d/* - - wget -O /etc/apt/sources.list https://mirrors.ustc.edu.cn/repogen/conf/debian-http-4-bookworm - - apt-get update - - apt-get install -y curl jq - - cp panel-example.conf .env - - echo "DB_FILE=$(pwd)/storage/panel.db" >> .env - - go run . artisan key:generate - - go test -v -coverprofile=coverage.txt -covermode=atomic ./... - -build: - stage: build - extends: .go_cache - script: - - go mod download - - CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel - artifacts: - name: "panel" - paths: - - panel - expire_in: 3 days - -fetch: - stage: build - image: alpine:latest - before_script: - - sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories - - apk add --no-cache curl jq unzip zip - script: - - curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip - - unzip frontend.zip - - mv dist/* embed/frontend/ - artifacts: - name: "frontend" - paths: - - embed/frontend - expire_in: 3 days - -release: - stage: release - dependencies: - - build - - fetch - image: - name: goreleaser/goreleaser - entrypoint: [ '' ] - only: - - tags - variables: - # Disable shallow cloning so that goreleaser can diff between tags to - # generate a changelog. - GIT_DEPTH: 0 - script: - - sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories - - apk add --no-cache upx - - goreleaser release --clean diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 047655f1..79c157cf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,21 +2,29 @@ project_name: panel builds: - id: panel + main: ./cmd/app binary: panel env: - CGO_ENABLED=0 - - GOPROXY=https://goproxy.cn,direct goos: - linux goarch: - amd64 - arm64 - goamd64: - - v2 ldflags: - -s -w --extldflags "-static" - tags: - - nomsgpack + - id: cli + main: ./cmd/cli + binary: cli + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + ldflags: + - -s -w --extldflags "-static" upx: - enabled: true diff --git a/README.md b/README.md index 127ed8c5..4a3bc88d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ## 项目现状 -**目前我在着手使用新的「自研」框架重构本项目,由于更改非常大需要一定时间,预期 9 月初会带来新的更新。** +**目前我在着手使用新的「自研」框架重构本项目,由于更改非常大需要一定时间,预期 9 月底会带来新的更新。** ## 优势 diff --git a/app/console/commands/cert_renew.go b/app/console/commands/cert_renew.go deleted file mode 100644 index a8fce5ee..00000000 --- a/app/console/commands/cert_renew.go +++ /dev/null @@ -1,78 +0,0 @@ -package commands - -import ( - "context" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal/services" - panelcert "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// CertRenew 证书续签 -type CertRenew struct { -} - -// Signature The name and signature of the console command. -func (receiver *CertRenew) Signature() string { - return "panel:cert-renew" -} - -// Description The console command description. -func (receiver *CertRenew) Description() string { - ctx := context.Background() - return facades.Lang(ctx).Get("commands.panel:cert-renew.description") -} - -// Extend The console command extend. -func (receiver *CertRenew) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *CertRenew) Handle(console.Context) error { - if types.Status != types.StatusNormal { - return nil - } - - var certs []models.Cert - err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs) - if err != nil { - return err - } - - for _, cert := range certs { - if !cert.AutoRenew { - continue - } - - decode, err := panelcert.ParseCert(cert.Cert) - if err != nil { - continue - } - - // 结束时间大于 7 天的证书不续签 - endTime := carbon.FromStdTime(decode.NotAfter) - if endTime.Gt(carbon.Now().AddDays(7)) { - continue - } - - certService := services.NewCertImpl() - _, err = certService.Renew(cert.ID) - if err != nil { - facades.Log().Tags("面板", "证书管理").With(map[string]any{ - "cert_id": cert.ID, - "error": err.Error(), - }).Infof("证书续签失败") - } - } - - return nil -} diff --git a/app/console/commands/monitoring.go b/app/console/commands/monitoring.go deleted file mode 100644 index 59a24494..00000000 --- a/app/console/commands/monitoring.go +++ /dev/null @@ -1,91 +0,0 @@ -package commands - -import ( - "context" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/color" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/tools" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// Monitoring 系统监控 -type Monitoring struct { -} - -// Signature The name and signature of the console command. -func (receiver *Monitoring) Signature() string { - return "panel:monitoring" -} - -// Description The console command description. -func (receiver *Monitoring) Description() string { - ctx := context.Background() - return facades.Lang(ctx).Get("commands.panel:monitoring.description") -} - -// Extend The console command extend. -func (receiver *Monitoring) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *Monitoring) Handle(console.Context) error { - if types.Status != types.StatusNormal { - return nil - } - - // 将等待中的任务分发 - task := services.NewTaskImpl() - _ = task.DispatchWaiting() - - setting := services.NewSettingImpl() - monitor := setting.Get(models.SettingKeyMonitor) - if !cast.ToBool(monitor) { - return nil - } - - info := tools.GetMonitoringInfo() - translate := facades.Lang(context.Background()) - - // 去除部分数据以减少数据库存储 - info.Disk = nil - info.Cpus = nil - - if types.Status != types.StatusNormal { - return nil - } - err := facades.Orm().Query().Create(&models.Monitor{ - Info: info, - }) - if err != nil { - facades.Log().Tags("面板", "系统监控").With(map[string]any{ - "error": err.Error(), - }).Infof("保存失败") - color.Red().Printfln(translate.Get("commands.panel:monitoring.fail")+": %s", err.Error()) - return nil - } - - // 删除过期数据 - days := cast.ToInt(setting.Get(models.SettingKeyMonitorDays)) - if days <= 0 || types.Status != types.StatusNormal { - return nil - } - if _, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{}); err != nil { - facades.Log().Tags("面板", "系统监控").With(map[string]any{ - "error": err.Error(), - }).Infof("删除过期数据失败") - return nil - } - - return nil -} diff --git a/app/console/commands/panel.go b/app/console/commands/panel.go deleted file mode 100644 index 0acb6c40..00000000 --- a/app/console/commands/panel.go +++ /dev/null @@ -1,732 +0,0 @@ -package commands - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/color" - "github.com/spf13/cast" - - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/tools" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// Panel 面板命令行 -type Panel struct { -} - -// Signature The name and signature of the console command. -func (receiver *Panel) Signature() string { - return "panel" -} - -// Description The console command description. -func (receiver *Panel) Description() string { - ctx := context.Background() - return facades.Lang(ctx).Get("commands.panel.description") -} - -// Extend The console command extend. -func (receiver *Panel) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *Panel) Handle(ctx console.Context) error { - action := ctx.Argument(0) - arg1 := ctx.Argument(1) - arg2 := ctx.Argument(2) - arg3 := ctx.Argument(3) - arg4 := ctx.Argument(4) - arg5 := ctx.Argument(5) - - translate := facades.Lang(context.Background()) - - switch action { - case "init": - var check models.User - err := facades.Orm().Query().FirstOrFail(&check) - if err == nil { - color.Red().Printfln(translate.Get("commands.panel.init.exist")) - return nil - } - - settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}} - err = facades.Orm().Query().Create(&settings) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.init.fail")) - return nil - } - - hash, err := facades.Hash().Make(str.RandomString(32)) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.init.fail")) - return nil - } - - user := services.NewUserImpl() - _, err = user.Create("admin", hash) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.init.adminFail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.init.success")) - - case "update": - var task models.Task - if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil { - color.Red().Printfln(translate.Get("commands.panel.update.taskCheck")) - return nil - } - if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { - types.Status = types.StatusFailed - color.Red().Printfln(translate.Get("commands.panel.update.dbFail")) - return nil - } - - panel, err := tools.GetLatestPanelVersion() - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.update.versionFail")) - return err - } - - // 停止面板服务,因为在shell中运行的和systemd的不同 - _ = systemctl.Stop("panel") - - types.Status = types.StatusUpgrade - if err = tools.UpdatePanel(panel); err != nil { - types.Status = types.StatusFailed - color.Red().Printfln(translate.Get("commands.panel.update.fail") + ": " + err.Error()) - return nil - } - - types.Status = types.StatusNormal - color.Green().Printfln(translate.Get("commands.panel.update.success")) - tools.RestartPanel() - - case "getInfo": - var user models.User - err := facades.Orm().Query().Where("id", 1).FirstOrFail(&user) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.getInfo.adminGetFail")) - return nil - } - - password := str.RandomString(16) - hash, err := facades.Hash().Make(password) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.getInfo.passwordGenerationFail")) - return nil - } - user.Username = str.RandomString(8) - user.Password = hash - if user.Email == "" { - user.Email = str.RandomString(8) + "@example.com" - } - - err = facades.Orm().Query().Save(&user) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.getInfo.adminSaveFail")) - return nil - } - - port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.portFail")) - return nil - } - ip, err := tools.GetPublicIP() - if err != nil { - ip = "127.0.0.1" - } - protocol := "http" - if facades.Config().GetBool("panel.ssl") { - protocol = "https" - } - - color.Green().Printfln(translate.Get("commands.panel.getInfo.username") + ": " + user.Username) - color.Green().Printfln(translate.Get("commands.panel.getInfo.password") + ": " + password) - color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port) - color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("panel.entrance")) - color.Green().Printfln(translate.Get("commands.panel.getInfo.address") + ": " + protocol + "://" + ip + ":" + port + facades.Config().GetString("panel.entrance")) - - case "getPort": - port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.portFail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port) - - case "getEntrance": - color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("panel.entrance")) - - case "deleteEntrance": - oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail")) - return nil - } - if _, err = shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=/!g' /www/panel/panel.conf"); err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deleteEntrance.success")) - - case "writePlugin": - slug := arg1 - version := arg2 - if len(slug) == 0 || len(version) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writePlugin.paramFail")) - return nil - } - - var plugin models.Plugin - err := facades.Orm().Query().UpdateOrCreate(&plugin, models.Plugin{ - Slug: slug, - }, models.Plugin{ - Version: version, - }) - - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writePlugin.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writePlugin.success")) - - case "deletePlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.deletePlugin.paramFail")) - return nil - } - - _, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{}) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deletePlugin.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deletePlugin.success")) - - case "writeMysqlPassword": - password := arg1 - if len(password) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.paramFail")) - return nil - } - - var setting models.Setting - err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{ - Key: models.SettingKeyMysqlRootPassword, - }, models.Setting{ - Value: password, - }) - - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writeMysqlPassword.success")) - - case "cleanTask": - _, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.cleanTask.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.cleanTask.success")) - - case "backup": - backupType := arg1 - name := arg2 - path := arg3 - save := arg4 - hr := `+----------------------------------------------------` - if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 { - color.Red().Printfln(translate.Get("commands.panel.backup.paramFail")) - return nil - } - - color.Green().Printfln(hr) - color.Green().Printfln("★ " + translate.Get("commands.panel.backup.start") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - if !io.Exists(path) { - if err := io.Mkdir(path, 0644); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupDirFail") + ": " + err.Error()) - return nil - } - } - - switch backupType { - case "website": - color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.targetSite") + ": " + name) - var website models.Website - if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.siteNotExist")) - color.Green().Printfln(hr) - return nil - } - - backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip" - if _, err := shell.Execf(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.backupSuccess")) - - case "mysql": - rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword) - backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - - err := os.Setenv("MYSQL_PWD", rootPassword) - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.mysqlBackupFail") + ": " + err.Error()) - color.Green().Printfln(hr) - return nil - } - - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetMysql") + ": " + name) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport")) - if _, err = shell.Execf(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress")) - if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error()) - return nil - } - if err := io.Remove("/tmp/" + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove")) - if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess")) - _ = os.Unsetenv("MYSQL_PWD") - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success")) - - case "postgresql": - backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - check, err := shell.Execf(`su - postgres -c "psql -l" 2>&1`) - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseGetFail") + ": " + err.Error()) - color.Green().Printfln(hr) - return nil - } - if !strings.Contains(check, name) { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseNotExist")) - color.Green().Printfln(hr) - return nil - } - - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetPostgres") + ": " + name) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport")) - if _, err = shell.Execf(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress")) - if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error()) - return nil - } - if err := io.Remove("/tmp/" + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove")) - if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success")) - } - - color.Green().Printfln(hr) - files, err := os.ReadDir(path) - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error()) - return nil - } - var filteredFiles []os.FileInfo - for _, file := range files { - if strings.HasPrefix(file.Name(), name) && strings.HasSuffix(file.Name(), ".zip") { - fileInfo, err := os.Stat(filepath.Join(path, file.Name())) - if err != nil { - continue - } - filteredFiles = append(filteredFiles, fileInfo) - } - } - sort.Slice(filteredFiles, func(i, j int) bool { - return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime()) - }) - for i := cast.ToInt(save); i < len(filteredFiles); i++ { - fileToDelete := filepath.Join(path, filteredFiles[i].Name()) - color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.cleanBackup") + ": " + fileToDelete) - if err := io.Remove(fileToDelete); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error()) - return nil - } - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.cleanupSuccess")) - color.Green().Printfln(hr) - color.Green().Printfln("☆ " + translate.Get("commands.panel.backup.success") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - case "cutoff": - name := arg1 - save := arg2 - hr := `+----------------------------------------------------` - if len(name) == 0 || len(save) == 0 { - color.Red().Printfln(translate.Get("commands.panel.cutoff.paramFail")) - return nil - } - - color.Green().Printfln(hr) - color.Green().Printfln("★ " + translate.Get("commands.panel.cutoff.start") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.targetSite") + ": " + name) - var website models.Website - if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.siteNotExist")) - color.Green().Printfln(hr) - return nil - } - - logPath := "/www/wwwlogs/" + website.Name + ".log" - if !io.Exists(logPath) { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.logNotExist")) - color.Green().Printfln(hr) - return nil - } - - backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip" - if _, err := shell.Execf(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log"); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.backupFail") + ": " + err.Error()) - return nil - } - if _, err := shell.Execf(`echo "" > ` + logPath); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.clearFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cutSuccess")) - - color.Green().Printfln(hr) - files, err := os.ReadDir("/www/wwwlogs") - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error()) - return nil - } - var filteredFiles []os.FileInfo - for _, file := range files { - if strings.HasPrefix(file.Name(), website.Name) && strings.HasSuffix(file.Name(), ".log.zip") { - fileInfo, err := os.Stat(filepath.Join("/www/wwwlogs", file.Name())) - if err != nil { - continue - } - filteredFiles = append(filteredFiles, fileInfo) - } - } - sort.Slice(filteredFiles, func(i, j int) bool { - return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime()) - }) - for i := cast.ToInt(save); i < len(filteredFiles); i++ { - fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name()) - color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.clearLog") + ": " + fileToDelete) - if err := io.Remove(fileToDelete); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error()) - return nil - } - } - color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupSuccess")) - color.Green().Printfln(hr) - color.Green().Printfln("☆ " + translate.Get("commands.panel.cutoff.end") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - case "writeSite": - name := arg1 - status := cast.ToBool(arg2) - path := arg3 - php := cast.ToInt(arg4) - ssl := cast.ToBool(ctx.Argument(5)) - if len(name) == 0 || len(path) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writeSite.paramFail")) - return nil - } - - var website models.Website - if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err == nil { - color.Red().Printfln(translate.Get("commands.panel.writeSite.siteExist")) - return nil - } - - _, err := os.Stat(path) - if os.IsNotExist(err) { - color.Red().Printfln(translate.Get("commands.panel.writeSite.pathNotExist")) - return nil - } - - err = facades.Orm().Query().Create(&models.Website{ - Name: name, - Status: status, - Path: path, - PHP: php, - SSL: ssl, - }) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writeSite.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writeSite.success")) - - case "deleteSite": - name := arg1 - if len(name) == 0 { - color.Red().Printfln(translate.Get("commands.panel.deleteSite.paramFail")) - return nil - } - - _, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{}) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteSite.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deleteSite.success")) - - case "writeSetting": - key := arg1 - value := arg2 - if len(key) == 0 || len(value) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writeSetting.paramFail")) - return nil - } - - var setting models.Setting - err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{ - Key: key, - }, models.Setting{ - Value: value, - }) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writeSetting.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writeSetting.success")) - - case "getSetting": - key := arg1 - if len(key) == 0 { - color.Red().Printfln(translate.Get("commands.panel.getSetting.paramFail")) - return nil - } - - var setting models.Setting - if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil { - return nil - } - - fmt.Printf("%s", setting.Value) - - case "deleteSetting": - key := arg1 - if len(key) == 0 { - color.Red().Printfln(translate.Get("commands.panel.deleteSetting.paramFail")) - return nil - } - - _, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{}) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteSetting.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deleteSetting.success")) - - case "addSite": - name := arg1 - domain := arg2 - port := arg3 - path := arg4 - php := arg5 - if len(name) == 0 || len(domain) == 0 || len(port) == 0 || len(path) == 0 { - color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail")) - return nil - } - - domains := strings.Split(domain, ",") - ports := strings.Split(port, ",") - if len(domains) == 0 || len(ports) == 0 { - color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail")) - return nil - } - - var uintPorts []uint - for _, p := range ports { - uintPorts = append(uintPorts, cast.ToUint(p)) - } - - website := services.NewWebsiteImpl() - id, err := website.GetIDByName(name) - if err != nil { - color.Red().Printfln(err.Error()) - return nil - } - if id != 0 { - color.Red().Printfln(translate.Get("commands.panel.addSite.siteExist")) - return nil - } - - _, err = website.Add(requests.Add{ - Name: name, - Domains: domains, - Ports: uintPorts, - Path: path, - PHP: php, - DB: false, - }) - if err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.addSite.success")) - - case "removeSite": - name := arg1 - if len(name) == 0 { - color.Red().Printfln(translate.Get("commands.panel.removeSite.paramFail")) - return nil - } - - website := services.NewWebsiteImpl() - id, err := website.GetIDByName(name) - if err != nil { - color.Red().Printfln(err.Error()) - return nil - } - if id == 0 { - color.Red().Printfln(translate.Get("commands.panel.removeSite.siteNotExist")) - return nil - } - - if err = website.Delete(requests.Delete{ID: id}); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.removeSite.success")) - - case "installPlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.installPlugin.paramFail")) - return nil - } - - plugin := services.NewPluginImpl() - if err := plugin.Install(slug); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.installPlugin.success")) - - case "uninstallPlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.uninstallPlugin.paramFail")) - return nil - } - - plugin := services.NewPluginImpl() - if err := plugin.Uninstall(slug); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.uninstallPlugin.success")) - - case "updatePlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.updatePlugin.paramFail")) - return nil - } - - plugin := services.NewPluginImpl() - if err := plugin.Update(slug); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.updatePlugin.success")) - - default: - color.Yellow().Printfln(facades.Config().GetString("panel.name") + " - " + translate.Get("commands.panel.tool") + " - " + facades.Config().GetString("panel.version")) - color.Green().Printfln(translate.Get("commands.panel.use") + ":") - color.Green().Printfln("panel update " + translate.Get("commands.panel.update.description")) - color.Green().Printfln("panel getInfo " + translate.Get("commands.panel.getInfo.description")) - color.Green().Printfln("panel getPort " + translate.Get("commands.panel.getPort.description")) - color.Green().Printfln("panel getEntrance " + translate.Get("commands.panel.getEntrance.description")) - color.Green().Printfln("panel deleteEntrance " + translate.Get("commands.panel.deleteEntrance.description")) - color.Green().Printfln("panel cleanTask " + translate.Get("commands.panel.cleanTask.description")) - color.Green().Printfln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} " + translate.Get("commands.panel.backup.description")) - color.Green().Printfln("panel cutoff {website_name} {save_copies} " + translate.Get("commands.panel.cutoff.description")) - color.Green().Printfln("panel installPlugin {slug} " + translate.Get("commands.panel.installPlugin.description")) - color.Green().Printfln("panel uninstallPlugin {slug} " + translate.Get("commands.panel.uninstallPlugin.description")) - color.Green().Printfln("panel updatePlugin {slug} " + translate.Get("commands.panel.updatePlugin.description")) - color.Green().Printfln("panel addSite {name} {domain} {port} {path} {php} " + translate.Get("commands.panel.addSite.description")) - color.Green().Printfln("panel removeSite {name} " + translate.Get("commands.panel.removeSite.description")) - color.Red().Printfln(translate.Get("commands.panel.forDeveloper") + ":") - color.Yellow().Printfln("panel init " + translate.Get("commands.panel.init.description")) - color.Yellow().Printfln("panel writePlugin {slug} {version} " + translate.Get("commands.panel.writePlugin.description")) - color.Yellow().Printfln("panel deletePlugin {slug} " + translate.Get("commands.panel.deletePlugin.description")) - color.Yellow().Printfln("panel writeMysqlPassword {password} " + translate.Get("commands.panel.writeMysqlPassword.description")) - color.Yellow().Printfln("panel writeSite {name} {status} {path} {php} {ssl} " + translate.Get("commands.panel.writeSite.description")) - color.Yellow().Printfln("panel deleteSite {name} " + translate.Get("commands.panel.deleteSite.description")) - color.Yellow().Printfln("panel getSetting {name} " + translate.Get("commands.panel.getSetting.description")) - color.Yellow().Printfln("panel writeSetting {name} {value} " + translate.Get("commands.panel.writeSetting.description")) - color.Yellow().Printfln("panel deleteSetting {name} " + translate.Get("commands.panel.deleteSetting.description")) - } - - return nil -} diff --git a/app/console/commands/panel_task.go b/app/console/commands/panel_task.go deleted file mode 100644 index f055ccc6..00000000 --- a/app/console/commands/panel_task.go +++ /dev/null @@ -1,79 +0,0 @@ -package commands - -import ( - "context" - "runtime" - "runtime/debug" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// PanelTask 面板每日任务 -type PanelTask struct { -} - -// Signature The name and signature of the console command. -func (receiver *PanelTask) Signature() string { - return "panel:task" -} - -// Description The console command description. -func (receiver *PanelTask) Description() string { - return facades.Lang(context.Background()).Get("commands.panel:task.description") -} - -// Extend The console command extend. -func (receiver *PanelTask) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *PanelTask) Handle(console.Context) error { - types.Status = types.StatusMaintain - - // 优化数据库 - if _, err := facades.Orm().Query().Exec("VACUUM"); err != nil { - types.Status = types.StatusFailed - facades.Log().Tags("面板", "每日任务"). - With(map[string]any{ - "error": err.Error(), - }).Error("优化面板数据库失败") - return err - } - - // 备份面板 - if err := io.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil { - types.Status = types.StatusFailed - facades.Log().Tags("面板", "每日任务"). - With(map[string]any{ - "error": err.Error(), - }).Error("备份面板失败") - return err - } - - // 清理 7 天前的备份 - if _, err := shell.Execf(`find /www/backup/panel -mtime +7 -name "*.zip" -exec rm -rf {} \;`); err != nil { - types.Status = types.StatusFailed - facades.Log().Tags("面板", "每日任务"). - With(map[string]any{ - "error": err.Error(), - }).Error("清理面板备份失败") - return err - } - - // 回收内存 - runtime.GC() - debug.FreeOSMemory() - - types.Status = types.StatusNormal - return nil -} diff --git a/app/console/kernel.go b/app/console/kernel.go deleted file mode 100644 index 42d977ee..00000000 --- a/app/console/kernel.go +++ /dev/null @@ -1,29 +0,0 @@ -package console - -import ( - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/schedule" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/console/commands" -) - -type Kernel struct { -} - -func (kernel *Kernel) Schedule() []schedule.Event { - return []schedule.Event{ - facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(), - facades.Schedule().Command("panel:cert-renew").DailyAt("04:00").SkipIfStillRunning(), - facades.Schedule().Command("panel:task").DailyAt("03:30").SkipIfStillRunning(), - } -} - -func (kernel *Kernel) Commands() []console.Command { - return []console.Command{ - &commands.Panel{}, - &commands.Monitoring{}, - &commands.CertRenew{}, - &commands.PanelTask{}, - } -} diff --git a/app/http/controllers/cert_controller.go b/app/http/controllers/cert_controller.go deleted file mode 100644 index f148ba5e..00000000 --- a/app/http/controllers/cert_controller.go +++ /dev/null @@ -1,706 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - requests "github.com/TheTNB/panel/v2/app/http/requests/cert" - commonrequests "github.com/TheTNB/panel/v2/app/http/requests/common" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/acme" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type CertController struct { - cron internal.Cron - cert internal.Cert -} - -func NewCertController() *CertController { - return &CertController{ - cron: services.NewCronImpl(), - cert: services.NewCertImpl(), - } -} - -// CAProviders -// -// @Summary 获取 CA 提供商 -// @Description 获取面板证书管理支持的 CA 提供商 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/caProviders [get] -func (r *CertController) CAProviders(ctx http.Context) http.Response { - return h.Success(ctx, []map[string]string{ - { - "name": "Let's Encrypt", - "ca": "letsencrypt", - }, - { - "name": "ZeroSSL", - "ca": "zerossl", - }, - { - "name": "SSL.com", - "ca": "sslcom", - }, - { - "name": "Google", - "ca": "google", - }, - { - "name": "Buypass", - "ca": "buypass", - }, - }) -} - -// DNSProviders -// -// @Summary 获取 DNS 提供商 -// @Description 获取面板证书管理支持的 DNS 提供商 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dnsProviders [get] -func (r *CertController) DNSProviders(ctx http.Context) http.Response { - return h.Success(ctx, []map[string]any{ - { - "name": "DNSPod", - "dns": acme.DnsPod, - }, - { - "name": "腾讯云", - "dns": acme.Tencent, - }, - { - "name": "阿里云", - "dns": acme.AliYun, - }, - { - "name": "CloudFlare", - "dns": acme.CloudFlare, - }, - }) -} - -// Algorithms -// -// @Summary 获取算法列表 -// @Description 获取面板证书管理支持的算法列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/algorithms [get] -func (r *CertController) Algorithms(ctx http.Context) http.Response { - return h.Success(ctx, []map[string]any{ - { - "name": "EC256", - "key": acme.KeyEC256, - }, - { - "name": "EC384", - "key": acme.KeyEC384, - }, - { - "name": "RSA2048", - "key": acme.KeyRSA2048, - }, - { - "name": "RSA4096", - "key": acme.KeyRSA4096, - }, - }) -} - -// UserList -// -// @Summary 获取用户列表 -// @Description 获取面板证书管理的 ACME 用户列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users [get] -func (r *CertController) UserList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - var users []models.CertUser - var total int64 - err := facades.Orm().Query().Paginate(paginateRequest.Page, paginateRequest.Limit, &users, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取ACME用户列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": users, - }) -} - -// UserStore -// -// @Summary 添加 ACME 用户 -// @Description 添加 ACME 用户到面板证书管理 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.UserStore true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users [post] -func (r *CertController) UserStore(ctx http.Context) http.Response { - var storeRequest requests.UserStore - sanitize := h.SanitizeRequest(ctx, &storeRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.UserStore(storeRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加ACME用户失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UserUpdate -// -// @Summary 更新 ACME 用户 -// @Description 更新面板证书管理的 ACME 用户 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "用户 ID" -// @Param data body requests.UserUpdate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users/{id} [put] -func (r *CertController) UserUpdate(ctx http.Context) http.Response { - var updateRequest requests.UserUpdate - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.UserUpdate(updateRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "userID": updateRequest.ID, - "error": err.Error(), - }).Info("更新ACME用户失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UserShow -// -// @Summary 获取 ACME 用户 -// @Description 获取面板证书管理的 ACME 用户 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param id path int true "用户 ID" -// @Success 200 {object} SuccessResponse{data=models.CertUser} -// @Router /panel/cert/users/{id} [get] -func (r *CertController) UserShow(ctx http.Context) http.Response { - var showAndDestroyRequest requests.UserShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - user, err := r.cert.UserShow(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "userID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("获取ACME用户失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, user) -} - -// UserDestroy -// -// @Summary 删除 ACME 用户 -// @Description 删除面板证书管理的 ACME 用户 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "用户 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users/{id} [delete] -func (r *CertController) UserDestroy(ctx http.Context) http.Response { - var showAndDestroyRequest requests.UserShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.UserDestroy(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "userID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("删除ACME用户失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DNSList -// -// @Summary 获取 DNS 接口列表 -// @Description 获取面板证书管理的 DNS 接口列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns [get] -func (r *CertController) DNSList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - var dns []models.CertDNS - var total int64 - err := facades.Orm().Query().Paginate(paginateRequest.Page, paginateRequest.Limit, &dns, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取DNS接口列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": dns, - }) -} - -// DNSStore -// -// @Summary 添加 DNS 接口 -// @Description 添加 DNS 接口到面板证书管理 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.DNSStore true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns [post] -func (r *CertController) DNSStore(ctx http.Context) http.Response { - var storeRequest requests.DNSStore - sanitize := h.SanitizeRequest(ctx, &storeRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.DNSStore(storeRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加DNS接口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// DNSShow -// -// @Summary 获取 DNS 接口 -// @Description 获取面板证书管理的 DNS 接口 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param id path int true "DNS 接口 ID" -// @Success 200 {object} SuccessResponse{data=models.CertDNS} -// @Router /panel/cert/dns/{id} [get] -func (r *CertController) DNSShow(ctx http.Context) http.Response { - var showAndDestroyRequest requests.DNSShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - dns, err := r.cert.DNSShow(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "dnsID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("获取DNS接口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, dns) -} - -// DNSUpdate -// -// @Summary 更新 DNS 接口 -// @Description 更新面板证书管理的 DNS 接口 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "DNS 接口 ID" -// @Param data body requests.DNSUpdate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns/{id} [put] -func (r *CertController) DNSUpdate(ctx http.Context) http.Response { - var updateRequest requests.DNSUpdate - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.DNSUpdate(updateRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "dnsID": updateRequest.ID, - "error": err.Error(), - }).Info("更新DNS接口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// DNSDestroy -// -// @Summary 删除 DNS 接口 -// @Description 删除面板证书管理的 DNS 接口 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "DNS 接口 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns/{id} [delete] -func (r *CertController) DNSDestroy(ctx http.Context) http.Response { - var showAndDestroyRequest requests.DNSShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.DNSDestroy(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "dnsID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("删除DNS接口失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// CertList -// -// @Summary 获取证书列表 -// @Description 获取面板证书管理的证书列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs [get] -func (r *CertController) CertList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - var certs []models.Cert - var total int64 - err := facades.Orm().Query().With("Website").With("User").With("DNS").Paginate(paginateRequest.Page, paginateRequest.Limit, &certs, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取证书列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": certs, - }) -} - -// CertStore -// -// @Summary 添加证书 -// @Description 添加证书到面板证书管理 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.CertStore true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs [post] -func (r *CertController) CertStore(ctx http.Context) http.Response { - var storeRequest requests.CertStore - sanitize := h.SanitizeRequest(ctx, &storeRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.CertStore(storeRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// CertUpdate -// -// @Summary 更新证书 -// @Description 更新面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "证书 ID" -// @Param data body requests.CertUpdate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs/{id} [put] -func (r *CertController) CertUpdate(ctx http.Context) http.Response { - var updateRequest requests.CertUpdate - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.CertUpdate(updateRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": updateRequest.ID, - "error": err.Error(), - }).Info("更新证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// CertShow -// -// @Summary 获取证书 -// @Description 获取面板证书管理的证书 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param id path int true "证书 ID" -// @Success 200 {object} SuccessResponse{data=models.Cert} -// @Router /panel/cert/certs/{id} [get] -func (r *CertController) CertShow(ctx http.Context) http.Response { - var showAndDestroyRequest requests.CertShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - cert, err := r.cert.CertShow(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("获取证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, cert) -} - -// CertDestroy -// -// @Summary 删除证书 -// @Description 删除面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "证书 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs/{id} [delete] -func (r *CertController) CertDestroy(ctx http.Context) http.Response { - var showAndDestroyRequest requests.CertShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.CertDestroy(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("删除证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// Obtain -// -// @Summary 签发证书 -// @Description 签发面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Obtain true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/obtain [post] -func (r *CertController) Obtain(ctx http.Context) http.Response { - var obtainRequest requests.Obtain - sanitize := h.SanitizeRequest(ctx, &obtainRequest) - if sanitize != nil { - return sanitize - } - - cert, err := r.cert.CertShow(obtainRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": obtainRequest.ID, - "error": err.Error(), - }).Info("获取证书失败") - return h.ErrorSystem(ctx) - } - - if cert.DNS != nil || cert.Website != nil { - _, err = r.cert.ObtainAuto(obtainRequest.ID) - } else { - _, err = r.cert.ObtainManual(obtainRequest.ID) - } - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("签发证书失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Renew -// -// @Summary 续签证书 -// @Description 续签面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Renew true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/renew [post] -func (r *CertController) Renew(ctx http.Context) http.Response { - var renewRequest requests.Renew - sanitize := h.SanitizeRequest(ctx, &renewRequest) - if sanitize != nil { - return sanitize - } - - _, err := r.cert.Renew(renewRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("续签证书失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ManualDNS -// -// @Summary 获取手动 DNS 记录 -// @Description 获取签发证书所需的 DNS 记录 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Obtain true "request" -// @Success 200 {object} SuccessResponse{data=[]acme.DNSRecord} -// @Router /panel/cert/manualDNS [post] -func (r *CertController) ManualDNS(ctx http.Context) http.Response { - var obtainRequest requests.Obtain - sanitize := h.SanitizeRequest(ctx, &obtainRequest) - if sanitize != nil { - return sanitize - } - - resolves, err := r.cert.ManualDNS(obtainRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取手动DNS记录失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, resolves) -} - -// Deploy -// -// @Summary 部署证书 -// @Description 部署面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.CertDeploy true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/deploy [post] -func (r *CertController) Deploy(ctx http.Context) http.Response { - var deployRequest requests.CertDeploy - sanitize := h.SanitizeRequest(ctx, &deployRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.Deploy(deployRequest.ID, deployRequest.WebsiteID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": deployRequest.ID, - "error": err.Error(), - }).Info("部署证书失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/container_controller.go b/app/http/controllers/container_controller.go deleted file mode 100644 index be82d3ba..00000000 --- a/app/http/controllers/container_controller.go +++ /dev/null @@ -1,1011 +0,0 @@ -package controllers - -import ( - "fmt" - "strconv" - "strings" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - - requests "github.com/TheTNB/panel/v2/app/http/requests/container" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type ContainerController struct { - container services.Container -} - -func NewContainerController() *ContainerController { - return &ContainerController{ - container: services.NewContainer(), - } -} - -// ContainerList -// -// @Summary 获取容器列表 -// @Description 获取所有容器列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/list [get] -func (r *ContainerController) ContainerList(ctx http.Context) http.Response { - containers, err := r.container.ContainerListAll() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, containers) - - items := make([]any, 0) - for _, item := range paged { - var name string - if len(item.Names) > 0 { - name = item.Names[0] - } - items = append(items, map[string]any{ - "id": item.ID, - "name": strings.TrimLeft(name, "/"), - "image": item.Image, - "image_id": item.ImageID, - "command": item.Command, - "created": carbon.FromTimestamp(item.Created).ToDateTimeString(), - "ports": item.Ports, - "labels": item.Labels, - "state": item.State, - "status": item.Status, - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// ContainerSearch -// -// @Summary 搜索容器 -// @Description 根据容器名称搜索容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param name query string true "容器名称" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/search [get] -func (r *ContainerController) ContainerSearch(ctx http.Context) http.Response { - fields := strings.Fields(ctx.Request().Query("name")) - containers, err := r.container.ContainerListByNames(fields) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, containers) -} - -// ContainerCreate -// -// @Summary 创建容器 -// @Description 创建一个容器 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.ContainerCreate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/create [post] -func (r *ContainerController) ContainerCreate(ctx http.Context) http.Response { - var request requests.ContainerCreate - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - var hostConf container.HostConfig - var networkConf network.NetworkingConfig - - portMap := make(nat.PortMap) - for _, port := range request.Ports { - if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd { - return h.Error(ctx, http.StatusUnprocessableEntity, fmt.Sprintf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)) - } - if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 { - return h.Error(ctx, http.StatusUnprocessableEntity, "端口范围不正确") - } - - count := 0 - for host := port.HostStart; host <= port.HostEnd; host++ { - bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host} - portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem} - count++ - } - } - - exposed := make(nat.PortSet) - for port := range portMap { - exposed[port] = struct{}{} - } - - if request.Network != "" { - switch request.Network { - case "host", "none", "bridge": - hostConf.NetworkMode = container.NetworkMode(request.Network) - } - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{request.Network: {}} - } else { - networkConf = network.NetworkingConfig{} - } - - hostConf.Privileged = request.Privileged - hostConf.AutoRemove = request.AutoRemove - hostConf.CPUShares = request.CPUShares - hostConf.PublishAllPorts = request.PublishAllPorts - hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(request.RestartPolicy)} - if request.RestartPolicy == "on-failure" { - hostConf.RestartPolicy.MaximumRetryCount = 5 - } - hostConf.NanoCPUs = request.CPUs * 1000000000 - hostConf.Memory = request.Memory * 1024 * 1024 - hostConf.MemorySwap = 0 - hostConf.PortBindings = portMap - hostConf.Binds = []string{} - - volumes := make(map[string]struct{}) - for _, v := range request.Volumes { - volumes[v.Container] = struct{}{} - hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode)) - } - - id, err := r.container.ContainerCreate(request.Name, - container.Config{ - Image: request.Image, - Env: r.container.KVToSlice(request.Env), - Entrypoint: request.Entrypoint, - Cmd: request.Command, - Labels: r.container.KVToMap(request.Labels), - ExposedPorts: exposed, - OpenStdin: request.OpenStdin, - Tty: request.Tty, - Volumes: volumes, - }, - hostConf, - networkConf, - ) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = r.container.ContainerStart(id); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, id) -} - -// ContainerRemove -// -// @Summary 删除容器 -// @Description 删除一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/remove [post] -func (r *ContainerController) ContainerRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerStart -// -// @Summary 启动容器 -// @Description 启动一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/start [post] -func (r *ContainerController) ContainerStart(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerStart(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerStop -// -// @Summary 停止容器 -// @Description 停止一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/stop [post] -func (r *ContainerController) ContainerStop(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerStop(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerRestart -// -// @Summary 重启容器 -// @Description 重启一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/restart [post] -func (r *ContainerController) ContainerRestart(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerRestart(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerPause -// -// @Summary 暂停容器 -// @Description 暂停一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -func (r *ContainerController) ContainerPause(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerPause(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerUnpause -// -// @Summary 取消暂停容器 -// @Description 取消暂停一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// -// @Router /panel/container/unpause [post] -func (r *ContainerController) ContainerUnpause(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerUnpause(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerInspect -// -// @Summary 查看容器 -// @Description 查看一个容器的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/inspect [get] -func (r *ContainerController) ContainerInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ContainerInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// ContainerKill -// -// @Summary 杀死容器 -// @Description 杀死一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/kill [post] -func (r *ContainerController) ContainerKill(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerKill(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerRename -// -// @Summary 重命名容器 -// @Description 重命名一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ContainerRename true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/rename [post] -func (r *ContainerController) ContainerRename(ctx http.Context) http.Response { - var request requests.ContainerRename - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerRename(request.ID, request.Name); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerStats -// -// @Summary 查看容器状态 -// @Description 查看一个容器的状态信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/stats [get] -func (r *ContainerController) ContainerStats(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ContainerStats(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// ContainerExist -// -// @Summary 检查容器是否存在 -// @Description 检查一个容器是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/exist [get] -func (r *ContainerController) ContainerExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.ContainerExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// ContainerLogs -// -// @Summary 查看容器日志 -// @Description 查看一个容器的日志 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/logs [get] -func (r *ContainerController) ContainerLogs(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ContainerLogs(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// ContainerPrune -// -// @Summary 清理容器 -// @Description 清理无用的容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/prune [post] -func (r *ContainerController) ContainerPrune(ctx http.Context) http.Response { - if err := r.container.ContainerPrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkList -// -// @Summary 获取网络列表 -// @Description 获取所有网络列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/list [get] -func (r *ContainerController) NetworkList(ctx http.Context) http.Response { - networks, err := r.container.NetworkList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, networks) - - items := make([]any, 0) - for _, item := range paged { - var ipamConfig []any - for _, v := range item.IPAM.Config { - ipamConfig = append(ipamConfig, map[string]any{ - "subnet": v.Subnet, - "gateway": v.Gateway, - "ip_range": v.IPRange, - "aux_address": v.AuxAddress, - }) - } - items = append(items, map[string]any{ - "id": item.ID, - "name": item.Name, - "driver": item.Driver, - "ipv6": item.EnableIPv6, - "scope": item.Scope, - "internal": item.Internal, - "attachable": item.Attachable, - "ingress": item.Ingress, - "labels": item.Labels, - "options": item.Options, - "ipam": map[string]any{ - "config": ipamConfig, - "driver": item.IPAM.Driver, - "options": item.IPAM.Options, - }, - "created": carbon.FromStdTime(item.Created).ToDateTimeString(), - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// NetworkCreate -// -// @Summary 创建网络 -// @Description 创建一个网络 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NetworkCreate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/create [post] -func (r *ContainerController) NetworkCreate(ctx http.Context) http.Response { - var request requests.NetworkCreate - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - id, err := r.container.NetworkCreate(request) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, id) -} - -// NetworkRemove -// -// @Summary 删除网络 -// @Description 删除一个网络 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/remove [post] -func (r *ContainerController) NetworkRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.NetworkRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkExist -// -// @Summary 检查网络是否存在 -// @Description 检查一个网络是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/exist [get] -func (r *ContainerController) NetworkExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.NetworkExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// NetworkInspect -// -// @Summary 查看网络 -// @Description 查看一个网络的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/inspect [get] -func (r *ContainerController) NetworkInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.NetworkInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// NetworkConnect -// -// @Summary 连接容器到网络 -// @Description 连接一个容器到一个网络 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NetworkConnectDisConnect true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/connect [post] -func (r *ContainerController) NetworkConnect(ctx http.Context) http.Response { - var request requests.NetworkConnectDisConnect - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.NetworkConnect(request.Network, request.Container); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkDisconnect -// -// @Summary 从网络断开容器 -// @Description 从一个网络断开一个容器 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NetworkConnectDisConnect true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/disconnect [post] -func (r *ContainerController) NetworkDisconnect(ctx http.Context) http.Response { - var request requests.NetworkConnectDisConnect - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.NetworkDisconnect(request.Network, request.Container); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkPrune -// -// @Summary 清理网络 -// @Description 清理无用的网络 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/prune [post] -func (r *ContainerController) NetworkPrune(ctx http.Context) http.Response { - if err := r.container.NetworkPrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImageList -// -// @Summary 获取镜像列表 -// @Description 获取所有镜像列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/list [get] -func (r *ContainerController) ImageList(ctx http.Context) http.Response { - images, err := r.container.ImageList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, images) - - items := make([]any, 0) - for _, item := range paged { - items = append(items, map[string]any{ - "id": item.ID, - "created": carbon.FromTimestamp(item.Created).ToDateTimeString(), - "containers": item.Containers, - "size": str.FormatBytes(float64(item.Size)), - "labels": item.Labels, - "repo_tags": item.RepoTags, - "repo_digests": item.RepoDigests, - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// ImageExist -// -// @Summary 检查镜像是否存在 -// @Description 检查一个镜像是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/exist [get] -func (r *ContainerController) ImageExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.ImageExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// ImagePull -// -// @Summary 拉取镜像 -// @Description 拉取一个镜像 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.ImagePull true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/pull [post] -func (r *ContainerController) ImagePull(ctx http.Context) http.Response { - var request requests.ImagePull - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ImagePull(request); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImageRemove -// -// @Summary 删除镜像 -// @Description 删除一个镜像 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/remove [post] -func (r *ContainerController) ImageRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ImageRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImagePrune -// -// @Summary 清理镜像 -// @Description 清理无用的镜像 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/prune [post] -func (r *ContainerController) ImagePrune(ctx http.Context) http.Response { - if err := r.container.ImagePrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImageInspect -// -// @Summary 查看镜像 -// @Description 查看一个镜像的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/inspect [get] -func (r *ContainerController) ImageInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ImageInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// VolumeList -// -// @Summary 获取卷列表 -// @Description 获取所有卷列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/list [get] -func (r *ContainerController) VolumeList(ctx http.Context) http.Response { - volumes, err := r.container.VolumeList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, volumes) - - items := make([]any, 0) - for _, item := range paged { - var usage any - if item.UsageData != nil { - usage = map[string]any{ - "ref_count": item.UsageData.RefCount, - "size": str.FormatBytes(float64(item.UsageData.Size)), - } - } - items = append(items, map[string]any{ - "id": item.Name, - "created": carbon.Parse(item.CreatedAt).ToDateTimeString(), - "driver": item.Driver, - "mount": item.Mountpoint, - "labels": item.Labels, - "options": item.Options, - "scope": item.Scope, - "status": item.Status, - "usage": usage, - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// VolumeCreate -// -// @Summary 创建卷 -// @Description 创建一个卷 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.VolumeCreate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/create [post] -func (r *ContainerController) VolumeCreate(ctx http.Context) http.Response { - var request requests.VolumeCreate - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.VolumeCreate(request) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data.Name) -} - -// VolumeExist -// -// @Summary 检查卷是否存在 -// @Description 检查一个卷是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/exist [get] -func (r *ContainerController) VolumeExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.VolumeExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// VolumeInspect -// -// @Summary 查看卷 -// @Description 查看一个卷的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/inspect [get] -func (r *ContainerController) VolumeInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.VolumeInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// VolumeRemove -// -// @Summary 删除卷 -// @Description 删除一个卷 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/remove [post] -func (r *ContainerController) VolumeRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.VolumeRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// VolumePrune -// -// @Summary 清理卷 -// @Description 清理无用的卷 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/prune [post] -func (r *ContainerController) VolumePrune(ctx http.Context) http.Response { - if err := r.container.VolumePrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/cron_controller.go b/app/http/controllers/cron_controller.go deleted file mode 100644 index 7b5c76c3..00000000 --- a/app/http/controllers/cron_controller.go +++ /dev/null @@ -1,296 +0,0 @@ -package controllers - -import ( - "regexp" - "strconv" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type CronController struct { - cron internal.Cron - setting internal.Setting -} - -func NewCronController() *CronController { - return &CronController{ - cron: services.NewCronImpl(), - setting: services.NewSettingImpl(), - } -} - -// List 获取计划任务列表 -func (r *CronController) List(ctx http.Context) http.Response { - limit := ctx.Request().QueryInt("limit", 10) - page := ctx.Request().QueryInt("page", 1) - - var crons []models.Cron - var total int64 - err := facades.Orm().Query().Paginate(page, limit, &crons, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("查询计划任务列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": crons, - }) -} - -// Add 添加计划任务 -func (r *CronController) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - "time": "required", - "script": "required", - "type": "required|in:shell,backup,cutoff", - "backup_type": "required_if:type,backup|in:website,mysql,postgresql", - }); sanitize != nil { - return sanitize - } - - // 单独验证时间格式 - if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) { - return h.Error(ctx, http.StatusUnprocessableEntity, "时间格式错误") - } - - script := ctx.Request().Input("script") - cronType := ctx.Request().Input("type") - if cronType == "backup" { - backupType := ctx.Request().Input("backup_type") - backupName := ctx.Request().Input("database") - if backupType == "website" { - backupName = ctx.Request().Input("website") - } - backupPath := ctx.Request().Input("backup_path") - if len(backupPath) == 0 { - backupPath = r.setting.Get(models.SettingKeyBackupPath) + "/" + backupType - } - backupSave := ctx.Request().InputInt("save", 10) - script = `#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -# 耗子面板 - 数据备份脚本 - -type=` + backupType + ` -path=` + backupPath + ` -name=` + backupName + ` -save=` + cast.ToString(backupSave) + ` - -# 执行备份 -panel backup ${type} ${name} ${path} ${save} 2>&1 -` - } - if cronType == "cutoff" { - website := ctx.Request().Input("website") - save := ctx.Request().InputInt("save", 180) - script = `#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -# 耗子面板 - 日志切割脚本 - -name=` + website + ` -save=` + cast.ToString(save) + ` - -# 执行切割 -panel cutoff ${name} ${save} 2>&1 -` - } - - shellDir := "/www/server/cron/" - shellLogDir := "/www/server/cron/logs/" - if !io.Exists(shellDir) { - return h.Error(ctx, http.StatusInternalServerError, "计划任务目录不存在") - } - if !io.Exists(shellLogDir) { - return h.Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在") - } - shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + str.RandomString(16) - if err := io.Write(shellDir+shellFile+".sh", script, 0700); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("dos2unix " + shellDir + shellFile + ".sh"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - var cron models.Cron - cron.Name = ctx.Request().Input("name") - cron.Type = ctx.Request().Input("type") - cron.Status = true - cron.Time = ctx.Request().Input("time") - cron.Shell = shellDir + shellFile + ".sh" - cron.Log = shellLogDir + shellFile + ".log" - - if err := facades.Orm().Query().Create(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("保存计划任务失败") - return h.ErrorSystem(ctx) - } - - if err := r.cron.AddToSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "id": cron.ID, - }) -} - -// Script 获取脚本内容 -func (r *CronController) Script(ctx http.Context) http.Response { - var cron models.Cron - err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - script, err := io.Read(cron.Shell) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, script) -} - -// Update 更新计划任务 -func (r *CronController) Update(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - "time": "required", - "script": "required", - }); sanitize != nil { - return sanitize - } - - // 单独验证时间格式 - if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) { - return h.Error(ctx, http.StatusUnprocessableEntity, "时间格式错误") - } - - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - if !cron.Status { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务已禁用") - } - - cron.Time = ctx.Request().Input("time") - cron.Name = ctx.Request().Input("name") - if err := facades.Orm().Query().Save(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("更新计划任务失败") - return h.ErrorSystem(ctx) - } - - if err := io.Write(cron.Shell, ctx.Request().Input("script"), 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("dos2unix " + cron.Shell); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := r.cron.DeleteFromSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if cron.Status { - if err := r.cron.AddToSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - return h.Success(ctx, nil) -} - -// Delete 删除计划任务 -func (r *CronController) Delete(ctx http.Context) http.Response { - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - if err := r.cron.DeleteFromSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := io.Remove(cron.Shell); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if _, err := facades.Orm().Query().Delete(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("删除计划任务失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// Status 更新计划任务状态 -func (r *CronController) Status(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "status": "bool", - }); sanitize != nil { - return sanitize - } - - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - cron.Status = ctx.Request().InputBool("status") - if err := facades.Orm().Query().Save(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("更新计划任务状态失败") - return h.ErrorSystem(ctx) - } - - if err := r.cron.DeleteFromSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if cron.Status { - if err := r.cron.AddToSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - return h.Success(ctx, nil) -} - -// Log 获取计划任务日志 -func (r *CronController) Log(ctx http.Context) http.Response { - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - if !io.Exists(cron.Log) { - return h.Error(ctx, http.StatusUnprocessableEntity, "日志文件不存在") - } - - log, err := shell.Execf("tail -n 1000 " + cron.Log) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, log) -} diff --git a/app/http/controllers/file_controller.go b/app/http/controllers/file_controller.go deleted file mode 100644 index d85e81d8..00000000 --- a/app/http/controllers/file_controller.go +++ /dev/null @@ -1,519 +0,0 @@ -package controllers - -import ( - "fmt" - stdio "io" - stdos "os" - "path/filepath" - "strconv" - "strings" - "syscall" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - - requests "github.com/TheTNB/panel/v2/app/http/requests/file" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type FileController struct { -} - -func NewFileController() *FileController { - return &FileController{} -} - -// Create -// -// @Summary 创建文件/目录 -// @Description 创建文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NotExist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/create [post] -func (r *FileController) Create(ctx http.Context) http.Response { - var request requests.NotExist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - isDir := ctx.Request().InputBool("dir") - if !isDir { - if out, err := shell.Execf("touch " + request.Path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if err := io.Mkdir(request.Path, 0755); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Content -// -// @Summary 获取文件内容 -// @Description 获取给定路径的文件内容 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data query requests.Exist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/content [get] -func (r *FileController) Content(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfo, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if fileInfo.IsDir() { - return h.Error(ctx, http.StatusInternalServerError, "目标路径不是文件") - } - if fileInfo.Size() > 10*1024*1024 { - return h.Error(ctx, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑") - } - - content, err := io.Read(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, content) -} - -// Save -// -// @Summary 保存文件内容 -// @Description 保存给定路径的文件内容 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Save true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/save [post] -func (r *FileController) Save(ctx http.Context) http.Response { - var request requests.Save - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfo, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = io.Write(request.Path, request.Content, fileInfo.Mode()); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Delete -// -// @Summary 删除文件/目录 -// @Description 删除给定路径的文件/目录 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Exist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/delete [post] -func (r *FileController) Delete(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if err := io.Remove(request.Path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Upload -// -// @Summary 上传文件 -// @Description 上传文件到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param file formData file true "file" -// @Param path formData string true "path" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/upload [post] -func (r *FileController) Upload(ctx http.Context) http.Response { - var request requests.Upload - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - src, err := request.File.Open() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - defer src.Close() - - if io.Exists(request.Path) && !ctx.Request().InputBool("force") { - return h.Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?") - } - - data, err := stdio.ReadAll(src) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = io.Write(request.Path, string(data), 0755); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Move -// -// @Summary 移动文件/目录 -// @Description 移动文件/目录到给定路径,等效于重命名 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Move true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/move [post] -func (r *FileController) Move(ctx http.Context) http.Response { - var request requests.Move - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if io.Exists(request.Target) && !ctx.Request().InputBool("force") { - return h.Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在") - } - - if err := io.Mv(request.Source, request.Target); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Target, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Copy -// -// @Summary 复制文件/目录 -// @Description 复制文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Copy true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/copy [post] -func (r *FileController) Copy(ctx http.Context) http.Response { - var request requests.Copy - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if io.Exists(request.Target) && !ctx.Request().InputBool("force") { - return h.Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在") - } - - if err := io.Cp(request.Source, request.Target); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Source, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Download -// -// @Summary 下载文件 -// @Description 下载给定路径的文件 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data query requests.NotExist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/download [get] -func (r *FileController) Download(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - info, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if info.IsDir() { - return h.Error(ctx, http.StatusInternalServerError, "不能下载目录") - } - - return ctx.Response().Download(request.Path, info.Name()) -} - -// RemoteDownload -// -// @Summary 下载远程文件 -// @Description 下载远程文件到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NotExist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/remoteDownload [post] -func (r *FileController) RemoteDownload(ctx http.Context) http.Response { - var request requests.NotExist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - // TODO 使用异步任务下载文件 - return nil -} - -// Info -// -// @Summary 获取文件/目录信息 -// @Description 获取给定路径的文件/目录信息 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data query requests.Exist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/info [get] -func (r *FileController) Info(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfo, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "name": fileInfo.Name(), - "size": str.FormatBytes(float64(fileInfo.Size())), - "mode_str": fileInfo.Mode().String(), - "mode": fmt.Sprintf("%04o", fileInfo.Mode().Perm()), - "dir": fileInfo.IsDir(), - "modify": carbon.FromStdTime(fileInfo.ModTime()).ToDateTimeString(), - }) -} - -// Permission -// -// @Summary 修改文件/目录权限 -// @Description 修改给定路径的文件/目录权限 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Permission true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/permission [post] -func (r *FileController) Permission(ctx http.Context) http.Response { - var request requests.Permission - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - // 解析成8进制 - mode, err := strconv.ParseUint(request.Mode, 8, 64) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = io.Chmod(request.Path, stdos.FileMode(mode)); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = io.Chown(request.Path, request.Owner, request.Group); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Archive -// -// @Summary 压缩文件/目录 -// @Description 压缩文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Archive true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/archive [post] -func (r *FileController) Archive(ctx http.Context) http.Response { - var request requests.Archive - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if err := io.Archive(request.Paths, request.File); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.File, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// UnArchive -// -// @Summary 解压文件/目录 -// @Description 解压文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.UnArchive true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/unArchive [post] -func (r *FileController) UnArchive(ctx http.Context) http.Response { - var request requests.UnArchive - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if err := io.UnArchive(request.File, request.Path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Search -// -// @Summary 搜索文件/目录 -// @Description 通过关键词搜索给定路径的文件/目录 -// @Tags 文件 -// @Accept json -// @Produce json -// @Param data body requests.Search true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/search [post] -func (r *FileController) Search(ctx http.Context) http.Response { - var request requests.Search - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - paths := make(map[string]stdos.FileInfo) - err := filepath.Walk(request.Path, func(path string, info stdos.FileInfo, err error) error { - if err != nil { - return err - } - if strings.Contains(info.Name(), request.KeyWord) { - paths[path] = info - } - return nil - }) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, paths) -} - -// List -// -// @Summary 获取文件/目录列表 -// @Description 获取给定路径的文件/目录列表 -// @Tags 文件 -// @Accept json -// @Produce json -// @Param data query requests.Exist true "request" -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/list [get] -func (r *FileController) List(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfoList, err := io.ReadDir(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - var paths []any - for _, fileInfo := range fileInfoList { - info, _ := fileInfo.Info() - stat := info.Sys().(*syscall.Stat_t) - - paths = append(paths, map[string]any{ - "name": info.Name(), - "full": filepath.Join(request.Path, info.Name()), - "size": str.FormatBytes(float64(info.Size())), - "mode_str": info.Mode().String(), - "mode": fmt.Sprintf("%04o", info.Mode().Perm()), - "owner": os.GetUser(stat.Uid), - "group": os.GetGroup(stat.Gid), - "uid": stat.Uid, - "gid": stat.Gid, - "hidden": io.IsHidden(info.Name()), - "symlink": io.IsSymlink(info.Mode()), - "link": io.GetSymlink(filepath.Join(request.Path, info.Name())), - "dir": info.IsDir(), - "modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(), - }) - } - - paged, total := h.Paginate(ctx, paths) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// setPermission -func (r *FileController) setPermission(path string, mode stdos.FileMode, owner, group string) { - _ = io.Chmod(path, mode) - _ = io.Chown(path, owner, group) -} diff --git a/app/http/controllers/info_controller.go b/app/http/controllers/info_controller.go deleted file mode 100644 index b3c07812..00000000 --- a/app/http/controllers/info_controller.go +++ /dev/null @@ -1,361 +0,0 @@ -package controllers - -import ( - "database/sql" - "fmt" - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/hashicorp/go-version" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/tools" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type MenuItem struct { - Name string `json:"name"` - Title string `json:"title"` - Icon string `json:"icon"` - Jump string `json:"jump"` -} - -type InfoController struct { - plugin internal.Plugin - setting internal.Setting -} - -func NewInfoController() *InfoController { - return &InfoController{ - plugin: services.NewPluginImpl(), - setting: services.NewSettingImpl(), - } -} - -// Panel 获取面板信息 -func (r *InfoController) Panel(ctx http.Context) http.Response { - return h.Success(ctx, http.Json{ - "name": r.setting.Get(models.SettingKeyName), - "language": facades.Config().GetString("app.locale"), - }) -} - -// HomePlugins 获取首页插件 -func (r *InfoController) HomePlugins(ctx http.Context) http.Response { - var plugins []models.Plugin - err := facades.Orm().Query().Where("show", 1).Find(&plugins) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取首页插件失败") - return h.ErrorSystem(ctx) - } - - type pluginsData struct { - models.Plugin - Name string `json:"name"` - } - - var pluginsJson []pluginsData - for _, plugin := range plugins { - pluginsJson = append(pluginsJson, pluginsData{ - Plugin: plugin, - Name: r.plugin.GetBySlug(plugin.Slug).Name, - }) - } - - return h.Success(ctx, pluginsJson) -} - -// NowMonitor 获取当前监控信息 -func (r *InfoController) NowMonitor(ctx http.Context) http.Response { - return h.Success(ctx, tools.GetMonitoringInfo()) -} - -// SystemInfo 获取系统信息 -func (r *InfoController) SystemInfo(ctx http.Context) http.Response { - monitorInfo := tools.GetMonitoringInfo() - - return h.Success(ctx, http.Json{ - "os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion, - "uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400), - "panel_version": facades.Config().GetString("panel.version"), - }) -} - -// CountInfo 获取面板统计信息 -func (r *InfoController) CountInfo(ctx http.Context) http.Response { - var websiteCount int64 - err := facades.Orm().Query().Model(models.Website{}).Count(&websiteCount) - if err != nil { - websiteCount = -1 - } - - var mysql models.Plugin - mysqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql) - if err != nil { - mysqlInstalled = false - } - var postgresql models.Plugin - postgresqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql) - if err != nil { - postgresqlInstalled = false - } - var databaseCount int64 - if mysqlInstalled { - status, err := systemctl.Status("mysqld") - if status && err == nil { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - type database struct { - Name string `json:"name"` - } - - db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/") - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取数据库列表失败") - databaseCount = -1 - } else { - defer db.Close() - rows, err := db.Query("SHOW DATABASES") - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取数据库列表失败") - databaseCount = -1 - } else { - defer rows.Close() - var databases []database - for rows.Next() { - var d database - err := rows.Scan(&d.Name) - if err != nil { - continue - } - if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" { - continue - } - - databases = append(databases, d) - } - databaseCount = int64(len(databases)) - } - } - } - } - if postgresqlInstalled { - status, err := systemctl.Status("postgresql") - if status && err == nil { - raw, err := shell.Execf(`echo "\l" | su - postgres -c "psql"`) - if err == nil { - databases := strings.Split(raw, "\n") - if len(databases) >= 4 { - databases = databases[3 : len(databases)-1] - for _, db := range databases { - parts := strings.Split(db, "|") - if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 || strings.TrimSpace(parts[0]) == "template0" || strings.TrimSpace(parts[0]) == "template1" || strings.TrimSpace(parts[0]) == "postgres" { - continue - } - - databaseCount++ - } - } - } - } - } - - var ftpCount int64 - var ftpPlugin = r.plugin.GetInstalledBySlug("pureftpd") - if ftpPlugin.ID != 0 { - listRaw, err := shell.Execf("pure-pw list") - if len(listRaw) != 0 && err == nil { - listArr := strings.Split(listRaw, "\n") - ftpCount = int64(len(listArr)) - } - } - - var cronCount int64 - err = facades.Orm().Query().Model(models.Cron{}).Count(&cronCount) - if err != nil { - cronCount = -1 - } - - return h.Success(ctx, http.Json{ - "website": websiteCount, - "database": databaseCount, - "ftp": ftpCount, - "cron": cronCount, - }) -} - -// InstalledDbAndPhp 获取已安装的数据库和 PHP 版本 -func (r *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response { - var php []models.Plugin - err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php) - if err != nil { - return h.ErrorSystem(ctx) - } - - var mysql models.Plugin - mysqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql) - if err != nil { - mysqlInstalled = false - } - - var postgresql models.Plugin - postgresqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql) - if err != nil { - postgresqlInstalled = false - } - - type data struct { - Label string `json:"label"` - Value string `json:"value"` - } - var phpData []data - var dbData []data - phpData = append(phpData, data{Value: "0", Label: "不使用"}) - dbData = append(dbData, data{Value: "0", Label: "不使用"}) - for _, p := range php { - match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug) - if len(match) == 0 { - continue - } - - phpData = append(phpData, data{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: r.plugin.GetBySlug(p.Slug).Name}) - } - - if mysqlInstalled { - dbData = append(dbData, data{Value: "mysql", Label: "MySQL"}) - } - if postgresqlInstalled { - dbData = append(dbData, data{Value: "postgresql", Label: "PostgreSQL"}) - } - - return h.Success(ctx, http.Json{ - "php": phpData, - "db": dbData, - }) -} - -// CheckUpdate 检查面板更新 -func (r *InfoController) CheckUpdate(ctx http.Context) http.Response { - current := facades.Config().GetString("panel.version") - latest, err := tools.GetLatestPanelVersion() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取最新版本失败") - } - - v1, err := version.NewVersion(current) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - v2, err := version.NewVersion(latest.Version) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - if v1.GreaterThanOrEqual(v2) { - return h.Success(ctx, http.Json{ - "update": false, - }) - } - - return h.Success(ctx, http.Json{ - "update": true, - }) -} - -// UpdateInfo 获取更新信息 -func (r *InfoController) UpdateInfo(ctx http.Context) http.Response { - current := facades.Config().GetString("panel.version") - latest, err := tools.GetLatestPanelVersion() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取最新版本失败") - } - - v1, err := version.NewVersion(current) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - v2, err := version.NewVersion(latest.Version) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - if v1.GreaterThanOrEqual(v2) { - return h.Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本") - } - - versions, err := tools.GenerateVersions(current, latest.Version) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取更新信息失败") - } - - var versionInfo []tools.PanelInfo - for _, v := range versions { - info, err := tools.GetPanelVersion(v) - if err != nil { - continue - } - - versionInfo = append(versionInfo, info) - } - - return h.Success(ctx, versionInfo) -} - -// Update 更新面板 -func (r *InfoController) Update(ctx http.Context) http.Response { - var task models.Task - if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil { - return h.Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新") - } - if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { - types.Status = types.StatusFailed - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("面板数据库异常,已终止操作:%s", err.Error())) - } - - panel, err := tools.GetLatestPanelVersion() - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取最新版本失败") - return h.Error(ctx, http.StatusInternalServerError, "获取最新版本失败") - } - - types.Status = types.StatusUpgrade - if err = tools.UpdatePanel(panel); err != nil { - types.Status = types.StatusFailed - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("更新面板失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - types.Status = types.StatusNormal - tools.RestartPanel() - return h.Success(ctx, nil) -} - -// Restart 重启面板 -func (r *InfoController) Restart(ctx http.Context) http.Response { - var task models.Task - err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task) - if err == nil { - return h.Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启") - } - - tools.RestartPanel() - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/plugin_controller.go b/app/http/controllers/plugin_controller.go deleted file mode 100644 index 5c6e56a4..00000000 --- a/app/http/controllers/plugin_controller.go +++ /dev/null @@ -1,207 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type PluginController struct { - plugin internal.Plugin - task internal.Task -} - -func NewPluginController() *PluginController { - return &PluginController{ - plugin: services.NewPluginImpl(), - task: services.NewTaskImpl(), - } -} - -// List -// -// @Summary 插件列表 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/list [get] -func (r *PluginController) List(ctx http.Context) http.Response { - plugins := r.plugin.All() - installedPlugins, err := r.plugin.AllInstalled() - if err != nil { - return h.ErrorSystem(ctx) - } - - installedPluginsMap := make(map[string]models.Plugin) - - for _, p := range installedPlugins { - installedPluginsMap[p.Slug] = p - } - - type plugin struct { - Name string `json:"name"` - Description string `json:"description"` - Slug string `json:"slug"` - Version string `json:"version"` - Requires []string `json:"requires"` - Excludes []string `json:"excludes"` - Installed bool `json:"installed"` - InstalledVersion string `json:"installed_version"` - Show bool `json:"show"` - } - - var pluginArr []plugin - for _, item := range plugins { - installed, installedVersion, show := false, "", false - if _, ok := installedPluginsMap[item.Slug]; ok { - installed = true - installedVersion = installedPluginsMap[item.Slug].Version - show = installedPluginsMap[item.Slug].Show - } - pluginArr = append(pluginArr, plugin{ - Name: item.Name, - Description: item.Description, - Slug: item.Slug, - Version: item.Version, - Requires: item.Requires, - Excludes: item.Excludes, - Installed: installed, - InstalledVersion: installedVersion, - Show: show, - }) - } - - paged, total := h.Paginate(ctx, pluginArr) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Install -// -// @Summary 安装插件 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/install [post] -func (r *PluginController) Install(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - if err := r.plugin.Install(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, "任务已提交") -} - -// Uninstall -// -// @Summary 卸载插件 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/uninstall [post] -func (r *PluginController) Uninstall(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - if err := r.plugin.Uninstall(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, "任务已提交") -} - -// Update -// -// @Summary 更新插件 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/update [post] -func (r *PluginController) Update(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - if err := r.plugin.Update(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, "任务已提交") -} - -// UpdateShow -// -// @Summary 更新插件首页显示状态 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Param show query bool true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/updateShow [post] -func (r *PluginController) UpdateShow(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - show := ctx.Request().InputBool("show") - - var plugin models.Plugin - if err := facades.Orm().Query().Where("slug", slug).First(&plugin); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{ - "slug": slug, - "err": err.Error(), - }).Info("获取插件失败") - return h.ErrorSystem(ctx) - } - if plugin.ID == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "插件未安装") - } - - plugin.Show = show - if err := facades.Orm().Query().Save(&plugin); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{ - "slug": slug, - "err": err.Error(), - }).Info("更新插件失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, "操作成功") -} - -// IsInstalled -// -// @Summary 检查插件是否已安装 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/isInstalled [get] -func (r *PluginController) IsInstalled(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - plugin := r.plugin.GetInstalledBySlug(slug) - info := r.plugin.GetBySlug(slug) - if plugin.Slug != slug { - return h.Success(ctx, http.Json{ - "name": info.Name, - "installed": false, - }) - } - - return h.Success(ctx, http.Json{ - "name": info.Name, - "installed": true, - }) -} diff --git a/app/http/controllers/safe_controller.go b/app/http/controllers/safe_controller.go deleted file mode 100644 index 39e4211f..00000000 --- a/app/http/controllers/safe_controller.go +++ /dev/null @@ -1,377 +0,0 @@ -package controllers - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type SafeController struct { - ssh string -} - -func NewSafeController() *SafeController { - var ssh string - if os.IsRHEL() { - ssh = "sshd" - } else { - ssh = "ssh" - } - - return &SafeController{ - ssh: ssh, - } -} - -// GetFirewallStatus 获取防火墙状态 -func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response { - return h.Success(ctx, r.firewallStatus()) -} - -// SetFirewallStatus 设置防火墙状态 -func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response { - var err error - if ctx.Request().InputBool("status") { - if os.IsRHEL() { - err = systemctl.Start("firewalld") - if err == nil { - err = systemctl.Enable("firewalld") - } - } else { - _, err = shell.Execf("echo y | ufw enable") - if err == nil { - err = systemctl.Start("ufw") - } - if err == nil { - err = systemctl.Enable("ufw") - } - } - } else { - if os.IsRHEL() { - err = systemctl.Stop("firewalld") - if err == nil { - err = systemctl.Disable("firewalld") - } - } else { - _, err = shell.Execf("ufw disable") - if err == nil { - err = systemctl.Stop("ufw") - } - if err == nil { - err = systemctl.Disable("ufw") - } - } - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetFirewallRules 获取防火墙规则 -func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response { - if !r.firewallStatus() { - return h.Success(ctx, nil) - } - - var rules []map[string]string - if os.IsRHEL() { - out, err := shell.Execf("firewall-cmd --list-all") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out) - if len(match) == 0 { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []map[string]string{}, - }) - } - ports := strings.Split(match[1], " ") - for _, port := range ports { - rule := strings.Split(port, "/") - if len(rule) < 2 { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": "all", - }) - } else { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": rule[1], - }) - } - } - } else { - out, err := shell.Execf("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if len(out) == 0 { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []map[string]string{}, - }) - } - for _, port := range strings.Split(out, "\n") { - rule := strings.Split(port, "/") - if len(rule) < 2 { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": "all", - }) - } else { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": rule[1], - }) - } - } - } - - paged, total := h.Paginate(ctx, rules) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddFirewallRule 添加防火墙规则 -func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response { - if !r.firewallStatus() { - return h.Error(ctx, http.StatusInternalServerError, "防火墙未启动") - } - - port := ctx.Request().Input("port") - protocol := ctx.Request().Input("protocol") - if port == "" || protocol == "" || (protocol != "tcp" && protocol != "udp") { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - // 端口有 2 种写法,一种是 80-443,一种是 80 - if strings.Contains(port, "-") { - ports := strings.Split(port, "-") - startPort := cast.ToInt(ports[0]) - endPort := cast.ToInt(ports[1]) - if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 || startPort > endPort { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - } else { - port := cast.ToInt(port) - if port < 1 || port > 65535 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - } - - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --remove-port=%s/%s --permanent", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --add-port=%s/%s --permanent", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - // ufw 需要替换 - 为 : 添加 - if strings.Contains(port, "-") { - port = strings.ReplaceAll(port, "-", ":") - } - if out, err := shell.Execf("ufw delete allow %s/%s", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw allow %s/%s", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - return h.Success(ctx, nil) -} - -// DeleteFirewallRule 删除防火墙规则 -func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response { - if !r.firewallStatus() { - return h.Error(ctx, http.StatusUnprocessableEntity, "防火墙未启动") - } - - port := ctx.Request().Input("port") - protocol := ctx.Request().Input("protocol") - if port == "" || protocol == "" { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - if protocol == "all" { - protocol = "" - } else { - protocol = "/" + protocol - } - - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --remove-port=%s%s --permanent", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw delete allow %s%s", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - return h.Success(ctx, nil) -} - -// firewallStatus 获取防火墙状态 -func (r *SafeController) firewallStatus() bool { - var running bool - if os.IsRHEL() { - running, _ = systemctl.Status("firewalld") - } else { - running, _ = systemctl.Status("ufw") - } - - return running -} - -// GetSshStatus 获取 SSH 状态 -func (r *SafeController) GetSshStatus(ctx http.Context) http.Response { - running, err := systemctl.Status(r.ssh) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, running) -} - -// SetSshStatus 设置 SSH 状态 -func (r *SafeController) SetSshStatus(ctx http.Context) http.Response { - if ctx.Request().InputBool("status") { - if err := systemctl.Enable(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := systemctl.Start(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } else { - if err := systemctl.Stop(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := systemctl.Disable(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - return h.Success(ctx, nil) -} - -// GetSshPort 获取 SSH 端口 -func (r *SafeController) GetSshPort(ctx http.Context) http.Response { - out, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, cast.ToInt(out)) -} - -// SetSshPort 设置 SSH 端口 -func (r *SafeController) SetSshPort(ctx http.Context) http.Response { - port := ctx.Request().InputInt("port", 0) - if port == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - - oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, oldPort) - } - _, _ = shell.Execf("sed -i 's/#Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) - _, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) - - status, _ := systemctl.Status(r.ssh) - if status { - _ = systemctl.Restart(r.ssh) - } - - return h.Success(ctx, nil) -} - -// GetPingStatus 获取 Ping 状态 -func (r *SafeController) GetPingStatus(ctx http.Context) http.Response { - if os.IsRHEL() { - out, err := shell.Execf(`firewall-cmd --list-all`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if !strings.Contains(out, `rule protocol value="icmp" drop`) { - return h.Success(ctx, true) - } else { - return h.Success(ctx, false) - } - } else { - config, err := io.Read("/etc/ufw/before.rules") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") { - return h.Success(ctx, true) - } else { - return h.Success(ctx, false) - } - } -} - -// SetPingStatus 设置 Ping 状态 -func (r *SafeController) SetPingStatus(ctx http.Context) http.Response { - var out string - var err error - if os.IsRHEL() { - if ctx.Request().InputBool("status") { - out, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`) - } else { - out, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`) - } - } else { - if ctx.Request().InputBool("status") { - out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`) - } else { - out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`) - } - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if os.IsRHEL() { - out, err = shell.Execf(`firewall-cmd --reload`) - } else { - out, err = shell.Execf(`ufw reload`) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/setting_controller.go b/app/http/controllers/setting_controller.go deleted file mode 100644 index 4d9f5d54..00000000 --- a/app/http/controllers/setting_controller.go +++ /dev/null @@ -1,290 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" - "github.com/spf13/cast" - - requests "github.com/TheTNB/panel/v2/app/http/requests/setting" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/tools" -) - -type SettingController struct { - setting internal.Setting -} - -func NewSettingController() *SettingController { - return &SettingController{ - setting: services.NewSettingImpl(), - } -} - -// List -// -// @Summary 设置列表 -// @Tags 面板设置 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/list [get] -func (r *SettingController) List(ctx http.Context) http.Response { - var settings []models.Setting - err := facades.Orm().Query().Get(&settings) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板设置列表失败") - return h.ErrorSystem(ctx) - } - - userID := cast.ToUint(ctx.Value("user_id")) - var user models.User - if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取用户信息失败") - } - - port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板端口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "name": r.setting.Get(models.SettingKeyName), - "language": facades.Config().GetString("app.locale"), - "entrance": facades.Config().GetString("panel.entrance"), - "ssl": facades.Config().GetBool("panel.ssl"), - "website_path": r.setting.Get(models.SettingKeyWebsitePath), - "backup_path": r.setting.Get(models.SettingKeyBackupPath), - "username": user.Username, - "password": "", - "email": user.Email, - "port": port, - }) -} - -// Update -// -// @Summary 更新设置 -// @Tags 面板设置 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Update true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/update [post] -func (r *SettingController) Update(ctx http.Context) http.Response { - var updateRequest requests.Update - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.setting.Set(models.SettingKeyName, updateRequest.Name) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存面板名称失败") - return h.ErrorSystem(ctx) - } - - if !io.Exists(updateRequest.BackupPath) { - if err = io.Mkdir(updateRequest.BackupPath, 0644); err != nil { - return h.ErrorSystem(ctx) - } - } - err = r.setting.Set(models.SettingKeyBackupPath, updateRequest.BackupPath) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存备份目录失败") - return h.ErrorSystem(ctx) - } - if !io.Exists(updateRequest.WebsitePath) { - if err = io.Mkdir(updateRequest.WebsitePath, 0755); err != nil { - return h.ErrorSystem(ctx) - } - if err = io.Chown(updateRequest.WebsitePath, "www", "www"); err != nil { - return h.ErrorSystem(ctx) - } - } - err = r.setting.Set(models.SettingKeyWebsitePath, updateRequest.WebsitePath) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存建站目录失败") - return h.ErrorSystem(ctx) - } - - userID := cast.ToUint(ctx.Value("user_id")) - var user models.User - if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取用户信息失败") - } - - user.Username = updateRequest.UserName - user.Email = updateRequest.Email - if len(updateRequest.Password) > 0 { - hash, err := facades.Hash().Make(updateRequest.Password) - if err != nil { - return h.ErrorSystem(ctx) - } - user.Password = hash - } - if err = facades.Orm().Query().Save(&user); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存用户信息失败") - return h.ErrorSystem(ctx) - } - - oldPort, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板端口失败") - return h.ErrorSystem(ctx) - } - - port := cast.ToString(updateRequest.Port) - if oldPort != port { - if out, err := shell.Execf("sed -i 's/APP_PORT=%s/APP_PORT=%s/g' /www/panel/panel.conf", oldPort, port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --remove-port=%s/tcp --permanent", oldPort); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --add-port=%s/tcp --permanent", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw delete allow %s/tcp", oldPort); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - } - - oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板入口失败") - return h.ErrorSystem(ctx) - } - entrance := cast.ToString(updateRequest.Entrance) - if oldEntrance != entrance { - if out, err := shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=" + entrance + "!g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - oldLanguage, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_LOCALE | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板语言失败") - return h.ErrorSystem(ctx) - } - if oldLanguage != updateRequest.Language { - if out, err := shell.Execf("sed -i 's/APP_LOCALE=" + oldLanguage + "/APP_LOCALE=" + updateRequest.Language + "/g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language { - tools.RestartPanel() - } - - return h.Success(ctx, nil) -} - -// GetHttps -// -// @Summary 获取面板 HTTPS 设置 -// @Tags 面板设置 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/https [get] -func (r *SettingController) GetHttps(ctx http.Context) http.Response { - certPath := facades.Config().GetString("http.tls.ssl.cert") - keyPath := facades.Config().GetString("http.tls.ssl.key") - crt, err := io.Read(certPath) - if err != nil { - return h.ErrorSystem(ctx) - } - key, err := io.Read(keyPath) - if err != nil { - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "https": facades.Config().GetBool("panel.ssl"), - "cert": crt, - "key": key, - }) -} - -// UpdateHttps -// -// @Summary 更新面板 HTTPS 设置 -// @Tags 面板设置 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Https true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/https [post] -func (r *SettingController) UpdateHttps(ctx http.Context) http.Response { - var httpsRequest requests.Https - sanitize := h.SanitizeRequest(ctx, &httpsRequest) - if sanitize != nil { - return sanitize - } - - if httpsRequest.Https { - if _, err := cert.ParseCert(httpsRequest.Cert); err != nil { - return h.Error(ctx, http.StatusBadRequest, "证书格式错误") - } - if _, err := cert.ParseKey(httpsRequest.Key); err != nil { - return h.Error(ctx, http.StatusBadRequest, "密钥格式错误") - } - if err := io.Write(path.Executable("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil { - return h.ErrorSystem(ctx) - } - if err := io.Write(path.Executable("storage/ssl.key"), httpsRequest.Key, 0700); err != nil { - return h.ErrorSystem(ctx) - } - if out, err := shell.Execf("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - tools.RestartPanel() - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/ssh_controller.go b/app/http/controllers/ssh_controller.go deleted file mode 100644 index 06122147..00000000 --- a/app/http/controllers/ssh_controller.go +++ /dev/null @@ -1,157 +0,0 @@ -package controllers - -import ( - "bytes" - "context" - nethttp "net/http" - "sync" - "time" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/gorilla/websocket" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/ssh" -) - -type SshController struct { - AuthMethod ssh.AuthMethod - setting internal.Setting -} - -func NewSshController() *SshController { - return &SshController{ - AuthMethod: ssh.PASSWORD, - setting: services.NewSettingImpl(), - } -} - -// GetInfo 获取 SSH 配置 -func (r *SshController) GetInfo(ctx http.Context) http.Response { - host := r.setting.Get(models.SettingKeySshHost) - port := r.setting.Get(models.SettingKeySshPort) - user := r.setting.Get(models.SettingKeySshUser) - password := r.setting.Get(models.SettingKeySshPassword) - if len(host) == 0 || len(user) == 0 || len(password) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "SSH 配置不完整") - } - - return h.Success(ctx, http.Json{ - "host": host, - "port": cast.ToInt(port), - "user": user, - "password": password, - }) -} - -// UpdateInfo 更新 SSH 配置 -func (r *SshController) UpdateInfo(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "host": "required", - "port": "required", - "user": "required", - "password": "required", - }); sanitize != nil { - return sanitize - } - - host := ctx.Request().Input("host") - port := ctx.Request().Input("port") - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - if err := r.setting.Set(models.SettingKeySshHost, host); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := r.setting.Set(models.SettingKeySshPort, port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := r.setting.Set(models.SettingKeySshUser, user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := r.setting.Set(models.SettingKeySshPassword, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Session SSH 会话 -func (r *SshController) Session(ctx http.Context) http.Response { - upGrader := websocket.Upgrader{ - ReadBufferSize: 4096, - WriteBufferSize: 4096, - CheckOrigin: func(r *nethttp.Request) bool { - return true - }, - Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")}, - } - - ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil) - if err != nil { - facades.Log().Tags("面板", "SSH").With(map[string]any{ - "error": err.Error(), - }).Infof("建立连接失败") - return h.ErrorSystem(ctx) - } - defer ws.Close() - - config := ssh.ClientConfigPassword( - r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort), - r.setting.Get(models.SettingKeySshUser), - r.setting.Get(models.SettingKeySshPassword), - ) - client, err := ssh.NewSSHClient(config) - - if err != nil { - _ = ws.WriteControl(websocket.CloseMessage, - []byte(err.Error()), time.Now().Add(time.Second)) - return h.ErrorSystem(ctx) - } - defer client.Close() - - turn, err := ssh.NewTurn(ws, client) - if err != nil { - _ = ws.WriteControl(websocket.CloseMessage, - []byte(err.Error()), time.Now().Add(time.Second)) - return h.ErrorSystem(ctx) - } - defer turn.Close() - - var bufPool = sync.Pool{ - New: func() any { - return new(bytes.Buffer) - }, - } - var logBuff = bufPool.Get().(*bytes.Buffer) - logBuff.Reset() - defer bufPool.Put(logBuff) - - sshCtx, cancel := context.WithCancel(context.Background()) - wg := sync.WaitGroup{} - wg.Add(2) - go func() { - defer wg.Done() - if err = turn.LoopRead(logBuff, sshCtx); err != nil { - facades.Log().Tags("面板", "SSH").With(map[string]any{ - "error": err.Error(), - }).Infof("读取数据失败") - } - }() - go func() { - defer wg.Done() - if err = turn.SessionWait(); err != nil { - facades.Log().Tags("面板", "SSH").With(map[string]any{ - "error": err.Error(), - }).Infof("会话错误") - } - cancel() - }() - wg.Wait() - - return nil -} diff --git a/app/http/controllers/swagger_controller.go b/app/http/controllers/swagger_controller.go deleted file mode 100644 index 917ace38..00000000 --- a/app/http/controllers/swagger_controller.go +++ /dev/null @@ -1,31 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/swaggo/http-swagger/v2" - - _ "github.com/TheTNB/panel/v2/docs" -) - -type SwaggerController struct { - // Dependent services -} - -func NewSwaggerController() *SwaggerController { - return &SwaggerController{} -} - -// Index -// -// @Summary Swagger UI -// @Description Swagger UI -// @Tags Swagger -// @Success 200 -// @Failure 500 -// @Router /swagger [get] -func (r *SwaggerController) Index(ctx http.Context) http.Response { - handler := httpSwagger.Handler() - handler(ctx.Response().Writer(), ctx.Request().Origin()) - - return nil -} diff --git a/app/http/controllers/system_controller.go b/app/http/controllers/system_controller.go deleted file mode 100644 index 61dbe95c..00000000 --- a/app/http/controllers/system_controller.go +++ /dev/null @@ -1,211 +0,0 @@ -package controllers - -import ( - "fmt" - - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type SystemController struct { -} - -func NewSystemController() *SystemController { - return &SystemController{} -} - -// ServiceStatus -// -// @Summary 服务状态 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/status [get] -func (r *SystemController) ServiceStatus(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Query("service") - status, err := systemctl.Status(service) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", service)) - } - - return h.Success(ctx, status) -} - -// ServiceIsEnabled -// -// @Summary 是否启用服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/isEnabled [get] -func (r *SystemController) ServiceIsEnabled(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Query("service") - enabled, err := systemctl.IsEnabled(service) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", service)) - } - - return h.Success(ctx, enabled) -} - -// ServiceEnable -// -// @Summary 启用服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/enable [post] -func (r *SystemController) ServiceEnable(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Enable(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceDisable -// -// @Summary 禁用服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/disable [post] -func (r *SystemController) ServiceDisable(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Disable(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceRestart -// -// @Summary 重启服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/restart [post] -func (r *SystemController) ServiceRestart(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Restart(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceReload -// -// @Summary 重载服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/reload [post] -func (r *SystemController) ServiceReload(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Reload(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceStart -// -// @Summary 启动服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/start [post] -func (r *SystemController) ServiceStart(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Start(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceStop -// -// @Summary 停止服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/stop [post] -func (r *SystemController) ServiceStop(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Stop(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/task_controller.go b/app/http/controllers/task_controller.go deleted file mode 100644 index 6974f837..00000000 --- a/app/http/controllers/task_controller.go +++ /dev/null @@ -1,88 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/shell" -) - -type TaskController struct { - // Dependent services -} - -func NewTaskController() *TaskController { - return &TaskController{ - // Inject services - } -} - -// Status 获取当前任务状态 -func (r *TaskController) Status(ctx http.Context) http.Response { - var task models.Task - err := facades.Orm().Query().Where("status", models.TaskStatusWaiting).OrWhere("status", models.TaskStatusRunning).FirstOrFail(&task) - if err == nil { - return h.Success(ctx, http.Json{ - "task": true, - }) - } - - return h.Success(ctx, http.Json{ - "task": false, - }) -} - -// List 获取任务列表 -func (r *TaskController) List(ctx http.Context) http.Response { - var tasks []models.Task - var total int64 - err := facades.Orm().Query().Order("id desc").Paginate(ctx.Request().QueryInt("page", 1), ctx.Request().QueryInt("limit", 10), &tasks, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{ - "error": err.Error(), - }).Info("查询任务列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": tasks, - }) -} - -// Log 获取任务日志 -func (r *TaskController) Log(ctx http.Context) http.Response { - var task models.Task - err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{ - "id": ctx.Request().QueryInt("id"), - "error": err.Error(), - }).Info("查询任务失败") - return h.ErrorSystem(ctx) - } - - log, err := shell.Execf(`tail -n 500 '` + task.Log + `'`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "日志已被清理") - } - - return h.Success(ctx, log) -} - -// Delete 删除任务 -func (r *TaskController) Delete(ctx http.Context) http.Response { - var task models.Task - _, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{ - "id": ctx.Request().QueryInt("id"), - "error": err.Error(), - }).Info("删除任务失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/user_controller.go b/app/http/controllers/user_controller.go deleted file mode 100644 index d9a6830a..00000000 --- a/app/http/controllers/user_controller.go +++ /dev/null @@ -1,123 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/http/requests/user" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type UserController struct { - // Dependent services -} - -func NewUserController() *UserController { - return &UserController{ - // Inject services - } -} - -// Login -// -// @Summary 登录 -// @Tags 用户鉴权 -// @Accept json -// @Produce json -// @Param data body requests.Login true "request" -// @Success 200 {object} SuccessResponse -// @Failure 403 {object} ErrorResponse "用户名或密码错误" -// @Failure 500 {object} ErrorResponse "系统内部错误 -// @Router /panel/user/login [post] -func (r *UserController) Login(ctx http.Context) http.Response { - var loginRequest requests.Login - sanitize := h.SanitizeRequest(ctx, &loginRequest) - if sanitize != nil { - return sanitize - } - - var user models.User - err := facades.Orm().Query().Where("username", loginRequest.Username).First(&user) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{ - "error": err.Error(), - }).Info("查询用户失败") - return h.ErrorSystem(ctx) - } - - if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) { - return h.Error(ctx, http.StatusForbidden, "用户名或密码错误") - } - - if facades.Hash().NeedsRehash(user.Password) { - user.Password, err = facades.Hash().Make(loginRequest.Password) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{ - "error": err.Error(), - }).Info("更新密码失败") - return h.ErrorSystem(ctx) - } - } - - ctx.Request().Session().Put("user_id", user.ID) - return h.Success(ctx, nil) -} - -// Logout -// -// @Summary 登出 -// @Tags 用户鉴权 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/user/logout [post] -func (r *UserController) Logout(ctx http.Context) http.Response { - if ctx.Request().HasSession() { - ctx.Request().Session().Forget("user_id") - } - - return h.Success(ctx, nil) -} - -// IsLogin -// -// @Summary 是否登录 -// @Tags 用户鉴权 -// @Produce json -// @Success 200 {object} SuccessResponse -// @Router /panel/user/isLogin [get] -func (r *UserController) IsLogin(ctx http.Context) http.Response { - if !ctx.Request().HasSession() { - return h.Success(ctx, false) - } - - return h.Success(ctx, ctx.Request().Session().Has("user_id")) -} - -// Info -// -// @Summary 用户信息 -// @Tags 用户鉴权 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/user/info [get] -func (r *UserController) Info(ctx http.Context) http.Response { - userID := cast.ToUint(ctx.Value("user_id")) - var user models.User - if err := facades.Orm().Query().Where("id", userID).Get(&user); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{ - "error": err.Error(), - }).Info("获取用户信息失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "id": user.ID, - "role": []string{"admin"}, - "username": user.Username, - "email": user.Email, - }) -} diff --git a/app/http/controllers/website_controller.go b/app/http/controllers/website_controller.go deleted file mode 100644 index 6bfb8e45..00000000 --- a/app/http/controllers/website_controller.go +++ /dev/null @@ -1,646 +0,0 @@ -package controllers - -import ( - "fmt" - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - commonrequests "github.com/TheTNB/panel/v2/app/http/requests/common" - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type WebsiteController struct { - website internal.Website - setting internal.Setting - backup internal.Backup -} - -func NewWebsiteController() *WebsiteController { - return &WebsiteController{ - website: services.NewWebsiteImpl(), - setting: services.NewSettingImpl(), - backup: services.NewBackupImpl(), - } -} - -// List -// -// @Summary 获取网站列表 -// @Tags 网站 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites [get] -func (r *WebsiteController) List(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - total, websites, err := r.website.List(paginateRequest.Page, paginateRequest.Limit) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取网站列表失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": websites, - }) -} - -// Add -// -// @Summary 添加网站 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Add true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites [post] -func (r *WebsiteController) Add(ctx http.Context) http.Response { - var addRequest requests.Add - sanitize := h.SanitizeRequest(ctx, &addRequest) - if sanitize != nil { - return sanitize - } - - if len(addRequest.Path) == 0 { - addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name - } - - _, err := r.website.Add(addRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加网站失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Delete -// -// @Summary 删除网站 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Delete true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/delete [post] -func (r *WebsiteController) Delete(ctx http.Context) http.Response { - var deleteRequest requests.Delete - sanitize := h.SanitizeRequest(ctx, &deleteRequest) - if sanitize != nil { - return sanitize - } - - if err := r.website.Delete(deleteRequest); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": deleteRequest.ID, - "error": err.Error(), - }).Info("删除网站失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetDefaultConfig -// -// @Summary 获取默认配置 -// @Tags 网站 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse{data=map[string]string} -// @Router /panel/website/defaultConfig [get] -func (r *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response { - index, err := io.Read("/www/server/openresty/html/index.html") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - stop, err := io.Read("/www/server/openresty/html/stop.html") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "index": index, - "stop": stop, - }) -} - -// SaveDefaultConfig -// -// @Summary 保存默认配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body map[string]string true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/website/defaultConfig [post] -func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response { - index := ctx.Request().Input("index") - stop := ctx.Request().Input("stop") - - if err := io.Write("/www/server/openresty/html/index.html", index, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("保存默认首页配置失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := io.Write("/www/server/openresty/html/stop.html", stop, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("保存默认停止页配置失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetConfig -// -// @Summary 获取网站配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd} -// @Router /panel/websites/{id}/config [get] -func (r *WebsiteController) GetConfig(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - config, err := r.website.GetConfig(idRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("获取网站配置失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveConfig -// -// @Summary 保存网站配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Param data body requests.SaveConfig true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/config [post] -func (r *WebsiteController) SaveConfig(ctx http.Context) http.Response { - var saveConfigRequest requests.SaveConfig - sanitize := h.SanitizeRequest(ctx, &saveConfigRequest) - if sanitize != nil { - return sanitize - } - - err := r.website.SaveConfig(saveConfigRequest) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ClearLog -// -// @Summary 清空网站日志 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/log [delete] -func (r *WebsiteController) ClearLog(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website) - if err != nil { - return h.ErrorSystem(ctx) - } - - if err := io.Remove("/www/wwwlogs/" + website.Name + ".log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UpdateRemark -// -// @Summary 更新网站备注 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/updateRemark [post] -func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website) - if err != nil { - return h.ErrorSystem(ctx) - } - - website.Remark = ctx.Request().Input("remark") - if err = facades.Orm().Query().Save(&website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("更新网站备注失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// BackupList -// -// @Summary 获取网站备份列表 -// @Tags 网站 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse{data=[]types.BackupFile} -// @Router /panel/website/backupList [get] -func (r *WebsiteController) BackupList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - backups, err := r.backup.WebsiteList() - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取备份列表失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, backups) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// CreateBackup -// -// @Summary 创建网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/createBackup [post] -func (r *WebsiteController) CreateBackup(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("获取网站信息失败") - return h.ErrorSystem(ctx) - } - - if err := r.backup.WebSiteBackup(website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("备份网站失败") - return h.Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error()) - } - - return h.Success(ctx, nil) -} - -// UploadBackup -// -// @Summary 上传网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param file formData file true "备份文件" -// @Success 200 {object} SuccessResponse -// @Router /panel/website/uploadBackup [put] -func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response { - file, err := ctx.Request().File("file") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "上传文件失败") - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website" - if !io.Exists(backupPath) { - if err = io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - name := file.GetClientOriginalName() - _, err = file.StoreAs(backupPath, name) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("上传备份失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// RestoreBackup -// -// @Summary 还原网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/restoreBackup [post] -func (r *WebsiteController) RestoreBackup(ctx http.Context) http.Response { - var restoreBackupRequest requests.RestoreBackup - sanitize := h.SanitizeRequest(ctx, &restoreBackupRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", restoreBackupRequest.ID).Get(&website); err != nil { - return h.ErrorSystem(ctx) - } - - if err := r.backup.WebsiteRestore(website, restoreBackupRequest.Name); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": restoreBackupRequest.ID, - "file": restoreBackupRequest.Name, - "error": err.Error(), - }).Info("还原网站失败") - return h.Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteBackup -// -// @Summary 删除网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.DeleteBackup true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/website/deleteBackup [delete] -func (r *WebsiteController) DeleteBackup(ctx http.Context) http.Response { - var deleteBackupRequest requests.DeleteBackup - sanitize := h.SanitizeRequest(ctx, &deleteBackupRequest) - if sanitize != nil { - return sanitize - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - if err := io.Remove(backupPath + "/" + deleteBackupRequest.Name); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ResetConfig -// -// @Summary 重置网站配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/resetConfig [post] -func (r *WebsiteController) ResetConfig(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil { - return h.ErrorSystem(ctx) - } - - website.Status = true - website.SSL = false - if err := facades.Orm().Query().Save(&website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("保存网站配置失败") - return h.ErrorSystem(ctx) - } - - raw := fmt.Sprintf(` -# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 - listen 80; - # port标记位结束 - # server_name标记位开始 - server_name localhost; - # server_name标记位结束 - # index标记位开始 - index index.php index.html; - # index标记位结束 - # root标记位开始 - root %s; - # root标记位结束 - - # ssl标记位开始 - # ssl标记位结束 - - # php标记位开始 - include enable-php-%d.conf; - # php标记位结束 - - # waf标记位开始 - waf off; - waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; - waf_mode DYNAMIC; - waf_cc_deny rate=1000r/m duration=60m; - waf_cache capacity=50; - # waf标记位结束 - - # 错误页配置,可自行设置 - error_page 404 /404.html; - #error_page 502 /502.html; - - # acme证书签发配置,不可修改 - include /www/server/vhost/acme/%s.conf; - - # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 - include /www/server/vhost/rewrite/%s.conf; - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log /www/wwwlogs/%s.log; - error_log /www/wwwlogs/%s.log; -} - -`, website.Path, website.PHP, website.Name, website.Name, website.Name, website.Name) - if err := io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil { - return nil - } - if err := io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil { - return nil - } - if err := io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil { - return nil - } - if err := systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -// Status -// -// @Summary 获取网站状态 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/status [post] -func (r *WebsiteController) Status(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil { - return h.ErrorSystem(ctx) - } - - website.Status = ctx.Request().InputBool("status") - if err := facades.Orm().Query().Save(&website); err != nil { - return h.ErrorSystem(ctx) - } - - raw, err := io.Read("/www/server/vhost/" + website.Name + ".conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - // 运行目录 - rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束") - match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig) - if len(match) == 2 { - if website.Status { - root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig) - raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ") - } else { - raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ") - } - } - - // 默认文件 - indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束") - match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig) - if len(match) == 2 { - if website.Status { - index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig) - raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ") - } else { - raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ") - } - } - - if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/kernel.go b/app/http/kernel.go deleted file mode 100644 index 8942e963..00000000 --- a/app/http/kernel.go +++ /dev/null @@ -1,23 +0,0 @@ -package http - -import ( - "github.com/goravel/framework/contracts/http" - sessionmiddleware "github.com/goravel/framework/session/middleware" - - "github.com/TheTNB/panel/v2/app/http/middleware" -) - -type Kernel struct { -} - -// The application's global HTTP middleware stack. -// These middleware are run during every request to your application. -func (kernel Kernel) Middleware() []http.Middleware { - return []http.Middleware{ - sessionmiddleware.StartSession(), - middleware.Log(), - middleware.Status(), - middleware.Entrance(), - middleware.Static(), - } -} diff --git a/app/http/middleware/entrance.go b/app/http/middleware/entrance.go deleted file mode 100644 index 67188ea5..00000000 --- a/app/http/middleware/entrance.go +++ /dev/null @@ -1,39 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -func Entrance() http.Middleware { - return func(ctx http.Context) { - translate := facades.Lang(ctx) - - if !ctx.Request().HasSession() { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.missing"), - }) - return - } - - entrance := facades.Config().GetString("panel.entrance") - if ctx.Request().Path() == entrance { - ctx.Request().Session().Put("verify_entrance", true) - _ = ctx.Response().Redirect(http.StatusFound, "/login").Render() - ctx.Request().AbortWithStatus(http.StatusFound) - return - } - - if !facades.Config().GetBool("app.debug") && - (ctx.Request().Session().Missing("verify_entrance") || !cast.ToBool(ctx.Request().Session().Get("verify_entrance"))) && - ctx.Request().Path() != "/robots.txt" { - ctx.Request().AbortWithStatusJson(http.StatusTeapot, http.Json{ - "message": "请通过正确的入口访问", - }) - return - } - - ctx.Request().Next() - } -} diff --git a/app/http/middleware/log.go b/app/http/middleware/log.go deleted file mode 100644 index 4dbc829d..00000000 --- a/app/http/middleware/log.go +++ /dev/null @@ -1,20 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" -) - -// Log 记录请求日志 -func Log() http.Middleware { - return func(ctx http.Context) { - facades.Log().Channel("http").With(map[string]any{ - "Method": ctx.Request().Method(), - "URL": ctx.Request().FullUrl(), - "IP": ctx.Request().Ip(), - "UA": ctx.Request().Header("User-Agent"), - "Body": ctx.Request().All(), - }).Info("HTTP Request") - ctx.Request().Next() - } -} diff --git a/app/http/middleware/must_install.go b/app/http/middleware/must_install.go deleted file mode 100644 index 6b4c5664..00000000 --- a/app/http/middleware/must_install.go +++ /dev/null @@ -1,93 +0,0 @@ -package middleware - -import ( - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/translation" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/internal/services" -) - -// MustInstall 确保已安装插件 -func MustInstall() http.Middleware { - return func(ctx http.Context) { - path := ctx.Request().Path() - translate := facades.Lang(ctx) - var slug string - if strings.HasPrefix(path, "/api/panel/website") { - slug = "openresty" - } else if strings.HasPrefix(path, "/api/panel/container") { - slug = "podman" - } else { - pathArr := strings.Split(path, "/") - if len(pathArr) < 4 { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.notExist"), - }) - return - } - slug = pathArr[3] - } - - plugin := services.NewPluginImpl().GetBySlug(slug) - installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug) - installedPlugins, err := services.NewPluginImpl().AllInstalled() - if err != nil { - ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{ - "message": translate.Get("errors.internal"), - }) - return - } - - if installedPlugin.Slug != plugin.Slug { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.notInstalled", translation.Option{ - Replace: map[string]string{ - "slug": slug, - }, - }), - }) - return - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.dependent", translation.Option{ - Replace: map[string]string{ - "slug": slug, - "dependency": require, - }, - }), - }) - return - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.incompatible", translation.Option{ - Replace: map[string]string{ - "slug": slug, - "exclude": exclude, - }, - }), - }) - return - } - } - - ctx.Request().Next() - } -} diff --git a/app/http/middleware/session.go b/app/http/middleware/session.go deleted file mode 100644 index c0a775df..00000000 --- a/app/http/middleware/session.go +++ /dev/null @@ -1,53 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -// Session 确保通过 JWT 鉴权 -func Session() http.Middleware { - return func(ctx http.Context) { - translate := facades.Lang(ctx) - - if !ctx.Request().HasSession() { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.missing"), - }) - return - } - - if ctx.Request().Session().Missing("user_id") { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.expired"), - }) - return - } - - userID := cast.ToUint(ctx.Request().Session().Get("user_id")) - if userID == 0 { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.invalid"), - }) - return - } - - // 刷新会话 - /*if err := ctx.Request().Session().Regenerate(); err == nil { - ctx.Response().Cookie(http.Cookie{ - Name: ctx.Request().Session().GetName(), - Value: ctx.Request().Session().GetID(), - MaxAge: facades.Config().GetInt("session.lifetime") * 60, - Path: facades.Config().GetString("session.path"), - Domain: facades.Config().GetString("session.domain"), - Secure: facades.Config().GetBool("session.secure"), - HttpOnly: facades.Config().GetBool("session.http_only"), - SameSite: facades.Config().GetString("session.same_site"), - }) - }*/ - - ctx.WithValue("user_id", userID) - ctx.Request().Next() - } -} diff --git a/app/http/middleware/static.go b/app/http/middleware/static.go deleted file mode 100644 index 2a92cfd8..00000000 --- a/app/http/middleware/static.go +++ /dev/null @@ -1,16 +0,0 @@ -package middleware - -import ( - "github.com/gin-contrib/static" - "github.com/goravel/framework/contracts/http" - "github.com/goravel/gin" - - "github.com/TheTNB/panel/v2/embed" -) - -func Static() http.Middleware { - return func(ctx http.Context) { - static.Serve("/", static.EmbedFolder(embed.PublicFS, "frontend"))(ctx.(*gin.Context).Instance()) - ctx.Request().Next() - } -} diff --git a/app/http/middleware/status.go b/app/http/middleware/status.go deleted file mode 100644 index 9f128fc7..00000000 --- a/app/http/middleware/status.go +++ /dev/null @@ -1,40 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -// Status 检查程序状态 -func Status() http.Middleware { - return func(ctx http.Context) { - translate := facades.Lang(ctx) - switch types.Status { - case types.StatusUpgrade: - ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{ - "message": translate.Get("status.upgrade"), - }) - return - case types.StatusMaintain: - ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{ - "message": translate.Get("status.maintain"), - }) - return - case types.StatusClosed: - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("status.closed"), - }) - return - case types.StatusFailed: - ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{ - "message": translate.Get("status.failed"), - }) - return - default: - ctx.Request().Next() - return - } - } -} diff --git a/app/http/requests/cert/cert_deploy.go b/app/http/requests/cert/cert_deploy.go deleted file mode 100644 index 7be07f34..00000000 --- a/app/http/requests/cert/cert_deploy.go +++ /dev/null @@ -1,41 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertDeploy struct { - ID uint `form:"id" json:"id"` - WebsiteID uint `form:"website_id" json:"website_id"` -} - -func (r *CertDeploy) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertDeploy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:certs,id", - "website_id": "required|uint|min:1|exists:websites,id", - } -} - -func (r *CertDeploy) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - "website_id": "uint", - } -} - -func (r *CertDeploy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertDeploy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertDeploy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/cert_show_and_destroy.go b/app/http/requests/cert/cert_show_and_destroy.go deleted file mode 100644 index 597606e7..00000000 --- a/app/http/requests/cert/cert_show_and_destroy.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertShowAndDestroy struct { - ID uint `form:"id" json:"id"` -} - -func (r *CertShowAndDestroy) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertShowAndDestroy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:certs,id", - } -} - -func (r *CertShowAndDestroy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertShowAndDestroy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertShowAndDestroy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/cert_store.go b/app/http/requests/cert/cert_store.go deleted file mode 100644 index 60539558..00000000 --- a/app/http/requests/cert/cert_store.go +++ /dev/null @@ -1,50 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertStore struct { - Type string `form:"type" json:"type"` - Domains []string `form:"domains" json:"domains"` - AutoRenew bool `form:"auto_renew" json:"auto_renew"` - UserID uint `form:"user_id" json:"user_id"` - DNSID uint `form:"dns_id" json:"dns_id"` - WebsiteID uint `form:"website_id" json:"website_id"` -} - -func (r *CertStore) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertStore) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "type": "required|in:P256,P384,2048,4096", - "domains": "required|slice", - "auto_renew": "required|bool", - "user_id": "required|uint|exists:cert_users,id", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertStore) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "user_id": "uint", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertStore) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertStore) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertStore) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/cert_update.go b/app/http/requests/cert/cert_update.go deleted file mode 100644 index f88dfb46..00000000 --- a/app/http/requests/cert/cert_update.go +++ /dev/null @@ -1,52 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertUpdate struct { - ID uint `form:"id" json:"id"` - Type string `form:"type" json:"type"` - Domains []string `form:"domains" json:"domains"` - AutoRenew bool `form:"auto_renew" json:"auto_renew"` - UserID uint `form:"user_id" json:"user_id"` - DNSID uint `form:"dns_id" json:"dns_id"` - WebsiteID uint `form:"website_id" json:"website_id"` -} - -func (r *CertUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:certs,id", - "type": "required|in:P256,P384,2048,4096", - "domains": "required|slice", - "auto_renew": "required|bool", - "user_id": "required|uint|exists:cert_users,id", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "user_id": "uint", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/dns_show_and_destroy.go b/app/http/requests/cert/dns_show_and_destroy.go deleted file mode 100644 index e709295b..00000000 --- a/app/http/requests/cert/dns_show_and_destroy.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type DNSShowAndDestroy struct { - ID uint `form:"id" json:"id"` -} - -func (r *DNSShowAndDestroy) Authorize(ctx http.Context) error { - return nil -} - -func (r *DNSShowAndDestroy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_dns,id", - } -} - -func (r *DNSShowAndDestroy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSShowAndDestroy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSShowAndDestroy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/dns_store.go b/app/http/requests/cert/dns_store.go deleted file mode 100644 index 6b2d0504..00000000 --- a/app/http/requests/cert/dns_store.go +++ /dev/null @@ -1,47 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type DNSStore struct { - Type string `form:"type" json:"type"` - Name string `form:"name" json:"name"` - Data acme.DNSParam `form:"data" json:"data"` -} - -func (r *DNSStore) Authorize(ctx http.Context) error { - return nil -} - -func (r *DNSStore) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "type": "required|in:dnspod,tencent,aliyun,cloudflare", - "name": "required", - "data": "required", - "data.id": "required_if:type,dnspod", - "data.token": "required_if:type,dnspod", - "data.access_key": "required_if:type,aliyun,tencent", - "data.secret_key": "required_if:type,aliyun,tencent", - "data.api_key": "required_if:type,cloudflare", - } -} - -func (r *DNSStore) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSStore) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSStore) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSStore) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/dns_update.go b/app/http/requests/cert/dns_update.go deleted file mode 100644 index 06b5e446..00000000 --- a/app/http/requests/cert/dns_update.go +++ /dev/null @@ -1,50 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type DNSUpdate struct { - ID uint `form:"id" json:"id"` - Type string `form:"type" json:"type"` - Name string `form:"name" json:"name"` - Data acme.DNSParam `form:"data" json:"data"` -} - -func (r *DNSUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *DNSUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_dns,id", - "type": "required|in:dnspod,aliyun,cloudflare", - "name": "required", - "data": "required", - "data.id": "required_if:type,dnspod", - "data.token": "required_if:type,dnspod", - "data.access_key": "required_if:type,aliyun", - "data.secret_key": "required_if:type,aliyun", - "data.email": "required_if:type,cloudflare", - "data.api_key": "required_if:type,cloudflare", - } -} - -func (r *DNSUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/obtain.go b/app/http/requests/cert/obtain.go deleted file mode 100644 index 53f82889..00000000 --- a/app/http/requests/cert/obtain.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Obtain struct { - ID uint `form:"id" json:"id"` -} - -func (r *Obtain) Authorize(ctx http.Context) error { - return nil -} - -func (r *Obtain) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:certs,id", - } -} - -func (r *Obtain) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Obtain) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Obtain) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Obtain) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/renew.go b/app/http/requests/cert/renew.go deleted file mode 100644 index 39be78cd..00000000 --- a/app/http/requests/cert/renew.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Renew struct { - ID uint `form:"id" json:"id"` -} - -func (r *Renew) Authorize(ctx http.Context) error { - return nil -} - -func (r *Renew) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:certs,id", - } -} - -func (r *Renew) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Renew) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Renew) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Renew) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/user_show_and_destroy.go b/app/http/requests/cert/user_show_and_destroy.go deleted file mode 100644 index ad8cf7e9..00000000 --- a/app/http/requests/cert/user_show_and_destroy.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UserShowAndDestroy struct { - ID uint `form:"id" json:"id"` -} - -func (r *UserShowAndDestroy) Authorize(ctx http.Context) error { - return nil -} - -func (r *UserShowAndDestroy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_users,id", - } -} - -func (r *UserShowAndDestroy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserShowAndDestroy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserShowAndDestroy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/user_store.go b/app/http/requests/cert/user_store.go deleted file mode 100644 index ad14f1e8..00000000 --- a/app/http/requests/cert/user_store.go +++ /dev/null @@ -1,44 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UserStore struct { - CA string `form:"ca" json:"ca"` - Email string `form:"email" json:"email"` - Kid string `form:"kid" json:"kid"` - HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` - KeyType string `form:"key_type" json:"key_type"` -} - -func (r *UserStore) Authorize(ctx http.Context) error { - return nil -} - -func (r *UserStore) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass", - "email": "required|email", - "kid": "required_if:ca,sslcom,google", - "hmac_encoded": "required_if:ca,sslcom,google", - "key_type": "required|in:P256,P384,2048,4096", - } -} - -func (r *UserStore) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserStore) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserStore) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserStore) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/user_update.go b/app/http/requests/cert/user_update.go deleted file mode 100644 index ccdb3d67..00000000 --- a/app/http/requests/cert/user_update.go +++ /dev/null @@ -1,46 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UserUpdate struct { - ID uint `form:"id" json:"id"` - CA string `form:"ca" json:"ca"` - Email string `form:"email" json:"email"` - Kid string `form:"kid" json:"kid"` - HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` - KeyType string `form:"key_type" json:"key_type"` -} - -func (r *UserUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *UserUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_users,id", - "ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass", - "email": "required|email", - "kid": "required_if:ca,sslcom,google", - "hmac_encoded": "required_if:ca,sslcom,google", - "key_type": "required|in:P256,P384,2048,4096", - } -} - -func (r *UserUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/common/paginate.go b/app/http/requests/common/paginate.go deleted file mode 100644 index 99f0373e..00000000 --- a/app/http/requests/common/paginate.go +++ /dev/null @@ -1,41 +0,0 @@ -package commonrequests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Paginate struct { - Page int `form:"page" json:"page"` - Limit int `form:"limit" json:"limit"` -} - -func (r *Paginate) Authorize(ctx http.Context) error { - return nil -} - -func (r *Paginate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "page": "required|int|min:1", - "limit": "required|int|min:1", - } -} - -func (r *Paginate) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "page": "int", - "limit": "int", - } -} - -func (r *Paginate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Paginate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Paginate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/container_create.go b/app/http/requests/container/container_create.go deleted file mode 100644 index 6fb2caa9..00000000 --- a/app/http/requests/container/container_create.go +++ /dev/null @@ -1,85 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type ContainerCreate struct { - Name string `form:"name" json:"name"` - Image string `form:"image" json:"image"` - Ports []types.ContainerPort `form:"ports" json:"ports"` - Network string `form:"network" json:"network"` - Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` - Labels []types.KV `form:"labels" json:"labels"` - Env []types.KV `form:"env" json:"env"` - Entrypoint []string `form:"entrypoint" json:"entrypoint"` - Command []string `form:"command" json:"command"` - RestartPolicy string `form:"restart_policy" json:"restart_policy"` - AutoRemove bool `form:"auto_remove" json:"auto_remove"` - Privileged bool `form:"privileged" json:"privileged"` - OpenStdin bool `form:"openStdin" json:"open_stdin"` - PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` - Tty bool `form:"tty" json:"tty"` - CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` - CPUs int64 `form:"cpus" json:"cpus"` - Memory int64 `form:"memory" json:"memory"` -} - -func (r *ContainerCreate) Authorize(ctx http.Context) error { - return nil -} - -func (r *ContainerCreate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "image": "required|string", - "ports": "slice", - /*"ports.*.host": "string", - "ports.*.host_start": "int", - "ports.*.host_end": "int", - "ports.*.container_start": "int", - "ports.*.container_end": "int", - "ports.*.protocol": "string|in:tcp,udp",*/ - "network": "string", - "volumes": "slice", - /*"volumes.*.host": "string", - "volumes.*.container": "string", - "volumes.*.mode": "string|in:ro,rw",*/ - "labels": "slice", - "env": "slice", - "entrypoint": "slice", - "command": "slice", - "restart_policy": "string|in:always,on-failure,unless-stopped,no", - "auto_remove": "bool", - "privileged": "bool", - "open_stdin": "bool", - "publish_all_ports": "bool", - "tty": "bool", - "cpu_shares": "int", - "cpus": "int", - "memory": "int", - } -} - -func (r *ContainerCreate) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "cpu_shares": "int", - "cpus": "int", - "memory": "int", - } -} - -func (r *ContainerCreate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerCreate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerCreate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/container_rename.go b/app/http/requests/container/container_rename.go deleted file mode 100644 index 8b746cf0..00000000 --- a/app/http/requests/container/container_rename.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ContainerRename struct { - ID string `form:"id" json:"id"` - Name string `form:"name" json:"name"` -} - -func (r *ContainerRename) Authorize(ctx http.Context) error { - return nil -} - -func (r *ContainerRename) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|string", - "name": "required|string", - } -} - -func (r *ContainerRename) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerRename) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerRename) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerRename) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/container_update.go b/app/http/requests/container/container_update.go deleted file mode 100644 index e7e3da9e..00000000 --- a/app/http/requests/container/container_update.go +++ /dev/null @@ -1,74 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type ContainerUpdate struct { - ID string `form:"id" json:"id"` - Name string `form:"name" json:"name"` - Image string `form:"image" json:"image"` - Ports []types.ContainerPort `form:"ports" json:"ports"` - Network string `form:"network" json:"network"` - Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` - Labels []string `form:"labels" json:"labels"` - Env []string `form:"env" json:"env"` - Entrypoint []string `form:"entrypoint" json:"entrypoint"` - Command []string `form:"command" json:"command"` - RestartPolicy string `form:"restart_policy" json:"restart_policy"` - AutoRemove bool `form:"auto_remove" json:"auto_remove"` - Privileged bool `form:"privileged" json:"privileged"` - OpenStdin bool `form:"openStdin" json:"open_stdin"` - PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` - Tty bool `form:"tty" json:"tty"` - CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` - CPUs int64 `form:"cpus" json:"cpus"` - Memory int64 `form:"memory" json:"memory"` -} - -func (r *ContainerUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *ContainerUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|string", - "name": "required|string", - "image": "required|string", - "ports": "slice", - "network": "string", - "volumes": "slice", - "labels": "slice", - "env": "slice", - "entrypoint": "slice", - "command": "slice", - "restart_policy": "string|in:always,on-failure,unless-stopped,no", - "auto_remove": "bool", - "privileged": "bool", - "open_stdin": "bool", - "publish_all_ports": "bool", - "tty": "bool", - "cpu_shares": "int", - "cpus": "int", - "memory": "int", - } -} - -func (r *ContainerUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/id.go b/app/http/requests/container/id.go deleted file mode 100644 index 6f16469e..00000000 --- a/app/http/requests/container/id.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ID struct { - ID string `form:"id" json:"id"` -} - -func (r *ID) Authorize(ctx http.Context) error { - return nil -} - -func (r *ID) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|string", - } -} - -func (r *ID) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/image_pull.go b/app/http/requests/container/image_pull.go deleted file mode 100644 index 7c9c2586..00000000 --- a/app/http/requests/container/image_pull.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ImagePull struct { - Name string `form:"name" json:"name"` - Auth bool `form:"auth" json:"auth"` - Username string `form:"username" json:"username"` - Password string `form:"password" json:"password"` -} - -func (r *ImagePull) Authorize(ctx http.Context) error { - return nil -} - -func (r *ImagePull) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "auth": "bool", - "username": "string", - "password": "string", - } -} - -func (r *ImagePull) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ImagePull) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ImagePull) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ImagePull) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/network_connect_disconnect.go b/app/http/requests/container/network_connect_disconnect.go deleted file mode 100644 index b90cea4f..00000000 --- a/app/http/requests/container/network_connect_disconnect.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type NetworkConnectDisConnect struct { - Network string `form:"network" json:"network"` - Container string `form:"container" json:"container"` -} - -func (r *NetworkConnectDisConnect) Authorize(ctx http.Context) error { - return nil -} - -func (r *NetworkConnectDisConnect) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "network": "required|string", - "container": "required|string", - } -} - -func (r *NetworkConnectDisConnect) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkConnectDisConnect) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkConnectDisConnect) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkConnectDisConnect) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/network_create.go b/app/http/requests/container/network_create.go deleted file mode 100644 index 8f63fa9b..00000000 --- a/app/http/requests/container/network_create.go +++ /dev/null @@ -1,48 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type NetworkCreate struct { - Name string `form:"name" json:"name"` - Driver string `form:"driver" json:"driver"` - Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"` - Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"` - Labels []types.KV `form:"labels" json:"labels"` - Options []types.KV `form:"options" json:"options"` -} - -func (r *NetworkCreate) Authorize(ctx http.Context) error { - return nil -} - -func (r *NetworkCreate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "driver": "required|string|in:bridge,overlay,macvlan,ipvlan", - "ipv4": "required", - "ipv6": "required", - "labels": "slice", - "options": "slice", - } -} - -func (r *NetworkCreate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkCreate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkCreate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkCreate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/volume_create.go b/app/http/requests/container/volume_create.go deleted file mode 100644 index 244c022a..00000000 --- a/app/http/requests/container/volume_create.go +++ /dev/null @@ -1,44 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type VolumeCreate struct { - Name string `form:"name" json:"name"` - Driver string `form:"driver" json:"driver"` - Labels []types.KV `form:"labels" json:"labels"` - Options []types.KV `form:"options" json:"options"` -} - -func (r *VolumeCreate) Authorize(ctx http.Context) error { - return nil -} - -func (r *VolumeCreate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "driver": "required|string|in:local", - "labels": "slice", - "options": "slice", - } -} - -func (r *VolumeCreate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *VolumeCreate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *VolumeCreate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *VolumeCreate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/archive.go b/app/http/requests/file/archive.go deleted file mode 100644 index c01a6ca0..00000000 --- a/app/http/requests/file/archive.go +++ /dev/null @@ -1,39 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Archive struct { - Paths []string `form:"paths" json:"paths"` - File string `form:"file" json:"file"` -} - -func (r *Archive) Authorize(ctx http.Context) error { - return nil -} - -func (r *Archive) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "paths": "array", - "paths.*": `regex:^/.*$|path_exists`, - "file": `regex:^/.*$|path_not_exists`, - } -} - -func (r *Archive) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Archive) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Archive) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Archive) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/copy.go b/app/http/requests/file/copy.go deleted file mode 100644 index 1678ba40..00000000 --- a/app/http/requests/file/copy.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Copy struct { - Source string `form:"source" json:"source"` - Target string `form:"target" json:"target"` -} - -func (r *Copy) Authorize(ctx http.Context) error { - return nil -} - -func (r *Copy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "source": `regex:^/.*$|path_exists`, - "target": `regex:^/.*$`, - } -} - -func (r *Copy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Copy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Copy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Copy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/exist.go b/app/http/requests/file/exist.go deleted file mode 100644 index 79c7ac64..00000000 --- a/app/http/requests/file/exist.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Exist struct { - Path string `form:"path" json:"path"` -} - -func (r *Exist) Authorize(ctx http.Context) error { - return nil -} - -func (r *Exist) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - } -} - -func (r *Exist) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Exist) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Exist) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Exist) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/move.go b/app/http/requests/file/move.go deleted file mode 100644 index 2b528f88..00000000 --- a/app/http/requests/file/move.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Move struct { - Source string `form:"source" json:"source"` - Target string `form:"target" json:"target"` -} - -func (r *Move) Authorize(ctx http.Context) error { - return nil -} - -func (r *Move) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "source": `regex:^/.*$|path_exists`, - "target": `regex:^/.*$`, - } -} - -func (r *Move) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Move) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Move) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Move) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/not_exist.go b/app/http/requests/file/not_exist.go deleted file mode 100644 index edc07c9c..00000000 --- a/app/http/requests/file/not_exist.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type NotExist struct { - Path string `form:"path" json:"path"` -} - -func (r *NotExist) Authorize(ctx http.Context) error { - return nil -} - -func (r *NotExist) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_not_exists`, - } -} - -func (r *NotExist) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NotExist) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NotExist) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NotExist) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/permission.go b/app/http/requests/file/permission.go deleted file mode 100644 index c3a2319b..00000000 --- a/app/http/requests/file/permission.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Permission struct { - Path string `form:"path" json:"path"` - Mode string `form:"mode" json:"mode"` - Owner string `form:"owner" json:"owner"` - Group string `form:"group" json:"group"` -} - -func (r *Permission) Authorize(ctx http.Context) error { - return nil -} - -func (r *Permission) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - "mode": "regex:^0[0-7]{3}$", - "owner": "regex:^[a-zA-Z0-9_-]+$", - "group": "regex:^[a-zA-Z0-9_-]+$", - } -} - -func (r *Permission) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Permission) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Permission) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Permission) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/save.go b/app/http/requests/file/save.go deleted file mode 100644 index 16be8f16..00000000 --- a/app/http/requests/file/save.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Save struct { - Path string `form:"path" json:"path"` - Content string `form:"content" json:"content"` -} - -func (r *Save) Authorize(ctx http.Context) error { - return nil -} - -func (r *Save) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - "content": "required|string", - } -} - -func (r *Save) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Save) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Save) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Save) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/search.go b/app/http/requests/file/search.go deleted file mode 100644 index ac5bee53..00000000 --- a/app/http/requests/file/search.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Search struct { - Path string `form:"path" json:"path"` - KeyWord string `form:"keyword" json:"keyword"` -} - -func (r *Search) Authorize(ctx http.Context) error { - return nil -} - -func (r *Search) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - "keyword": "required|string", - } -} - -func (r *Search) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Search) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Search) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Search) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/un_archive.go b/app/http/requests/file/un_archive.go deleted file mode 100644 index 80a11d27..00000000 --- a/app/http/requests/file/un_archive.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UnArchive struct { - File string `form:"file" json:"file"` - Path string `form:"path" json:"path"` -} - -func (r *UnArchive) Authorize(ctx http.Context) error { - return nil -} - -func (r *UnArchive) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "file": `regex:^/.*$|path_exists`, - "path": `regex:^/.*$`, - } -} - -func (r *UnArchive) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UnArchive) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UnArchive) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UnArchive) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/upload.go b/app/http/requests/file/upload.go deleted file mode 100644 index 3b0860c7..00000000 --- a/app/http/requests/file/upload.go +++ /dev/null @@ -1,40 +0,0 @@ -package requests - -import ( - "mime/multipart" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Upload struct { - Path string `form:"path" json:"path"` - File *multipart.FileHeader `form:"file" json:"file"` -} - -func (r *Upload) Authorize(ctx http.Context) error { - return nil -} - -func (r *Upload) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$`, - "file": "required", - } -} - -func (r *Upload) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Upload) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Upload) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Upload) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/frp/service.go b/app/http/requests/plugins/frp/service.go deleted file mode 100644 index 2f47777d..00000000 --- a/app/http/requests/plugins/frp/service.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Service struct { - Service string `form:"service" json:"service"` -} - -func (r *Service) Authorize(ctx http.Context) error { - return nil -} - -func (r *Service) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "service": "required|string|in:frps,frpc", - } -} - -func (r *Service) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Service) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Service) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Service) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/frp/update_config.go b/app/http/requests/plugins/frp/update_config.go deleted file mode 100644 index 03bc614c..00000000 --- a/app/http/requests/plugins/frp/update_config.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateConfig struct { - Service string `form:"service" json:"service"` - Config string `form:"config" json:"config"` -} - -func (r *UpdateConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "service": "required|string|in:frps,frpc", - "config": "required|string", - } -} - -func (r *UpdateConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/gitea/update_config.go b/app/http/requests/plugins/gitea/update_config.go deleted file mode 100644 index f54e5737..00000000 --- a/app/http/requests/plugins/gitea/update_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/podman/update_registry_config.go b/app/http/requests/plugins/podman/update_registry_config.go deleted file mode 100644 index 56e947ef..00000000 --- a/app/http/requests/plugins/podman/update_registry_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateRegistryConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateRegistryConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateRegistryConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateRegistryConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateRegistryConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateRegistryConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateRegistryConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/podman/update_storage_config.go b/app/http/requests/plugins/podman/update_storage_config.go deleted file mode 100644 index 646a6f8d..00000000 --- a/app/http/requests/plugins/podman/update_storage_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateStorageConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateStorageConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateStorageConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateStorageConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateStorageConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateStorageConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateStorageConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/rsync/create.go b/app/http/requests/plugins/rsync/create.go deleted file mode 100644 index c4be10f1..00000000 --- a/app/http/requests/plugins/rsync/create.go +++ /dev/null @@ -1,46 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Create struct { - Name string `form:"name" json:"name"` - Path string `form:"path" json:"path"` - Comment string `form:"comment" json:"comment"` - AuthUser string `form:"auth_user" json:"auth_user"` - Secret string `form:"secret" json:"secret"` - HostsAllow string `form:"hosts_allow" json:"hosts_allow"` -} - -func (r *Create) Authorize(ctx http.Context) error { - return nil -} - -func (r *Create) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|regex:^[a-zA-Z0-9-_]+$", - "path": `regex:^/.*$`, - "comment": "string", - "auth_user": "required|regex:^[a-zA-Z0-9-_]+$", - "secret": "required|min_len:8", - "hosts_allow": "string", - } -} - -func (r *Create) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Create) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Create) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Create) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/rsync/update.go b/app/http/requests/plugins/rsync/update.go deleted file mode 100644 index ba00a271..00000000 --- a/app/http/requests/plugins/rsync/update.go +++ /dev/null @@ -1,46 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Update struct { - Name string `form:"name" json:"name"` - Path string `form:"path" json:"path"` - Comment string `form:"comment" json:"comment"` - AuthUser string `form:"auth_user" json:"auth_user"` - Secret string `form:"secret" json:"secret"` - HostsAllow string `form:"hosts_allow" json:"hosts_allow"` -} - -func (r *Update) Authorize(ctx http.Context) error { - return nil -} - -func (r *Update) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|regex:^[a-zA-Z0-9-_]+$", - "path": `regex:^/.*$`, - "comment": "string", - "auth_user": "required|regex:^[a-zA-Z0-9-_]+$", - "secret": "required|min_len:8", - "hosts_allow": "string", - } -} - -func (r *Update) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/rsync/update_config.go b/app/http/requests/plugins/rsync/update_config.go deleted file mode 100644 index f54e5737..00000000 --- a/app/http/requests/plugins/rsync/update_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/setting/https.go b/app/http/requests/setting/https.go deleted file mode 100644 index 4fe64dc6..00000000 --- a/app/http/requests/setting/https.go +++ /dev/null @@ -1,40 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Https struct { - Https bool `form:"https" json:"https"` - Cert string `form:"cert" json:"cert"` - Key string `form:"key" json:"key"` -} - -func (r *Https) Authorize(ctx http.Context) error { - return nil -} - -func (r *Https) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "https": "bool", - "cert": "string", - "key": "string", - } -} - -func (r *Https) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Https) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Https) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Https) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/setting/update.go b/app/http/requests/setting/update.go deleted file mode 100644 index 240ee997..00000000 --- a/app/http/requests/setting/update.go +++ /dev/null @@ -1,57 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Update struct { - Name string `form:"name" json:"name"` - Language string `form:"language" json:"language"` - Port uint `form:"port" json:"port"` - BackupPath string `form:"backup_path" json:"backup_path"` - WebsitePath string `form:"website_path" json:"website_path"` - Entrance string `form:"entrance" json:"entrance"` - UserName string `form:"username" json:"username"` - Email string `form:"email" json:"email"` - Password string `form:"password" json:"password"` -} - -func (r *Update) Authorize(ctx http.Context) error { - return nil -} - -func (r *Update) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string:2,20", - "language": "required|in:zh_CN,en", - "port": "required|int:1000,65535", - "backup_path": "required|string:2,255", - "website_path": "required|string:2,255", - "entrance": `required|regex:^/(\w+)?$|not_in:/api`, - "username": "required|string:2,20", - "email": "required|email", - "password": "string:8,255", - } -} - -func (r *Update) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "port": "uint", - } -} - -func (r *Update) Messages(ctx http.Context) map[string]string { - return map[string]string{ - "port.int": "port 值必须是一个整数且在 1000 - 65535 之间", - "password.string": "password 必须是一个字符串且长度在 8 - 255 之间", - } -} - -func (r *Update) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/user/login.go b/app/http/requests/user/login.go deleted file mode 100644 index 5b27edf9..00000000 --- a/app/http/requests/user/login.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Login struct { - Username string `json:"username" form:"username"` - Password string `json:"password" form:"password"` -} - -func (r *Login) Authorize(ctx http.Context) error { - return nil -} - -func (r *Login) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "username": "required", - "password": "required|min_len:8", - } -} - -func (r *Login) Messages(ctx http.Context) map[string]string { - return map[string]string{ - "username.required": "用户名不能为空", - "password.required": "密码不能为空", - "password.min_len": "密码长度不能小于 8 位", - } -} - -func (r *Login) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Login) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Login) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/add.go b/app/http/requests/website/add.go deleted file mode 100644 index 03b23299..00000000 --- a/app/http/requests/website/add.go +++ /dev/null @@ -1,54 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Add struct { - Name string `form:"name" json:"name"` - Domains []string `form:"domains" json:"domains"` - Ports []uint `form:"ports" json:"ports"` - Path string `form:"path" json:"path"` - PHP string `form:"php" json:"php"` - DB bool `form:"db" json:"db"` - DBType string `form:"db_type" json:"db_type"` - DBName string `form:"db_name" json:"db_name"` - DBUser string `form:"db_user" json:"db_user"` - DBPassword string `form:"db_password" json:"db_password"` -} - -func (r *Add) Authorize(ctx http.Context) error { - return nil -} - -func (r *Add) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name|not_in:phpmyadmin,mysql,panel,ssh", - "domains": "required|slice", - "ports": "required|slice", - "path": `regex:^/.*$`, - "php": "required", - "db": "bool", - "db_type": "required_if:db,true|in:0,mysql,postgresql", - "db_name": "required_if:db,true|regex:^[a-zA-Z0-9_-]+$", - "db_user": "required_if:db,true|regex:^[a-zA-Z0-9_-]+$", - "db_password": "required_if:db,true|min_len:8", - } -} - -func (r *Add) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Add) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Add) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Add) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/delete.go b/app/http/requests/website/delete.go deleted file mode 100644 index dece27f5..00000000 --- a/app/http/requests/website/delete.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Delete struct { - ID uint `form:"id" json:"id"` - Path bool `form:"path" json:"path"` - DB bool `form:"db" json:"db"` -} - -func (r *Delete) Authorize(ctx http.Context) error { - return nil -} - -func (r *Delete) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - "path": "bool", - "db": "bool", - } -} - -func (r *Delete) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - } -} - -func (r *Delete) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Delete) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Delete) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/delete_backup.go b/app/http/requests/website/delete_backup.go deleted file mode 100644 index 62c0ab13..00000000 --- a/app/http/requests/website/delete_backup.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type DeleteBackup struct { - Name string `form:"name" json:"name"` -} - -func (r *DeleteBackup) Authorize(ctx http.Context) error { - return nil -} - -func (r *DeleteBackup) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - } -} - -func (r *DeleteBackup) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DeleteBackup) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DeleteBackup) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DeleteBackup) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/id.go b/app/http/requests/website/id.go deleted file mode 100644 index 65fdc9fa..00000000 --- a/app/http/requests/website/id.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ID struct { - ID uint `form:"id" json:"id"` -} - -func (r *ID) Authorize(ctx http.Context) error { - return nil -} - -func (r *ID) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - } -} - -func (r *ID) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - } -} - -func (r *ID) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/restore_backup.go b/app/http/requests/website/restore_backup.go deleted file mode 100644 index 5c5ad2c8..00000000 --- a/app/http/requests/website/restore_backup.go +++ /dev/null @@ -1,40 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type RestoreBackup struct { - ID uint `form:"id" json:"id"` - Name string `form:"name" json:"name"` -} - -func (r *RestoreBackup) Authorize(ctx http.Context) error { - return nil -} - -func (r *RestoreBackup) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - "name": "required|string", - } -} - -func (r *RestoreBackup) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - } -} - -func (r *RestoreBackup) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *RestoreBackup) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *RestoreBackup) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/save_config.go b/app/http/requests/website/save_config.go deleted file mode 100644 index ac794cf0..00000000 --- a/app/http/requests/website/save_config.go +++ /dev/null @@ -1,100 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type SaveConfig struct { - ID uint `form:"id" json:"id"` - Domains []string `form:"domains" json:"domains"` - Ports []uint `form:"ports" json:"ports"` - SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"` - QUICPorts []uint `form:"quic_ports" json:"quic_ports"` - OCSP bool `form:"ocsp" json:"ocsp"` - HSTS bool `form:"hsts" json:"hsts"` - SSL bool `form:"ssl" json:"ssl"` - HTTPRedirect bool `form:"http_redirect" json:"http_redirect"` - OpenBasedir bool `form:"open_basedir" json:"open_basedir"` - Waf bool `form:"waf" json:"waf"` - WafCache string `form:"waf_cache" json:"waf_cache"` - WafMode string `form:"waf_mode" json:"waf_mode"` - WafCcDeny string `form:"waf_cc_deny" json:"waf_cc_deny"` - Index string `form:"index" json:"index"` - Path string `form:"path" json:"path"` - Root string `form:"root" json:"root"` - Raw string `form:"raw" json:"raw"` - Rewrite string `form:"rewrite" json:"rewrite"` - PHP int `form:"php" json:"php"` - SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` - SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` -} - -func (r *SaveConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *SaveConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - "domains": "required|slice", - "ports": "required|slice", - "ssl_ports": "slice|not_in:80", - "quic_ports": "slice|not_in:80", - "ocsp": "bool", - "hsts": "bool", - "ssl": "bool", - "http_redirect": "bool", - "open_basedir": "bool", - "waf": "bool", - "waf_cache": "required|string", - "waf_mode": "required|string", - "waf_cc_deny": "required|string", - "index": "required|string", - "path": "required|string", - "root": "required|string", - "raw": "required|string", - "rewrite": "string", - "php": "int", - "ssl_certificate": "required_if:ssl,true", - "ssl_certificate_key": "required_if:ssl,true", - } -} - -func (r *SaveConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - "php": "int", - } -} - -func (r *SaveConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *SaveConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *SaveConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - _, exist := data.Get("waf_mode") - if !exist { - if err := data.Set("waf_mode", "DYNAMIC"); err != nil { - return err - } - } - _, exist = data.Get("waf_cc_deny") - if !exist { - if err := data.Set("waf_cc_deny", "rate=1000r/m duration=60m"); err != nil { - return err - } - } - _, exist = data.Get("waf_cache") - if !exist { - if err := data.Set("waf_cache", "capacity=50"); err != nil { - return err - } - } - - return nil -} diff --git a/app/jobs/process_task.go b/app/jobs/process_task.go deleted file mode 100644 index 41d273cc..00000000 --- a/app/jobs/process_task.go +++ /dev/null @@ -1,74 +0,0 @@ -package jobs - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/shell" -) - -// ProcessTask 处理面板任务 -type ProcessTask struct { -} - -// Signature The name and signature of the job. -func (receiver *ProcessTask) Signature() string { - return "process_task" -} - -// Handle Execute the job. -func (receiver *ProcessTask) Handle(args ...any) error { - taskID, ok := args[0].(uint) - if !ok { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "args": args, - }).Infof("参数错误") - return nil - } - - var task models.Task - _ = facades.Orm().Query().Where("id = ?", taskID).Get(&task) - if task.ID == 0 { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - }).Infof("任务不存在") - return nil - } - - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - }).Infof("开始执行任务") - - task.Status = models.TaskStatusRunning - if err := facades.Orm().Query().Save(&task); err != nil { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - "error": err.Error(), - }).Infof("更新任务状态失败") - return nil - } - - if _, err := shell.Execf(task.Shell); err != nil { - task.Status = models.TaskStatusFailed - _ = facades.Orm().Query().Save(&task) - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - "error": err.Error(), - }).Infof("执行任务失败") - return nil - } - - task.Status = models.TaskStatusSuccess - if err := facades.Orm().Query().Save(&task); err != nil { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - "error": err.Error(), - }).Infof("更新任务状态失败") - return nil - } - - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - }).Infof("执行任务成功") - return nil -} diff --git a/app/models/cert.go b/app/models/cert.go deleted file mode 100644 index bdcaab65..00000000 --- a/app/models/cert.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -import ( - "github.com/goravel/framework/database/orm" -) - -type Cert struct { - orm.Model - UserID uint `gorm:"not null" json:"user_id"` // 关联的 ACME 用户 ID - WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID - DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID - Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096) - Domains []string `gorm:"not null;serializer:json" json:"domains"` - AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签 - CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用) - Cert string `gorm:"not null" json:"cert"` // 证书内容 - Key string `gorm:"not null" json:"key"` // 私钥内容 - - Website *Website `gorm:"foreignKey:WebsiteID" json:"website"` - User *CertUser `gorm:"foreignKey:UserID" json:"user"` - DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"` -} diff --git a/app/models/cert_dns.go b/app/models/cert_dns.go deleted file mode 100644 index 014d33f2..00000000 --- a/app/models/cert_dns.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import ( - "github.com/goravel/framework/database/orm" - - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type CertDNS struct { - orm.Model - Name string `gorm:"not null" json:"name"` // 备注名称 - Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, tencent, aliyun, cloudflare) - Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"` - - Certs []*Cert `gorm:"foreignKey:DNSID" json:"-"` -} diff --git a/app/models/cert_user.go b/app/models/cert_user.go deleted file mode 100644 index 9a9c881c..00000000 --- a/app/models/cert_user.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type CertUser struct { - orm.Model - Email string `gorm:"not null" json:"email"` - CA string `gorm:"not null" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) - Kid string `gorm:"not null" json:"kid"` - HmacEncoded string `gorm:"not null" json:"hmac_encoded"` - PrivateKey string `gorm:"not null" json:"private_key"` - KeyType string `gorm:"not null" json:"key_type"` - - Certs []*Cert `gorm:"foreignKey:UserID" json:"-"` -} diff --git a/app/models/cron.go b/app/models/cron.go deleted file mode 100644 index 5d22f5eb..00000000 --- a/app/models/cron.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Cron struct { - orm.Model - Name string `gorm:"not null;unique" json:"name"` - Status bool `gorm:"not null" json:"status"` - Type string `gorm:"not null" json:"type"` - Time string `gorm:"not null" json:"time"` - Shell string `gorm:"not null" json:"shell"` - Log string `gorm:"not null" json:"log"` -} diff --git a/app/models/database.go b/app/models/database.go deleted file mode 100644 index 57c3e85e..00000000 --- a/app/models/database.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Database struct { - orm.Model - Name string `gorm:"not null;unique" json:"name"` - Type string `gorm:"not null" json:"type"` - Host string `gorm:"not null" json:"host"` - Port int `gorm:"not null" json:"port"` - Username string `gorm:"not null" json:"username"` - Password string `gorm:"not null" json:"password"` - Remark string `gorm:"not null" json:"remark"` -} diff --git a/app/models/monitor.go b/app/models/monitor.go deleted file mode 100644 index e8566083..00000000 --- a/app/models/monitor.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import ( - "github.com/goravel/framework/database/orm" - - "github.com/TheTNB/panel/v2/pkg/tools" -) - -type Monitor struct { - orm.Model - Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"` -} diff --git a/app/models/plugin.go b/app/models/plugin.go deleted file mode 100644 index 87ac7370..00000000 --- a/app/models/plugin.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Plugin struct { - orm.Model - Slug string `gorm:"not null;unique" json:"slug"` - Version string `gorm:"not null" json:"version"` - Show bool `gorm:"not null" json:"show"` - ShowOrder int `gorm:"not null" json:"show_order"` -} diff --git a/app/models/setting.go b/app/models/setting.go deleted file mode 100644 index 04081bd2..00000000 --- a/app/models/setting.go +++ /dev/null @@ -1,23 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -const ( - SettingKeyName = "name" - SettingKeyVersion = "version" - SettingKeyMonitor = "monitor" - SettingKeyMonitorDays = "monitor_days" - SettingKeyBackupPath = "backup_path" - SettingKeyWebsitePath = "website_path" - SettingKeyMysqlRootPassword = "mysql_root_password" - SettingKeySshHost = "ssh_host" - SettingKeySshPort = "ssh_port" - SettingKeySshUser = "ssh_user" - SettingKeySshPassword = "ssh_password" -) - -type Setting struct { - orm.Model - Key string `gorm:"not null;unique" json:"key"` - Value string `gorm:"not null" json:"value"` -} diff --git a/app/models/task.go b/app/models/task.go deleted file mode 100644 index 87896048..00000000 --- a/app/models/task.go +++ /dev/null @@ -1,18 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -const ( - TaskStatusWaiting = "waiting" - TaskStatusRunning = "running" - TaskStatusSuccess = "finished" - TaskStatusFailed = "failed" -) - -type Task struct { - orm.Model - Name string `gorm:"not null;index" json:"name"` - Status string `gorm:"not null;default:'waiting'" json:"status"` - Shell string `gorm:"not null" json:"shell"` - Log string `gorm:"not null" json:"log"` -} diff --git a/app/models/user.go b/app/models/user.go deleted file mode 100644 index 55095690..00000000 --- a/app/models/user.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type User struct { - orm.Model - Username string `gorm:"not null;unique" json:"username"` - Password string `gorm:"not null" json:"password"` - Email string `gorm:"not null" json:"email"` -} diff --git a/app/models/website.go b/app/models/website.go deleted file mode 100644 index eface35c..00000000 --- a/app/models/website.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Website struct { - orm.Model - Name string `gorm:"not null;unique" json:"name"` - Status bool `gorm:"not null;default:true" json:"status"` - Path string `gorm:"not null" json:"path"` - PHP int `gorm:"not null" json:"php"` - SSL bool `gorm:"not null" json:"ssl"` - Remark string `gorm:"not null" json:"remark"` - - Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"` -} diff --git a/app/plugins/README.md b/app/plugins/README.md deleted file mode 100644 index 0e6118a9..00000000 --- a/app/plugins/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 面板插件目录 - -文档待定 diff --git a/app/plugins/fail2ban/controller.go b/app/plugins/fail2ban/controller.go deleted file mode 100644 index 9df9ac43..00000000 --- a/app/plugins/fail2ban/controller.go +++ /dev/null @@ -1,342 +0,0 @@ -package openresty - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Controller struct { - website internal.Website -} - -func NewController() *Controller { - return &Controller{ - website: services.NewWebsiteImpl(), - } -} - -// List 所有 Fail2ban 规则 -func (r *Controller) List(ctx http.Context) http.Response { - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1) - if len(jailList) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "Fail2ban 规则为空") - } - - var jails []types.Fail2banJail - for i, jail := range jailList { - if i == 0 { - continue - } - - jailName := jail[1] - jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END") - if len(jailRaw) == 0 { - continue - } - jailEnabled := strings.Contains(jailRaw, "enabled = true") - jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw) - jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw) - jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw) - jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw) - - jails = append(jails, types.Fail2banJail{ - Name: jailName, - Enabled: jailEnabled, - LogPath: jailLogPath[1], - MaxRetry: cast.ToInt(jailMaxRetry[1]), - FindTime: cast.ToInt(jailFindTime[1]), - BanTime: cast.ToInt(jailBanTime[1]), - }) - } - - paged, total := h.Paginate(ctx, jails) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Add 添加 Fail2ban 规则 -func (r *Controller) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required", - "type": "required|in:website,service", - "maxretry": "required", - "findtime": "required", - "bantime": "required", - "website_name": "required_if:type,website", - "website_mode": "required_if:type,website", - "website_path": "required_if:website_mode,path", - }); sanitize != nil { - return sanitize - } - - jailName := ctx.Request().Input("name") - jailType := ctx.Request().Input("type") - jailMaxRetry := ctx.Request().Input("maxretry") - jailFindTime := ctx.Request().Input("findtime") - jailBanTime := ctx.Request().Input("bantime") - jailWebsiteName := ctx.Request().Input("website_name") - jailWebsiteMode := ctx.Request().Input("website_mode") - jailWebsitePath := ctx.Request().Input("website_path") - - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") { - return h.Error(ctx, http.StatusUnprocessableEntity, "规则已存在") - } - - switch jailType { - case "website": - var website models.Website - err := facades.Orm().Query().Where("name", jailWebsiteName).FirstOrFail(&website) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "网站不存在") - } - config, err := r.website.GetConfig(website.ID) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败") - } - var ports string - for _, port := range config.Ports { - fields := strings.Fields(cast.ToString(port)) - ports += fields[0] + "," - } - - rule := ` -# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START -[` + jailWebsiteName + `-` + jailWebsiteMode + `] -enabled = true -filter = haozi-` + jailWebsiteName + `-` + jailWebsiteMode + ` -port = ` + ports + ` -maxretry = ` + jailMaxRetry + ` -findtime = ` + jailFindTime + ` -bantime = ` + jailBanTime + ` -action = %(action_mwl)s -logpath = /www/wwwlogs/` + website.Name + `.log -# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END -` - raw += rule - if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - var filter string - if jailWebsiteMode == "cc" { - filter = ` -[Definition] -failregex = ^\s-.*HTTP/.*$ -ignoreregex = -` - } else { - filter = ` -[Definition] -failregex = ^\s-.*\s` + jailWebsitePath + `.*HTTP/.*$ -ignoreregex = -` - } - if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - case "service": - var logPath string - var filter string - var port string - var err error - switch jailName { - case "ssh": - if os.IsDebian() || os.IsUbuntu() { - logPath = "/var/log/auth.log" - } else { - logPath = "/var/log/secure" - } - filter = "sshd" - port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") - case "mysql": - logPath = "/www/server/mysql/mysql-error.log" - filter = "mysqld-auth" - port, err = shell.Execf("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'") - case "pure-ftpd": - logPath = "/var/log/messages" - filter = "pure-ftpd" - port, err = shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`) - default: - return h.Error(ctx, http.StatusUnprocessableEntity, "未知服务") - } - if len(port) == 0 || err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装") - } - - rule := ` -# ` + jailName + `-START -[` + jailName + `] -enabled = true -filter = ` + filter + ` -port = ` + port + ` -maxretry = ` + jailMaxRetry + ` -findtime = ` + jailFindTime + ` -bantime = ` + jailBanTime + ` -action = %(action_mwl)s -logpath = ` + logPath + ` -# ` + jailName + `-END -` - raw += rule - if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - } - - if _, err := shell.Execf("fail2ban-client reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载配置失败") - } - - return h.Success(ctx, nil) -} - -// Delete 删除规则 -func (r *Controller) Delete(ctx http.Context) http.Response { - jailName := ctx.Request().Input("name") - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if !strings.Contains(raw, "["+jailName+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "规则不存在") - } - - rule := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END") - raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1) - raw = strings.TrimSpace(raw) - if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - if _, err := shell.Execf("fail2ban-client reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载配置失败") - } - - return h.Success(ctx, nil) -} - -// BanList 获取封禁列表 -func (r *Controller) BanList(ctx http.Context) http.Response { - name := ctx.Request().Input("name") - if len(name) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "缺少参数") - } - - currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, name) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败") - } - totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, name) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败") - } - bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | awk -F ":" '{print $2}'`, name) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败") - } - bannedIpList := strings.Split(bannedIp, " ") - - var list []map[string]string - for _, ip := range bannedIpList { - if len(ip) > 0 { - list = append(list, map[string]string{ - "name": name, - "ip": ip, - }) - } - } - if list == nil { - list = []map[string]string{} - } - - return h.Success(ctx, http.Json{ - "currently_ban": currentlyBan, - "total_ban": totalBan, - "baned_list": list, - }) -} - -// Unban 解封 -func (r *Controller) Unban(ctx http.Context) http.Response { - name := ctx.Request().Input("name") - ip := ctx.Request().Input("ip") - if len(name) == 0 || len(ip) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "缺少参数") - } - - if _, err := shell.Execf("fail2ban-client set %s unbanip %s", name, ip); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "解封失败") - } - - return h.Success(ctx, nil) -} - -// SetWhiteList 设置白名单 -func (r *Controller) SetWhiteList(ctx http.Context) http.Response { - ip := ctx.Request().Input("ip") - if len(ip) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "缺少参数") - } - - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - // 正则替换 - reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`) - if reg.MatchString(raw) { - raw = reg.ReplaceAllString(raw, "ignoreip = "+ip+"\n") - } else { - return h.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏") - } - - if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - if _, err := shell.Execf("fail2ban-client reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载配置失败") - } - return h.Success(ctx, nil) -} - -// GetWhiteList 获取白名单 -func (r *Controller) GetWhiteList(ctx http.Context) http.Response { - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`) - if reg.MatchString(raw) { - ignoreIp := reg.FindStringSubmatch(raw)[1] - return h.Success(ctx, ignoreIp) - } else { - return h.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏") - } -} diff --git a/app/plugins/fail2ban/main.go b/app/plugins/fail2ban/main.go deleted file mode 100644 index ab3621c5..00000000 --- a/app/plugins/fail2ban/main.go +++ /dev/null @@ -1,39 +0,0 @@ -package openresty - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/route" - - "github.com/TheTNB/panel/v2/app/http/middleware" - "github.com/TheTNB/panel/v2/app/plugins/loader" - "github.com/TheTNB/panel/v2/pkg/types" -) - -func init() { - loader.Register(&types.Plugin{ - Name: "Fail2ban", - Description: "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址,将该IP地址加入防火墙的拒绝访问列表中", - Slug: "fail2ban", - Version: "1.0.2", - Requires: []string{}, - Excludes: []string{}, - Install: `bash /www/panel/scripts/fail2ban/install.sh`, - Uninstall: `bash /www/panel/scripts/fail2ban/uninstall.sh`, - Update: `bash /www/panel/scripts/fail2ban/update.sh`, - Boot: func(app foundation.Application) { - RouteFacade := app.MakeRoute() - RouteFacade.Prefix("api/plugins/fail2ban").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - r.Prefix("openresty").Group(func(route route.Router) { - controller := NewController() - route.Get("jails", controller.List) - route.Post("jails", controller.Add) - route.Delete("jails", controller.Delete) - route.Get("jails/{name}", controller.BanList) - route.Post("unban", controller.Unban) - route.Post("whiteList", controller.SetWhiteList) - route.Get("whiteList", controller.GetWhiteList) - }) - }) - }, - }) -} diff --git a/app/plugins/frp_controller.go b/app/plugins/frp_controller.go deleted file mode 100644 index e199d8a1..00000000 --- a/app/plugins/frp_controller.go +++ /dev/null @@ -1,72 +0,0 @@ -package plugins - -import ( - "fmt" - - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/frp" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type FrpController struct { -} - -func NewFrpController() *FrpController { - return &FrpController{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Description 获取 Frp 配置 -// @Tags 插件-Frp -// @Produce json -// @Security BearerToken -// @Param service query string false "服务" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/frp/config [get] -func (r *FrpController) GetConfig(ctx http.Context) http.Response { - var serviceRequest requests.Service - sanitize := h.SanitizeRequest(ctx, &serviceRequest) - if sanitize != nil { - return sanitize - } - - config, err := io.Read(fmt.Sprintf("/www/server/frp/%s.toml", serviceRequest.Service)) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateConfig -// -// @Summary 更新配置 -// @Description 更新 Frp 配置 -// @Tags 插件-Frp -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/frp/config [post] -func (r *FrpController) UpdateConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write(fmt.Sprintf("/www/server/frp/%s.toml", updateRequest.Service), updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart(updateRequest.Service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/gitea_controller.go b/app/plugins/gitea_controller.go deleted file mode 100644 index ad361949..00000000 --- a/app/plugins/gitea_controller.go +++ /dev/null @@ -1,63 +0,0 @@ -package plugins - -import ( - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/gitea" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type GiteaController struct { -} - -func NewGiteaController() *GiteaController { - return &GiteaController{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Description 获取 Gitea 配置 -// @Tags 插件-Gitea -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/gitea/config [get] -func (r *GiteaController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/gitea/app.ini") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateConfig -// -// @Summary 更新配置 -// @Description 更新 Gitea 配置 -// @Tags 插件-Gitea -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/gitea/config [post] -func (r *GiteaController) UpdateConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/www/server/gitea/app.ini", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("gitea"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/loader/loader.go b/app/plugins/loader/loader.go deleted file mode 100644 index 21efd81d..00000000 --- a/app/plugins/loader/loader.go +++ /dev/null @@ -1,17 +0,0 @@ -package loader - -import ( - "github.com/TheTNB/panel/v2/pkg/types" -) - -var data []*types.Plugin - -// All 获取所有插件 -func All() []*types.Plugin { - return data -} - -// Register 注册插件 -func Register(plugin *types.Plugin) { - data = append(data, plugin) -} diff --git a/app/plugins/main.go b/app/plugins/main.go deleted file mode 100644 index 0595f1c3..00000000 --- a/app/plugins/main.go +++ /dev/null @@ -1,6 +0,0 @@ -package plugins - -import _ "github.com/TheTNB/panel/v2/app/plugins/openresty" - -// Boot 启动所有插件 -func Boot() {} diff --git a/app/plugins/mysql_controller.go b/app/plugins/mysql_controller.go deleted file mode 100644 index d2e60215..00000000 --- a/app/plugins/mysql_controller.go +++ /dev/null @@ -1,506 +0,0 @@ -package plugins - -import ( - "fmt" - "regexp" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/db" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type MySQLController struct { - setting internal.Setting - backup internal.Backup -} - -func NewMySQLController() *MySQLController { - return &MySQLController{ - setting: services.NewSettingImpl(), - backup: services.NewBackupImpl(), - } -} - -// GetConfig 获取配置 -func (r *MySQLController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/mysql/conf/my.cnf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *MySQLController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败") - } - - if err := systemctl.Reload("mysqld"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载MySQL失败") - } - - return h.Success(ctx, nil) -} - -// Load 获取负载 -func (r *MySQLController) Load(ctx http.Context) http.Response { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - if len(rootPassword) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空") - } - - status, _ := systemctl.Status("mysqld") - if !status { - return h.Success(ctx, []types.NV{}) - } - - raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败") - } - - var data []map[string]string - expressions := []struct { - regex string - name string - }{ - {`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"}, - {`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"}, - {`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"}, - {`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"}, - {`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"}, - {`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"}, - {`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"}, - {`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"}, - {`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"}, - {`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"}, - {`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"}, - {`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"}, - {`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"}, - {`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"}, - {`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"}, - {`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"}, - {`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"}, - {`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"}, - } - - for _, expression := range expressions { - re := regexp.MustCompile(expression.regex) - matches := re.FindStringSubmatch(raw) - if len(matches) > 1 { - d := map[string]string{"name": expression.name, "value": matches[1]} - if expression.name == "发送" || expression.name == "接收" { - d["value"] = str.FormatBytes(cast.ToFloat64(matches[1])) - } - - data = append(data, d) - } - } - - // 索引命中率 - readRequests := cast.ToFloat64(data[9]["value"]) - reads := cast.ToFloat64(data[10]["value"]) - data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100) - // Innodb 索引命中率 - bufferPoolReads := cast.ToFloat64(data[11]["value"]) - bufferPoolReadRequests := cast.ToFloat64(data[12]["value"]) - data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100) - - return h.Success(ctx, data) -} - -// ErrorLog 获取错误日志 -func (r *MySQLController) ErrorLog(ctx http.Context) http.Response { - log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-error.log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearErrorLog 清空错误日志 -func (r *MySQLController) ClearErrorLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-error.log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// SlowLog 获取慢查询日志 -func (r *MySQLController) SlowLog(ctx http.Context) http.Response { - log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-slow.log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearSlowLog 清空慢查询日志 -func (r *MySQLController) ClearSlowLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-slow.log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - return h.Success(ctx, nil) -} - -// GetRootPassword 获取root密码 -func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - if len(rootPassword) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空") - } - - return h.Success(ctx, rootPassword) -} - -// SetRootPassword 设置root密码 -func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response { - rootPassword := ctx.Request().Input("password") - if len(rootPassword) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空") - } - - oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix") - if err != nil { - // 尝试安全模式直接改密 - if err = db.MySQLResetRootPassword(rootPassword); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } else { - if err = mysql.UserPassword("root", rootPassword); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -// DatabaseList 获取数据库列表 -func (r *MySQLController) DatabaseList(ctx http.Context) http.Response { - password := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", password, r.getSock(), "unix") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []types.MySQLDatabase{}, - }) - } - - databases, err := mysql.Databases() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败") - } - paged, total := h.Paginate(ctx, databases) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddDatabase 添加数据库 -func (r *MySQLController) AddDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:32", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - database := ctx.Request().Input("database") - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.DatabaseCreate(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserCreate(user, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.PrivilegesGrant(user, database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteDatabase 删除数据库 -func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - database := ctx.Request().Input("database") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.DatabaseDrop(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// BackupList 获取备份列表 -func (r *MySQLController) BackupList(ctx http.Context) http.Response { - backups, err := r.backup.MysqlList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, backups) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// UploadBackup 上传备份 -func (r *MySQLController) UploadBackup(ctx http.Context) http.Response { - file, err := ctx.Request().File("file") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql" - if !io.Exists(backupPath) { - if err = io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - name := file.GetClientOriginalName() - _, err = file.StoreAs(backupPath, name) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - return h.Success(ctx, nil) -} - -// CreateBackup 创建备份 -func (r *MySQLController) CreateBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - if err := r.backup.MysqlBackup(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteBackup 删除备份 -func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - }); sanitize != nil { - return sanitize - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql" - fileName := ctx.Request().Input("name") - if err := io.Remove(backupPath + "/" + fileName); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// RestoreBackup 还原备份 -func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "backup": "required|min_len:1|max_len:255", - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys", - }); sanitize != nil { - return sanitize - } - - if err := r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UserList 用户列表 -func (r *MySQLController) UserList(ctx http.Context) http.Response { - password := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", password, r.getSock(), "unix") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []types.MySQLUser{}, - }) - } - - users, err := mysql.Users() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取用户列表失败") - } - paged, total := h.Paginate(ctx, users) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddUser 添加用户 -func (r *MySQLController) AddUser(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:32", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - database := ctx.Request().Input("database") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserCreate(user, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.PrivilegesGrant(user, database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteUser 删除用户 -func (r *MySQLController) DeleteUser(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserDrop(user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// SetUserPassword 设置用户密码 -func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:32", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserPassword(user, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// SetUserPrivileges 设置用户权限 -func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - database := ctx.Request().Input("database") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.PrivilegesGrant(user, database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// getSock 获取sock文件位置 -func (r *MySQLController) getSock() string { - if io.Exists("/tmp/mysql.sock") { - return "/tmp/mysql.sock" - } - if io.Exists("/www/server/mysql/config/my.cnf") { - config, _ := io.Read("/www/server/mysql/config/my.cnf") - re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`) - matches := re.FindStringSubmatch(config) - if len(matches) > 2 { - return matches[2] - } - } - if io.Exists("/etc/my.cnf") { - config, _ := io.Read("/etc/my.cnf") - re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`) - matches := re.FindStringSubmatch(config) - if len(matches) > 2 { - return matches[2] - } - } - - return "/tmp/mysql.sock" -} diff --git a/app/plugins/openresty/controller.go b/app/plugins/openresty/controller.go deleted file mode 100644 index f37414dd..00000000 --- a/app/plugins/openresty/controller.go +++ /dev/null @@ -1,187 +0,0 @@ -package openresty - -import ( - "fmt" - "regexp" - "time" - - "github.com/go-resty/resty/v2" - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Controller struct { - // Dependent services -} - -func NewController() *Controller { - return &Controller{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/config [get] -func (r *Controller) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/openresty/conf/nginx.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig -// -// @Summary 保存配置 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Param config body string true "配置" -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/config [post] -func (r *Controller) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "配置不能为空") - } - - if err := io.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "保存配置失败") - } - - if err := systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -// ErrorLog -// -// @Summary 获取错误日志 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/errorLog [get] -func (r *Controller) ErrorLog(ctx http.Context) http.Response { - if !io.Exists("/www/wwwlogs/nginx_error.log") { - return h.Success(ctx, "") - } - - out, err := shell.Execf("tail -n 100 /www/wwwlogs/openresty_error.log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, out) -} - -// ClearErrorLog -// -// @Summary 清空错误日志 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/clearErrorLog [post] -func (r *Controller) ClearErrorLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/wwwlogs/openresty_error.log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// Load -// -// @Summary 获取负载状态 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/load [get] -func (r *Controller) Load(ctx http.Context) http.Response { - client := resty.New().SetTimeout(10 * time.Second) - resp, err := client.R().Get("http://127.0.0.1/nginx_status") - if err != nil || !resp.IsSuccess() { - return h.Success(ctx, []types.NV{}) - } - - raw := resp.String() - var data []types.NV - - workers, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | wc -l") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取负载失败") - } - data = append(data, types.NV{ - Name: "工作进程", - Value: workers, - }) - - out, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取负载失败") - } - mem := str.FormatBytes(cast.ToFloat64(out)) - data = append(data, types.NV{ - Name: "内存占用", - Value: mem, - }) - - match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw) - if len(match) == 2 { - data = append(data, types.NV{ - Name: "活跃连接数", - Value: match[1], - }) - } - - match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw) - if len(match) == 4 { - data = append(data, types.NV{ - Name: "总连接次数", - Value: match[1], - }) - data = append(data, types.NV{ - Name: "总握手次数", - Value: match[2], - }) - data = append(data, types.NV{ - Name: "总请求次数", - Value: match[3], - }) - } - - match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw) - if len(match) == 4 { - data = append(data, types.NV{ - Name: "请求数", - Value: match[1], - }) - data = append(data, types.NV{ - Name: "响应数", - Value: match[2], - }) - data = append(data, types.NV{ - Name: "驻留进程", - Value: match[3], - }) - } - - return h.Success(ctx, data) -} diff --git a/app/plugins/openresty/main.go b/app/plugins/openresty/main.go deleted file mode 100644 index aaa4da0f..00000000 --- a/app/plugins/openresty/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package openresty - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/route" - - "github.com/TheTNB/panel/v2/app/http/middleware" - "github.com/TheTNB/panel/v2/app/plugins/loader" - "github.com/TheTNB/panel/v2/pkg/types" -) - -func init() { - loader.Register(&types.Plugin{ - Name: "OpenResty", - Description: "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台", - Slug: "openresty", - Version: "1.25.3.1", - Requires: []string{}, - Excludes: []string{}, - Install: "bash /www/panel/scripts/openresty/install.sh", - Uninstall: "bash /www/panel/scripts/openresty/uninstall.sh", - Update: "bash /www/panel/scripts/openresty/install.sh", - Boot: func(app foundation.Application) { - RouteFacade := app.MakeRoute() - RouteFacade.Prefix("api/plugins/openresty").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - r.Prefix("openresty").Group(func(route route.Router) { - controller := NewController() - route.Get("load", controller.Load) - route.Get("config", controller.GetConfig) - route.Post("config", controller.SaveConfig) - route.Get("errorLog", controller.ErrorLog) - route.Post("clearErrorLog", controller.ClearErrorLog) - }) - }) - }, - }) -} diff --git a/app/plugins/php_controller.go b/app/plugins/php_controller.go deleted file mode 100644 index f14fd7e5..00000000 --- a/app/plugins/php_controller.go +++ /dev/null @@ -1,246 +0,0 @@ -package plugins - -import ( - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type PHPController struct{} - -func NewPHPController() *PHPController { - return &PHPController{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/config [get] -func (r *PHPController) GetConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config, err := service.GetConfig() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveConfig -// -// @Summary 保存配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param config body string true "配置" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/config [post] -func (r *PHPController) SaveConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config := ctx.Request().Input("config") - if err := service.SaveConfig(config); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetFPMConfig -// -// @Summary 获取 FPM 配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/fpmConfig [get] -func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config, err := service.GetFPMConfig() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveFPMConfig -// -// @Summary 保存 FPM 配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param config body string true "配置" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/fpmConfig [post] -func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config := ctx.Request().Input("config") - if err := service.SaveFPMConfig(config); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Load -// -// @Summary 获取负载状态 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/load [get] -func (r *PHPController) Load(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - load, err := service.Load() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, load) -} - -// ErrorLog -// -// @Summary 获取错误日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/errorLog [get] -func (r *PHPController) ErrorLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - log, _ := service.GetErrorLog() - return h.Success(ctx, log) -} - -// SlowLog -// -// @Summary 获取慢日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/slowLog [get] -func (r *PHPController) SlowLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - log, _ := service.GetSlowLog() - return h.Success(ctx, log) -} - -// ClearErrorLog -// -// @Summary 清空错误日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/clearErrorLog [post] -func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - err := service.ClearErrorLog() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ClearSlowLog -// -// @Summary 清空慢日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/clearSlowLog [post] -func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - err := service.ClearSlowLog() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ExtensionList -// -// @Summary 获取扩展列表 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/extensions [get] -func (r *PHPController) ExtensionList(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - extensions, err := service.GetExtensions() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, extensions) -} - -// InstallExtension -// -// @Summary 安装扩展 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param slug query string true "slug" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/extensions [post] -func (r *PHPController) InstallExtension(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - slug := ctx.Request().Input("slug") - if len(slug) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - - if err := service.InstallExtension(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UninstallExtension -// -// @Summary 卸载扩展 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param slug query string true "slug" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/extensions [delete] -func (r *PHPController) UninstallExtension(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - slug := ctx.Request().Input("slug") - if len(slug) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - - if err := service.UninstallExtension(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/phpmyadmin_controller.go b/app/plugins/phpmyadmin_controller.go deleted file mode 100644 index d0c386d5..00000000 --- a/app/plugins/phpmyadmin_controller.go +++ /dev/null @@ -1,127 +0,0 @@ -package plugins - -import ( - "fmt" - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type PhpMyAdminController struct { -} - -func NewPhpMyAdminController() *PhpMyAdminController { - return &PhpMyAdminController{} -} - -func (r *PhpMyAdminController) Info(ctx http.Context) http.Response { - files, err := io.ReadDir("/www/server/phpmyadmin") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录") - } - - var phpmyadmin string - for _, f := range files { - if strings.HasPrefix(f.Name(), "phpmyadmin_") { - phpmyadmin = f.Name() - } - } - if len(phpmyadmin) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录") - } - - conf, err := io.Read("/www/server/vhost/phpmyadmin.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf) - if len(match) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 端口") - } - - return h.Success(ctx, http.Json{ - "path": phpmyadmin, - "port": cast.ToInt(match[1]), - }) -} - -func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response { - port := ctx.Request().InputInt("port") - if port == 0 { - return h.Error(ctx, http.StatusInternalServerError, "端口不能为空") - } - - conf, err := io.Read("/www/server/vhost/phpmyadmin.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(port)+";") - if err := io.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{ - "error": err.Error(), - }).Info("修改 phpMyAdmin 端口失败") - return h.ErrorSystem(ctx) - } - - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%d/tcp --permanent", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw allow %d/tcp", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -func (r *PhpMyAdminController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/vhost/phpmyadmin.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -func (r *PhpMyAdminController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "配置不能为空") - } - - if err := io.Write("/www/server/vhost/phpmyadmin.conf", config, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{ - "error": err.Error(), - }).Info("修改 phpMyAdmin 配置失败") - return h.ErrorSystem(ctx) - } - - if err := systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/podman_controller.go b/app/plugins/podman_controller.go deleted file mode 100644 index d5bddcef..00000000 --- a/app/plugins/podman_controller.go +++ /dev/null @@ -1,109 +0,0 @@ -package plugins - -import ( - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/podman" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type PodmanController struct { -} - -func NewPodmanController() *PodmanController { - return &PodmanController{} -} - -// GetRegistryConfig -// -// @Summary 获取注册表配置 -// @Description 获取 Podman 注册表配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/registryConfig [get] -func (r *PodmanController) GetRegistryConfig(ctx http.Context) http.Response { - config, err := io.Read("/etc/containers/registries.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateRegistryConfig -// -// @Summary 更新注册表配置 -// @Description 更新 Podman 注册表配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateRegistryConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/registryConfig [post] -func (r *PodmanController) UpdateRegistryConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateRegistryConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/etc/containers/registries.conf", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("podman"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetStorageConfig -// -// @Summary 获取存储配置 -// @Description 获取 Podman 存储配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/storageConfig [get] -func (r *PodmanController) GetStorageConfig(ctx http.Context) http.Response { - config, err := io.Read("/etc/containers/storage.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateStorageConfig -// -// @Summary 更新存储配置 -// @Description 更新 Podman 存储配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateStorageConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/storageConfig [post] -func (r *PodmanController) UpdateStorageConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateStorageConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/etc/containers/storage.conf", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("podman"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/postgresql_controller.go b/app/plugins/postgresql_controller.go deleted file mode 100644 index 55b76c51..00000000 --- a/app/plugins/postgresql_controller.go +++ /dev/null @@ -1,491 +0,0 @@ -package plugins - -import ( - "database/sql" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - _ "github.com/lib/pq" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PostgreSQLController struct { - setting internal.Setting - backup internal.Backup -} - -func NewPostgreSQLController() *PostgreSQLController { - return &PostgreSQLController{ - setting: services.NewSettingImpl(), - backup: services.NewBackupImpl(), - } -} - -// GetConfig 获取配置 -func (r *PostgreSQLController) GetConfig(ctx http.Context) http.Response { - // 获取配置 - config, err := io.Read("/www/server/postgresql/data/postgresql.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败") - } - - return h.Success(ctx, config) -} - -// GetUserConfig 获取用户配置 -func (r *PostgreSQLController) GetUserConfig(ctx http.Context) http.Response { - // 获取配置 - config, err := io.Read("/www/server/postgresql/data/pg_hba.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *PostgreSQLController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败") - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// SaveUserConfig 保存用户配置 -func (r *PostgreSQLController) SaveUserConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败") - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// Load 获取负载 -func (r *PostgreSQLController) Load(ctx http.Context) http.Response { - status, _ := systemctl.Status("postgresql") - if !status { - return h.Success(ctx, []types.NV{}) - } - - time, err := shell.Execf(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败") - } - pid, err := shell.Execf(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败") - } - process, err := shell.Execf(`ps aux | grep postgres | grep -v grep | wc -l`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败") - } - connections, err := shell.Execf(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败") - } - storage, err := shell.Execf(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败") - } - - data := []types.NV{ - {Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()}, - {Name: "进程 PID", Value: pid}, - {Name: "进程数", Value: process}, - {Name: "总连接数", Value: connections}, - {Name: "空间占用", Value: storage}, - } - - return h.Success(ctx, data) -} - -// Log 获取日志 -func (r *PostgreSQLController) Log(ctx http.Context) http.Response { - log, err := shell.Execf("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearLog 清空日志 -func (r *PostgreSQLController) ClearLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// DatabaseList 获取数据库列表 -func (r *PostgreSQLController) DatabaseList(ctx http.Context) http.Response { - type database struct { - Name string `json:"name"` - Owner string `json:"owner"` - Encoding string `json:"encoding"` - } - - db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []database{}, - }) - } - - if err = db.Ping(); err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []database{}, - }) - } - - query := ` - SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba), pg_catalog.pg_encoding_to_char(d.encoding) - FROM pg_catalog.pg_database d - WHERE datistemplate = false; - ` - rows, err := db.Query(query) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - defer rows.Close() - - var databases []database - for rows.Next() { - var db database - if err := rows.Scan(&db.Name, &db.Owner, &db.Encoding); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - databases = append(databases, db) - } - if err = rows.Err(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, databases) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddDatabase 添加数据库 -func (r *PostgreSQLController) AddDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:40", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - - if out, err := shell.Execf(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256" - if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// DeleteDatabase 删除数据库 -func (r *PostgreSQLController) DeleteDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - if out, err := shell.Execf(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// BackupList 获取备份列表 -func (r *PostgreSQLController) BackupList(ctx http.Context) http.Response { - backups, err := r.backup.PostgresqlList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取备份列表失败") - } - - paged, total := h.Paginate(ctx, backups) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// UploadBackup 上传备份 -func (r *PostgreSQLController) UploadBackup(ctx http.Context) http.Response { - file, err := ctx.Request().File("file") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - if !io.Exists(backupPath) { - if err = io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - name := file.GetClientOriginalName() - _, err = file.StoreAs(backupPath, name) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - return h.Success(ctx, nil) -} - -// CreateBackup 创建备份 -func (r *PostgreSQLController) CreateBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - if err := r.backup.PostgresqlBackup(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteBackup 删除备份 -func (r *PostgreSQLController) DeleteBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - }); sanitize != nil { - return sanitize - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - fileName := ctx.Request().Input("name") - if err := io.Remove(backupPath + "/" + fileName); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// RestoreBackup 还原备份 -func (r *PostgreSQLController) RestoreBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "backup": "required|min_len:1|max_len:255", - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1", - }); sanitize != nil { - return sanitize - } - - if err := r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error()) - } - - return h.Success(ctx, nil) -} - -// RoleList 角色列表 -func (r *PostgreSQLController) RoleList(ctx http.Context) http.Response { - type role struct { - Role string `json:"role"` - Attributes []string `json:"attributes"` - } - - db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []role{}, - }) - } - if err = db.Ping(); err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []role{}, - }) - } - - query := ` - SELECT rolname, - rolsuper, - rolcreaterole, - rolcreatedb, - rolreplication, - rolbypassrls - FROM pg_roles - WHERE rolcanlogin = true; - ` - rows, err := db.Query(query) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - defer rows.Close() - - var roles []role - for rows.Next() { - var r role - var super, canCreateRole, canCreateDb, replication, bypassRls bool - if err = rows.Scan(&r.Role, &super, &canCreateRole, &canCreateDb, &replication, &bypassRls); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - permissions := map[string]bool{ - "超级用户": super, - "创建角色": canCreateRole, - "创建数据库": canCreateDb, - "可以复制": replication, - "绕过行级安全": bypassRls, - } - for perm, enabled := range permissions { - if enabled { - r.Attributes = append(r.Attributes, perm) - } - } - - if len(r.Attributes) == 0 { - r.Attributes = append(r.Attributes, "无") - } - - roles = append(roles, r) - } - if err = rows.Err(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, roles) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddRole 添加角色 -func (r *PostgreSQLController) AddRole(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:40", - }); sanitize != nil { - return sanitize - } - - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - database := ctx.Request().Input("database") - if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256" - if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// DeleteRole 删除角色 -func (r *PostgreSQLController) DeleteRole(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - }); sanitize != nil { - return sanitize - } - - user := ctx.Request().Input("user") - if out, err := shell.Execf(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// SetRolePassword 设置用户密码 -func (r *PostgreSQLController) SetRolePassword(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:40", - }); sanitize != nil { - return sanitize - } - - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - if out, err := shell.Execf(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/pureftpd_controller.go b/app/plugins/pureftpd_controller.go deleted file mode 100644 index 79394c5d..00000000 --- a/app/plugins/pureftpd_controller.go +++ /dev/null @@ -1,179 +0,0 @@ -package plugins - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PureFtpdController struct { -} - -func NewPureFtpdController() *PureFtpdController { - return &PureFtpdController{} -} - -// List 获取用户列表 -func (r *PureFtpdController) List(ctx http.Context) http.Response { - listRaw, err := shell.Execf("pure-pw list") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []types.PureFtpdUser{}, - }) - } - - listArr := strings.Split(listRaw, "\n") - var users []types.PureFtpdUser - for _, v := range listArr { - if len(v) == 0 { - continue - } - - match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v) - users = append(users, types.PureFtpdUser{ - Username: match[1], - Path: strings.Replace(match[2], "/./", "/", 1), - }) - } - - paged, total := h.Paginate(ctx, users) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Add 添加用户 -func (r *PureFtpdController) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "username": "required", - "password": "required|min_len:6", - "path": "required", - }); sanitize != nil { - return sanitize - } - - username := ctx.Request().Input("username") - password := ctx.Request().Input("password") - path := ctx.Request().Input("path") - - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - if !io.Exists(path) { - return h.Error(ctx, http.StatusUnprocessableEntity, "目录不存在") - } - - if err := io.Chmod(path, 0755); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "修改目录权限失败") - } - if err := io.Chown(path, "www", "www"); err != nil { - return nil - } - if out, err := shell.Execf(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("pure-pw mkdb"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// Delete 删除用户 -func (r *PureFtpdController) Delete(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "username": "required", - }); sanitize != nil { - return sanitize - } - - username := ctx.Request().Input("username") - - if out, err := shell.Execf("pure-pw userdel " + username + " -m"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("pure-pw mkdb"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// ChangePassword 修改密码 -func (r *PureFtpdController) ChangePassword(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "username": "required", - "password": "required|min_len:6", - }); sanitize != nil { - return sanitize - } - - username := ctx.Request().Input("username") - password := ctx.Request().Input("password") - - if out, err := shell.Execf(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("pure-pw mkdb"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// GetPort 获取端口 -func (r *PureFtpdController) GetPort(ctx http.Context) http.Response { - port, err := shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败") - } - - return h.Success(ctx, cast.ToInt(port)) -} - -// SetPort 设置端口 -func (r *PureFtpdController) SetPort(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "port": "required", - }); sanitize != nil { - return sanitize - } - - port := ctx.Request().Input("port") - if out, err := shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%s/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`, port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%s/tcp --permanent", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err := systemctl.Restart("pure-ftpd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/redis_controller.go b/app/plugins/redis_controller.go deleted file mode 100644 index 1e234182..00000000 --- a/app/plugins/redis_controller.go +++ /dev/null @@ -1,93 +0,0 @@ -package plugins - -import ( - "strings" - - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type RedisController struct { -} - -func NewRedisController() *RedisController { - return &RedisController{} -} - -// GetConfig 获取配置 -func (r *RedisController) GetConfig(ctx http.Context) http.Response { - // 获取配置 - config, err := io.Read("/www/server/redis/redis.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *RedisController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/redis/redis.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败") - } - - if err := systemctl.Restart("redis"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重启Redis失败") - } - - return h.Success(ctx, nil) -} - -// Load 获取负载 -func (r *RedisController) Load(ctx http.Context) http.Response { - status, err := systemctl.Status("redis") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败") - } - if !status { - return h.Error(ctx, http.StatusInternalServerError, "Redis已停止运行") - } - - raw, err := shell.Execf("redis-cli info") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取Redis负载失败") - } - - infoLines := strings.Split(raw, "\n") - dataRaw := make(map[string]string) - - for _, item := range infoLines { - parts := strings.Split(item, ":") - if len(parts) == 2 { - dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } - - data := []types.NV{ - {Name: "TCP 端口", Value: dataRaw["tcp_port"]}, - {Name: "已运行天数", Value: dataRaw["uptime_in_days"]}, - {Name: "连接的客户端数", Value: dataRaw["connected_clients"]}, - {Name: "已分配的内存总量", Value: dataRaw["used_memory_human"]}, - {Name: "占用内存总量", Value: dataRaw["used_memory_rss_human"]}, - {Name: "占用内存峰值", Value: dataRaw["used_memory_peak_human"]}, - {Name: "内存碎片比率", Value: dataRaw["mem_fragmentation_ratio"]}, - {Name: "运行以来连接过的客户端的总数", Value: dataRaw["total_connections_received"]}, - {Name: "运行以来执行过的命令的总数", Value: dataRaw["total_commands_processed"]}, - {Name: "每秒执行的命令数", Value: dataRaw["instantaneous_ops_per_sec"]}, - {Name: "查找数据库键成功次数", Value: dataRaw["keyspace_hits"]}, - {Name: "查找数据库键失败次数", Value: dataRaw["keyspace_misses"]}, - {Name: "最近一次 fork() 操作耗费的毫秒数", Value: dataRaw["latest_fork_usec"]}, - } - - return h.Success(ctx, data) -} diff --git a/app/plugins/rsync_controller.go b/app/plugins/rsync_controller.go deleted file mode 100644 index 1c15b859..00000000 --- a/app/plugins/rsync_controller.go +++ /dev/null @@ -1,299 +0,0 @@ -package plugins - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/rsync" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type RsyncController struct { -} - -func NewRsyncController() *RsyncController { - return &RsyncController{} -} - -// List -// -// @Summary 列出模块 -// @Description 列出所有 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules [get] -func (r *RsyncController) List(ctx http.Context) http.Response { - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - var modules []types.RsyncModule - lines := strings.Split(config, "\n") - var currentModule *types.RsyncModule - - for _, line := range lines { - line = strings.TrimSpace(line) - - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { - if currentModule != nil { - modules = append(modules, *currentModule) - } - moduleName := line[1 : len(line)-1] - currentModule = &types.RsyncModule{ - Name: moduleName, - } - } else if currentModule != nil { - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - switch key { - case "path": - currentModule.Path = value - case "comment": - currentModule.Comment = value - case "read only": - currentModule.ReadOnly = value == "yes" || value == "true" - case "auth users": - currentModule.AuthUser = value - currentModule.Secret, err = shell.Execf("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败") - } - case "hosts allow": - currentModule.HostsAllow = value - } - } - } - } - - if currentModule != nil { - modules = append(modules, *currentModule) - } - - paged, total := h.Paginate(ctx, modules) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Create -// -// @Summary 添加模块 -// @Description 添加 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param data body requests.Create true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules [post] -func (r *RsyncController) Create(ctx http.Context) http.Response { - var createRequest requests.Create - sanitize := h.SanitizeRequest(ctx, &createRequest) - if sanitize != nil { - return sanitize - } - - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if strings.Contains(config, "["+createRequest.Name+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "模块 "+createRequest.Name+" 已存在") - } - - conf := `# ` + createRequest.Name + `-START -[` + createRequest.Name + `] -path = ` + createRequest.Path + ` -comment = ` + createRequest.Comment + ` -read only = no -auth users = ` + createRequest.AuthUser + ` -hosts allow = ` + createRequest.HostsAllow + ` -secrets file = /etc/rsyncd.secrets -# ` + createRequest.Name + `-END -` - - if err := io.WriteAppend("/etc/rsyncd.conf", conf); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("echo '" + createRequest.AuthUser + ":" + createRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Destroy -// -// @Summary 删除模块 -// @Description 删除 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param name path string true "模块名称" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules/{name} [delete] -func (r *RsyncController) Destroy(ctx http.Context) http.Response { - name := ctx.Request().Input("name") - if len(name) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空") - } - - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if !strings.Contains(config, "["+name+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在") - } - - module := str.Cut(config, "# "+name+"-START", "# "+name+"-END") - config = strings.Replace(config, "\n# "+name+"-START"+module+"# "+name+"-END", "", -1) - - match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module) - if len(match) == 2 { - authUser := match[1] - if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Update -// -// @Summary 更新模块 -// @Description 更新 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param name path string true "模块名称" -// @Param data body requests.Update true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules/{name} [post] -func (r *RsyncController) Update(ctx http.Context) http.Response { - var updateRequest requests.Update - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if !strings.Contains(config, "["+updateRequest.Name+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "模块 "+updateRequest.Name+" 不存在") - } - - newConf := `# ` + updateRequest.Name + `-START -[` + updateRequest.Name + `] -path = ` + updateRequest.Path + ` -comment = ` + updateRequest.Comment + ` -read only = no -auth users = ` + updateRequest.AuthUser + ` -hosts allow = ` + updateRequest.HostsAllow + ` -secrets file = /etc/rsyncd.secrets -# ` + updateRequest.Name + `-END` - - module := str.Cut(config, "# "+updateRequest.Name+"-START", "# "+updateRequest.Name+"-END") - config = strings.Replace(config, "# "+updateRequest.Name+"-START"+module+"# "+updateRequest.Name+"-END", newConf, -1) - - match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module) - if len(match) == 2 { - authUser := match[1] - if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("echo '" + updateRequest.AuthUser + ":" + updateRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err = systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetConfig -// -// @Summary 获取配置 -// @Description 获取 Rsync 配置 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/config [get] -func (r *RsyncController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateConfig -// -// @Summary 更新配置 -// @Description 更新 Rsync 配置 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/config [post] -func (r *RsyncController) UpdateConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/etc/rsyncd.conf", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/s3fs_controller.go b/app/plugins/s3fs_controller.go deleted file mode 100644 index c16c1b3e..00000000 --- a/app/plugins/s3fs_controller.go +++ /dev/null @@ -1,181 +0,0 @@ -package plugins - -import ( - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/json" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type S3fsController struct { - setting internal.Setting -} - -func NewS3fsController() *S3fsController { - return &S3fsController{ - setting: services.NewSettingImpl(), - } -} - -// List 所有 S3fs 挂载 -func (r *S3fsController) List(ctx http.Context) http.Response { - var s3fsList []types.S3fsMount - err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") - } - - paged, total := h.Paginate(ctx, s3fsList) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Add 添加 S3fs 挂载 -func (r *S3fsController) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "ak": "required|regex:^[a-zA-Z0-9]*$", - "sk": "required|regex:^[a-zA-Z0-9]*$", - "bucket": "required|regex:^[a-zA-Z0-9_-]*$", - "url": "required|full_url", - "path": "required|regex:^/[a-zA-Z0-9_-]+$", - }); sanitize != nil { - return sanitize - } - - ak := ctx.Request().Input("ak") - sk := ctx.Request().Input("sk") - path := ctx.Request().Input("path") - bucket := ctx.Request().Input("bucket") - url := ctx.Request().Input("url") - - // 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的 - if strings.Contains(url, bucket) { - return h.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称") - } - - // 检查挂载目录是否存在且为空 - if !io.Exists(path) { - if err := io.Mkdir(path, 0755); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载目录创建失败") - } - } - if !io.Empty(path) { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空") - } - - var s3fsList []types.S3fsMount - if err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") - } - - for _, s := range s3fsList { - if s.Path == path { - return h.Error(ctx, http.StatusUnprocessableEntity, "路径已存在") - } - } - - id := carbon.Now().TimestampMilli() - password := ak + ":" + sk - if err := io.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil { - return nil - } - out, err := shell.Execf(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil { - _, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`) - return h.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck) - } - if _, err := shell.Execf("df -h | grep " + path + " 2>&1"); err != nil { - _, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`) - return h.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确") - } - - s3fsList = append(s3fsList, types.S3fsMount{ - ID: id, - Path: path, - Bucket: bucket, - Url: url, - }) - encoded, err := json.MarshalString(s3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败") - } - err = r.setting.Set("s3fs", encoded) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败") - } - - return h.Success(ctx, nil) -} - -// Delete 删除 S3fs 挂载 -func (r *S3fsController) Delete(ctx http.Context) http.Response { - id := ctx.Request().Input("id") - if len(id) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空") - } - - var s3fsList []types.S3fsMount - err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") - } - - var mount types.S3fsMount - for _, s := range s3fsList { - if cast.ToString(s.ID) == id { - mount = s - break - } - } - if mount.ID == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在") - } - - if out, err := shell.Execf(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`umount '` + mount.Path + `' 2>&1`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck) - } - if err := io.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - var newS3fsList []types.S3fsMount - for _, s := range s3fsList { - if s.ID != mount.ID { - newS3fsList = append(newS3fsList, s) - } - } - encoded, err := json.MarshalString(newS3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败") - } - err = r.setting.Set("s3fs", encoded) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败") - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/supervisor_controller.go b/app/plugins/supervisor_controller.go deleted file mode 100644 index 4be32032..00000000 --- a/app/plugins/supervisor_controller.go +++ /dev/null @@ -1,336 +0,0 @@ -package plugins - -import ( - "fmt" - "strconv" - "strings" - - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type SupervisorController struct { - service string -} - -func NewSupervisorController() *SupervisorController { - var service string - if os.IsRHEL() { - service = "supervisord" - } else { - service = "supervisor" - } - - return &SupervisorController{ - service: service, - } -} - -// Service 获取服务名称 -func (r *SupervisorController) Service(ctx http.Context) http.Response { - return h.Success(ctx, r.service) -} - -// Log 日志 -func (r *SupervisorController) Log(ctx http.Context) http.Response { - log, err := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearLog 清空日志 -func (r *SupervisorController) ClearLog(ctx http.Context) http.Response { - if out, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// Config 获取配置 -func (r *SupervisorController) Config(ctx http.Context) http.Response { - var config string - var err error - if os.IsRHEL() { - config, err = io.Read(`/etc/supervisord.conf`) - } else { - config, err = io.Read(`/etc/supervisor/supervisord.conf`) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *SupervisorController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - var err error - if os.IsRHEL() { - err = io.Write(`/etc/supervisord.conf`, config, 0644) - } else { - err = io.Write(`/etc/supervisor/supervisord.conf`, config, 0644) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = systemctl.Restart(r.service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", r.service)) - } - - return h.Success(ctx, nil) -} - -// Processes 进程列表 -func (r *SupervisorController) Processes(ctx http.Context) http.Response { - type process struct { - Name string `json:"name"` - Status string `json:"status"` - Pid string `json:"pid"` - Uptime string `json:"uptime"` - } - - out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - var processes []process - for _, line := range strings.Split(out, "\n") { - if len(line) == 0 { - continue - } - - var p process - p.Name = line - if status, err := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $2}'`); err == nil { - p.Status = status - } - if p.Status == "RUNNING" { - pid, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $4}'`) - p.Pid = strings.ReplaceAll(pid, ",", "") - uptime, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $6}'`) - p.Uptime = uptime - } else { - p.Pid = "-" - p.Uptime = "-" - } - processes = append(processes, p) - } - - paged, total := h.Paginate(ctx, processes) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// StartProcess 启动进程 -func (r *SupervisorController) StartProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl start %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// StopProcess 停止进程 -func (r *SupervisorController) StopProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// RestartProcess 重启进程 -func (r *SupervisorController) RestartProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl restart %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// ProcessLog 进程日志 -func (r *SupervisorController) ProcessLog(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - var logPath string - var err error - if os.IsRHEL() { - logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } else { - logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径") - } - - log, err := shell.Execf(`tail -n 200 ` + logPath) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearProcessLog 清空进程日志 -func (r *SupervisorController) ClearProcessLog(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - var logPath string - var err error - if os.IsRHEL() { - logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } else { - logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("无法从进程%s的配置文件中获取日志路径", process)) - } - - if out, err := shell.Execf(`echo "" > ` + logPath); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// ProcessConfig 获取进程配置 -func (r *SupervisorController) ProcessConfig(ctx http.Context) http.Response { - process := ctx.Request().Query("process") - var config string - var err error - if os.IsRHEL() { - config, err = io.Read(`/etc/supervisord.d/` + process + `.conf`) - } else { - config, err = io.Read(`/etc/supervisor/conf.d/` + process + `.conf`) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveProcessConfig 保存进程配置 -func (r *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - config := ctx.Request().Input("config") - var err error - if os.IsRHEL() { - err = io.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644) - } else { - err = io.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644) - } - - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - _, _ = shell.Execf(`supervisorctl reread`) - _, _ = shell.Execf(`supervisorctl update`) - _, _ = shell.Execf(`supervisorctl restart %s`, process) - - return h.Success(ctx, nil) -} - -// AddProcess 添加进程 -func (r *SupervisorController) AddProcess(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|alpha_dash", - "user": "required|alpha_dash", - "path": "required", - "command": "required", - "num": "required", - }); sanitize != nil { - return sanitize - } - - name := ctx.Request().Input("name") - user := ctx.Request().Input("user") - path := ctx.Request().Input("path") - command := ctx.Request().Input("command") - num := ctx.Request().InputInt("num", 1) - config := `[program:` + name + `] -command=` + command + ` -process_name=%(program_name)s -directory=` + path + ` -autostart=true -autorestart=true -user=` + user + ` -numprocs=` + strconv.Itoa(num) + ` -redirect_stderr=true -stdout_logfile=/var/log/supervisor/` + name + `.log -stdout_logfile_maxbytes=2MB -` - - var err error - if os.IsRHEL() { - err = io.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644) - } else { - err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644) - } - - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - _, _ = shell.Execf(`supervisorctl reread`) - _, _ = shell.Execf(`supervisorctl update`) - _, _ = shell.Execf(`supervisorctl start %s`, name) - - return h.Success(ctx, nil) -} - -// DeleteProcess 删除进程 -func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - var logPath string - var err error - if os.IsRHEL() { - logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - if err := io.Remove(`/etc/supervisord.d/` + process + `.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } else { - logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - if err := io.Remove(`/etc/supervisor/conf.d/` + process + `.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径") - } - - if err := io.Remove(logPath); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - _, _ = shell.Execf(`supervisorctl reread`) - _, _ = shell.Execf(`supervisorctl update`) - - return h.Success(ctx, nil) -} diff --git a/app/plugins/toolbox_controller.go b/app/plugins/toolbox_controller.go deleted file mode 100644 index a6e079fc..00000000 --- a/app/plugins/toolbox_controller.go +++ /dev/null @@ -1,241 +0,0 @@ -package plugins - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type ToolBoxController struct { -} - -func NewToolBoxController() *ToolBoxController { - return &ToolBoxController{} -} - -// GetDNS 获取 DNS 信息 -func (r *ToolBoxController) GetDNS(ctx http.Context) http.Response { - raw, err := io.Read("/etc/resolv.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1) - if len(match) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "找不到 DNS 信息") - } - - var dns []string - for _, m := range match { - dns = append(dns, m[1]) - } - - return h.Success(ctx, dns) -} - -// SetDNS 设置 DNS 信息 -func (r *ToolBoxController) SetDNS(ctx http.Context) http.Response { - dns1 := ctx.Request().Input("dns1") - dns2 := ctx.Request().Input("dns2") - - if len(dns1) == 0 || len(dns2) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "DNS 信息不能为空") - } - - var dns string - dns += "nameserver " + dns1 + "\n" - dns += "nameserver " + dns2 + "\n" - - if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入 DNS 信息失败") - } - - return h.Success(ctx, nil) -} - -// GetSWAP 获取 SWAP 信息 -func (r *ToolBoxController) GetSWAP(ctx http.Context) http.Response { - var total, used, free string - var size int64 - if io.Exists("/www/swap") { - file, err := io.FileInfo("/www/swap") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败") - } - - size = file.Size() / 1024 / 1024 - total = str.FormatBytes(float64(file.Size())) - } else { - size = 0 - total = "0.00 B" - } - - raw, err := shell.Execf("free | grep Swap") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败") - } - - match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw) - if len(match) > 0 { - used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024) - free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024) - } - - return h.Success(ctx, http.Json{ - "total": total, - "size": size, - "used": used, - "free": free, - }) -} - -// SetSWAP 设置 SWAP 信息 -func (r *ToolBoxController) SetSWAP(ctx http.Context) http.Response { - size := ctx.Request().InputInt("size") - - if io.Exists("/www/swap") { - if out, err := shell.Execf("swapoff /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("rm -f /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("sed -i '/www\\/swap/d' /etc/fstab"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - } - - if size > 1 { - free, err := shell.Execf("df -k /www | awk '{print $4}' | tail -n 1") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取磁盘空间失败") - } - if cast.ToInt64(free)*1024 < int64(size)*1024*1024 { - return h.Error(ctx, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+str.FormatBytes(cast.ToFloat64(free))) - } - - btrfsCheck, _ := shell.Execf("df -T /www | awk '{print $2}' | tail -n 1") - if strings.Contains(btrfsCheck, "btrfs") { - if out, err := shell.Execf("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - } else { - if out, err := shell.Execf("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size)); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("mkswap -f /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if err := io.Chmod("/www/swap", 0600); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "设置 SWAP 权限失败") - } - } - if out, err := shell.Execf("swapon /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - } - - return h.Success(ctx, nil) -} - -// GetTimezone 获取时区 -func (r *ToolBoxController) GetTimezone(ctx http.Context) http.Response { - raw, err := shell.Execf("timedatectl | grep zone") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取时区信息失败") - } - - match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw) - if len(match) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "找不到时区信息") - } - - type zone struct { - Label string `json:"label"` - Value string `json:"value"` - } - - zonesRaw, err := shell.Execf("timedatectl list-timezones") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取时区列表失败") - } - zones := strings.Split(zonesRaw, "\n") - - var zonesList []zone - for _, z := range zones { - zonesList = append(zonesList, zone{ - Label: z, - Value: z, - }) - } - - return h.Success(ctx, http.Json{ - "timezone": match[1], - "timezones": zonesList, - }) -} - -// SetTimezone 设置时区 -func (r *ToolBoxController) SetTimezone(ctx http.Context) http.Response { - timezone := ctx.Request().Input("timezone") - if len(timezone) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "时区不能为空") - } - - if out, err := shell.Execf("timedatectl set-timezone %s", timezone); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - - return h.Success(ctx, nil) -} - -// GetHosts 获取 hosts 信息 -func (r *ToolBoxController) GetHosts(ctx http.Context) http.Response { - hosts, err := io.Read("/etc/hosts") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - return h.Success(ctx, hosts) -} - -// SetHosts 设置 hosts 信息 -func (r *ToolBoxController) SetHosts(ctx http.Context) http.Response { - hosts := ctx.Request().Input("hosts") - if len(hosts) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "hosts 信息不能为空") - } - - if err := io.Write("/etc/hosts", hosts, 0644); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "写入 hosts 信息失败") - } - - return h.Success(ctx, nil) -} - -// SetRootPassword 设置 root 密码 -func (r *ToolBoxController) SetRootPassword(ctx http.Context) http.Response { - password := ctx.Request().Input("password") - if len(password) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "密码不能为空") - } - if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) { - return h.Error(ctx, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符") - } - - password = strings.ReplaceAll(password, `'`, `\'`) - if out, err := shell.Execf(`yes '` + password + `' | passwd root`); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - - return h.Success(ctx, nil) -} diff --git a/app/providers/app_service_provider.go b/app/providers/app_service_provider.go deleted file mode 100644 index a4dd934d..00000000 --- a/app/providers/app_service_provider.go +++ /dev/null @@ -1,16 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" -) - -type AppServiceProvider struct { -} - -func (receiver *AppServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *AppServiceProvider) Boot(app foundation.Application) { - -} diff --git a/app/providers/auth_service_provider.go b/app/providers/auth_service_provider.go deleted file mode 100644 index 6bdc039e..00000000 --- a/app/providers/auth_service_provider.go +++ /dev/null @@ -1,16 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" -) - -type AuthServiceProvider struct { -} - -func (receiver *AuthServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *AuthServiceProvider) Boot(app foundation.Application) { - -} diff --git a/app/providers/console_service_provider.go b/app/providers/console_service_provider.go deleted file mode 100644 index 18d5090d..00000000 --- a/app/providers/console_service_provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/console" -) - -type ConsoleServiceProvider struct { -} - -func (receiver *ConsoleServiceProvider) Register(app foundation.Application) { - kernel := console.Kernel{} - facades.Schedule().Register(kernel.Schedule()) - facades.Artisan().Register(kernel.Commands()) -} - -func (receiver *ConsoleServiceProvider) Boot(app foundation.Application) { - -} diff --git a/app/providers/database_service_provider.go b/app/providers/database_service_provider.go deleted file mode 100644 index 7a08070a..00000000 --- a/app/providers/database_service_provider.go +++ /dev/null @@ -1,22 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/database/seeder" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/database/gorm" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/pkg/migrate" -) - -type DatabaseServiceProvider struct { -} - -func (receiver *DatabaseServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) { - facades.Seeder().Register([]seeder.Seeder{}) - migrate.Migrate(facades.Orm().Query().(*gorm.QueryImpl).Instance()) -} diff --git a/app/providers/event_service_provider.go b/app/providers/event_service_provider.go deleted file mode 100644 index 24e66f81..00000000 --- a/app/providers/event_service_provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/event" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/facades" -) - -type EventServiceProvider struct{} - -func (receiver *EventServiceProvider) Register(app foundation.Application) { - facades.Event().Register(receiver.listen()) -} - -func (receiver *EventServiceProvider) Boot(app foundation.Application) { - -} - -func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener { - return map[event.Event][]event.Listener{} -} diff --git a/app/providers/plugin_service_provider.go b/app/providers/plugin_service_provider.go deleted file mode 100644 index 43858399..00000000 --- a/app/providers/plugin_service_provider.go +++ /dev/null @@ -1,20 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - - "github.com/TheTNB/panel/v2/app/plugins" - "github.com/TheTNB/panel/v2/app/plugins/loader" -) - -type PluginServiceProvider struct{} - -func (receiver *PluginServiceProvider) Register(app foundation.Application) { - plugins.Boot() -} - -func (receiver *PluginServiceProvider) Boot(app foundation.Application) { - for _, plugin := range loader.All() { - plugin.Boot(app) - } -} diff --git a/app/providers/queue_service_provider.go b/app/providers/queue_service_provider.go deleted file mode 100644 index cc8f1215..00000000 --- a/app/providers/queue_service_provider.go +++ /dev/null @@ -1,28 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/queue" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/jobs" -) - -type QueueServiceProvider struct { -} - -func (receiver *QueueServiceProvider) Register(app foundation.Application) { - if err := facades.Queue().Register(receiver.Jobs()); err != nil { - panic(err.Error()) - } -} - -func (receiver *QueueServiceProvider) Boot(app foundation.Application) { - -} - -func (receiver *QueueServiceProvider) Jobs() []queue.Job { - return []queue.Job{ - &jobs.ProcessTask{}, - } -} diff --git a/app/providers/route_service_provider.go b/app/providers/route_service_provider.go deleted file mode 100644 index 3e215008..00000000 --- a/app/providers/route_service_provider.go +++ /dev/null @@ -1,48 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - contractshttp "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/http/limit" - - "github.com/TheTNB/panel/v2/app/http" - "github.com/TheTNB/panel/v2/routes" -) - -type RouteServiceProvider struct{} - -func (receiver *RouteServiceProvider) Register(app foundation.Application) { -} - -func (receiver *RouteServiceProvider) Boot(app foundation.Application) { - // Add HTTP middlewares - facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...) - - receiver.configureRateLimiting() - - routes.Plugin() - routes.Api() -} - -func (receiver *RouteServiceProvider) configureRateLimiting() { - facades.RateLimiter().ForWithLimits("login", func(ctx contractshttp.Context) []contractshttp.Limit { - return []contractshttp.Limit{ - limit.PerMinute(5).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) { - ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{ - "message": "请求过于频繁,请等待一分钟后再试", - }) - }), - limit.PerHour(100).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) { - ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{ - "message": "请求过于频繁,请等待一小时后再试", - }) - }), - limit.PerDay(1000).Response(func(ctx contractshttp.Context) { - ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{ - "message": "面板遭受登录爆破攻击过多,已暂时屏蔽登录,请立刻更换面板端口", - }) - }), - } - }) -} diff --git a/app/providers/validation_service_provider.go b/app/providers/validation_service_provider.go deleted file mode 100644 index c5de9426..00000000 --- a/app/providers/validation_service_provider.go +++ /dev/null @@ -1,31 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/validation" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/rules" -) - -type ValidationServiceProvider struct { -} - -func (receiver *ValidationServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *ValidationServiceProvider) Boot(app foundation.Application) { - if err := facades.Validation().AddRules(receiver.rules()); err != nil { - facades.Log().Infof("add rules error: %+v", err) - } -} - -func (receiver *ValidationServiceProvider) rules() []validation.Rule { - return []validation.Rule{ - &rules.Exists{}, - &rules.NotExists{}, - &rules.PathExists{}, - &rules.PathNotExists{}, - } -} diff --git a/app/rules/exists.go b/app/rules/exists.go deleted file mode 100644 index 6909eb9e..00000000 --- a/app/rules/exists.go +++ /dev/null @@ -1,55 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -type Exists struct { -} - -// Signature The name of the rule. -func (receiver *Exists) Signature() string { - return "exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *Exists) Passes(_ validation.Data, val any, options ...any) bool { - - // 第一个参数,表名称,如 categories - tableName := options[0].(string) - // 第二个参数,字段名称,如 id - fieldName := options[1].(string) - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - // 判断是否存在 - var count int64 - query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue) - // 判断第三个参数及之后的参数是否存在 - if len(options) > 2 { - for i := 2; i < len(options); i++ { - query = query.OrWhere(options[i].(string), requestValue) - } - } - err = query.Count(&count) - if err != nil { - return false - } - - return count != 0 -} - -// Message Get the validation error message. -func (receiver *Exists) Message() string { - return "记录不存在" -} diff --git a/app/rules/not_exists.go b/app/rules/not_exists.go deleted file mode 100644 index e8231459..00000000 --- a/app/rules/not_exists.go +++ /dev/null @@ -1,55 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -type NotExists struct { -} - -// Signature The name of the rule. -func (receiver *NotExists) Signature() string { - return "not_exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *NotExists) Passes(_ validation.Data, val any, options ...any) bool { - - // 第一个参数,表名称,如 categories - tableName := options[0].(string) - // 第二个参数,字段名称,如 id - fieldName := options[1].(string) - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - // 判断是否存在 - var count int64 - query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue) - // 判断第三个参数及之后的参数是否存在 - if len(options) > 2 { - for i := 2; i < len(options); i++ { - query = query.OrWhere(options[i].(string), requestValue) - } - } - err = query.Count(&count) - if err != nil { - return false - } - - return count == 0 -} - -// Message Get the validation error message. -func (receiver *NotExists) Message() string { - return "记录已存在" -} diff --git a/app/rules/path_exist.go b/app/rules/path_exist.go deleted file mode 100644 index 8bc0800f..00000000 --- a/app/rules/path_exist.go +++ /dev/null @@ -1,37 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/io" -) - -type PathExists struct { -} - -// Signature The name of the rule. -func (receiver *PathExists) Signature() string { - return "path_exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *PathExists) Passes(_ validation.Data, val any, options ...any) bool { - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - return io.Exists(requestValue) -} - -// Message Get the validation error message. -func (receiver *PathExists) Message() string { - return "路径不存在" -} diff --git a/app/rules/path_not_exist.go b/app/rules/path_not_exist.go deleted file mode 100644 index c50fda88..00000000 --- a/app/rules/path_not_exist.go +++ /dev/null @@ -1,37 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/io" -) - -type PathNotExists struct { -} - -// Signature The name of the rule. -func (receiver *PathNotExists) Signature() string { - return "path_not_exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *PathNotExists) Passes(_ validation.Data, val any, options ...any) bool { - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - return !io.Exists(requestValue) -} - -// Message Get the validation error message. -func (receiver *PathNotExists) Message() string { - return "路径已存在" -} diff --git a/bootstrap/app.go b/bootstrap/app.go deleted file mode 100644 index fdb26d3c..00000000 --- a/bootstrap/app.go +++ /dev/null @@ -1,25 +0,0 @@ -package bootstrap - -import ( - "runtime/debug" - - "github.com/gookit/validate/locales/zhcn" - "github.com/goravel/framework/foundation" - - "github.com/TheTNB/panel/v2/config" -) - -func Boot() { - debug.SetGCPercent(10) - debug.SetMemoryLimit(64 << 20) - - zhcn.RegisterGlobal() - - app := foundation.NewApplication() - - // Bootstrap the application - app.Boot() - - // Bootstrap the config. - config.Boot() -} diff --git a/cmd/README.md b/cmd/README.md new file mode 100644 index 00000000..570a8169 --- /dev/null +++ b/cmd/README.md @@ -0,0 +1,3 @@ +# cmd + +cmd 目录存放应用的入口文件。 \ No newline at end of file diff --git a/scripts/panel.sh b/cmd/app/main.go similarity index 63% rename from scripts/panel.sh rename to cmd/app/main.go index 305b7ef3..173d5af3 100644 --- a/scripts/panel.sh +++ b/cmd/app/main.go @@ -1,7 +1,4 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' +/* Copyright (C) 2022 - now HaoZi Technology Co., Ltd. This program is free software: you can redistribute it and/or modify @@ -16,6 +13,22 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -' +*/ +package main -/www/panel/panel --env="/www/panel/panel.conf" artisan panel "$@" +import "github.com/TheTNB/panel/internal/bootstrap" + +// @title 耗子面板 API +// @version 2 +// @description 耗子面板的 API 信息 + +// @contact.name 耗子科技 +// @contact.email admin@haozi.net + +// @license.name GNU Affero General Public License v3 +// @license url https://www.gnu.org/licenses/agpl-3.0.html + +// @BasePath /api +func main() { + bootstrap.Boot() +} diff --git a/scripts/frp/uninstall.sh b/cmd/cli/main.go similarity index 53% rename from scripts/frp/uninstall.sh rename to cmd/cli/main.go index e2e3db94..9ec50a63 100644 --- a/scripts/frp/uninstall.sh +++ b/cmd/cli/main.go @@ -1,7 +1,4 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' +/* Copyright (C) 2022 - now HaoZi Technology Co., Ltd. This program is free software: you can redistribute it and/or modify @@ -16,22 +13,38 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -' +*/ +package main -HR="+----------------------------------------------------" -frpPath="/www/server/frp" +import ( + "fmt" + "log" + "os" -systemctl stop frps -systemctl stop frpc -systemctl disable frps -systemctl disable frpc + "github.com/urfave/cli/v2" +) -rm -rf ${frpPath} -rm -f /etc/systemd/system/frps.service -rm -f /etc/systemd/system/frpc.service -systemctl daemon-reload +func main() { + app := &cli.App{ + Name: "panel", + HelpName: "耗子面板", + Usage: "命令行工具", + UsageText: "panel [global options] command [command options] [arguments...]", + HideVersion: true, + Commands: []*cli.Command{ + { + Name: "test", + Aliases: []string{"t"}, + Usage: "print a test message", + Action: func(c *cli.Context) error { + fmt.Println("Hello, World!") + return nil + }, + }, + }, + } -panel deletePlugin frp -echo -e $HR -echo "frp 卸载完成" -echo -e $HR + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..20f1eba1 --- /dev/null +++ b/config/README.md @@ -0,0 +1,5 @@ +# config + +config 目录存放应用使用的各种配置文件。 + +不喜欢 toml 格式?你可以随意调整 `init.go` 以兼容你想要配置格式。 \ No newline at end of file diff --git a/config/app.go b/config/app.go deleted file mode 100644 index d7e9dd91..00000000 --- a/config/app.go +++ /dev/null @@ -1,123 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/auth" - "github.com/goravel/framework/cache" - "github.com/goravel/framework/console" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/crypt" - "github.com/goravel/framework/database" - "github.com/goravel/framework/event" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/filesystem" - "github.com/goravel/framework/hash" - "github.com/goravel/framework/http" - "github.com/goravel/framework/log" - "github.com/goravel/framework/mail" - "github.com/goravel/framework/queue" - "github.com/goravel/framework/route" - "github.com/goravel/framework/schedule" - "github.com/goravel/framework/session" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/path" - "github.com/goravel/framework/testing" - "github.com/goravel/framework/translation" - "github.com/goravel/framework/validation" - "github.com/goravel/gin" - - "github.com/TheTNB/panel/v2/app/providers" -) - -// Boot Start all init methods of the current folder to bootstrap all config. -func Boot() {} - -func init() { - config := facades.Config() - config.Add("app", map[string]any{ - // Application Name - // - // This value is the name of your application. This value is used when the - // framework needs to place the application's name in a notification or - // any other location as required by the application or its pkg. - "name": "耗子面板", - - // Application Environment - // - // This value determines the "environment" your application is currently - // running in. This may determine how you prefer to configure various - // services the application utilizes. Set this in your "panel.conf" file. - "env": config.Env("APP_ENV", "production"), - - // Application Debug Mode - "debug": config.Env("APP_DEBUG", false), - - // Application Timezone - // - // Here you may specify the default timezone for your application. - // Example: UTC, Asia/Shanghai - // More: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - "timezone": carbon.PRC, - - // Application Locale Configuration - // - // The application locale determines the default locale that will be used - // by the translation service provider.You are free to set this value - // to any of the locales which will be supported by the application. - "locale": config.Env("APP_LOCALE", "zh_CN"), - - // Application Fallback Locale - // - // The fallback locale determines the locale to use when the current one - // is not available.You may change the value to correspond to any of - // the language folders that are provided through your application. - "fallback_locale": "zh_CN", - - // Application Lang Path - // - // The path to the language files for the application. You may change - // the path to a different directory if you would like to customize it. - "lang_path": path.Executable("lang"), - - // Encryption Key - // - // 32 character string, otherwise these encrypted strings - // will not be safe. Please do this before deploying an application! - "key": config.Env("APP_KEY", ""), - - // Autoload service providers - // - // The service providers listed here will be automatically loaded on the - // request to your application. Feel free to add your own services to - // this array to grant expanded functionality to your applications. - "providers": []foundation.ServiceProvider{ - &log.ServiceProvider{}, - &console.ServiceProvider{}, - &database.ServiceProvider{}, - &cache.ServiceProvider{}, - &http.ServiceProvider{}, - &route.ServiceProvider{}, - &schedule.ServiceProvider{}, - &event.ServiceProvider{}, - &queue.ServiceProvider{}, - &mail.ServiceProvider{}, - &auth.ServiceProvider{}, - &hash.ServiceProvider{}, - &crypt.ServiceProvider{}, - &filesystem.ServiceProvider{}, - &validation.ServiceProvider{}, - &session.ServiceProvider{}, - &translation.ServiceProvider{}, - &testing.ServiceProvider{}, - &providers.AppServiceProvider{}, - &providers.AuthServiceProvider{}, - &providers.RouteServiceProvider{}, - &providers.PluginServiceProvider{}, - &providers.ConsoleServiceProvider{}, - &providers.QueueServiceProvider{}, - &providers.EventServiceProvider{}, - &providers.ValidationServiceProvider{}, - &providers.DatabaseServiceProvider{}, - &gin.ServiceProvider{}, - }, - }) -} diff --git a/config/auth.go b/config/auth.go deleted file mode 100644 index c05b7a6e..00000000 --- a/config/auth.go +++ /dev/null @@ -1,36 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("auth", map[string]any{ - // Authentication Defaults - // - // This option controls the default authentication "guard" - // reset options for your application. You may change these defaults - // as required, but they're a perfect start for most applications. - "defaults": map[string]any{ - "guard": "user", - }, - - // Authentication Guards - // - // Next, you may define every authentication guard for your application. - // Of course, a great default configuration has been defined for you - // here which uses session storage and the Eloquent user provider. - // - // All authentication drivers have a user provider. This defines how the - // users are actually retrieved out of your database or other storage - // mechanisms used by this application to persist your user's data. - // - // Supported: "jwt" - "guards": map[string]any{ - "user": map[string]any{ - "driver": "jwt", - }, - }, - }) -} diff --git a/config/cache.go b/config/cache.go deleted file mode 100644 index 286bd657..00000000 --- a/config/cache.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("cache", map[string]any{ - // Default Cache Store - // - // This option controls the default cache connection that gets used while - // using this caching library. This connection is used when another is - // not explicitly specified when executing a given caching function. - "default": config.Env("CACHE_STORE", "memory"), - - // Cache Stores - // - // Here you may define all the cache "stores" for your application as - // well as their drivers. You may even define multiple stores for the - // same cache driver to group types of items stored in your caches. - // Available Drivers: "memory", "custom" - "stores": map[string]any{ - "memory": map[string]any{ - "driver": "memory", - }, - }, - - // Cache Key Prefix - // - // When utilizing a RAM based store such as APC or Memcached, there might - // be other applications utilizing the same cache. So, we'll specify a - // value to get prefixed to all our keys, so we can avoid collisions. - // Must: a-zA-Z0-9_- - "prefix": "panel_cache", - }) -} diff --git a/config/config.example.yml b/config/config.example.yml new file mode 100644 index 00000000..ac0ba2ab --- /dev/null +++ b/config/config.example.yml @@ -0,0 +1,12 @@ +app: + key: "a-long-string-with-32-characters" + locale: "zh_CN" + +http: + debug: true + address: ":3000" + entrance: "/" + ssl: false + +database: + debug: true diff --git a/config/cors.go b/config/cors.go deleted file mode 100644 index 21428d77..00000000 --- a/config/cors.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("cors", map[string]any{ - // Cross-Origin Resource Sharing (CORS) Configuration - // - // Here you may configure your settings for cross-origin resource sharing - // or "CORS". This determines what cross-origin operations may execute - // in web browsers. You are free to adjust these settings as needed. - // - // To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS - "paths": []string{"1145141919810"}, // 避免框架使用CORS中间件 - "allowed_methods": []string{"*"}, - "allowed_origins": []string{"*"}, - "allowed_headers": []string{"“"}, - "exposed_headers": []string{""}, - "max_age": 0, - "supports_credentials": false, - }) -} diff --git a/config/database.go b/config/database.go deleted file mode 100644 index 86c5511d..00000000 --- a/config/database.go +++ /dev/null @@ -1,79 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("database", map[string]any{ - // Default database connection name - "default": "panel", - - // Database connections - "connections": map[string]any{ - "panel": map[string]any{ - "driver": "sqlite", - "database": config.Env("DB_FILE", path.Executable("storage/panel.db")), - "prefix": "", - "singular": false, // Table name is singular - }, - }, - - // Set pool configuration - "pool": map[string]any{ - // Sets the maximum number of connections in the idle - // connection pool. - // - // If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, - // then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. - // - // If n <= 0, no idle connections are retained. - "max_idle_conns": 10, - // Sets the maximum number of open connections to the database. - // - // If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than - // MaxIdleConns, then MaxIdleConns will be reduced to match the new - // MaxOpenConns limit. - // - // If n <= 0, then there is no limit on the number of open connections. - "max_open_conns": 100, - // Sets the maximum amount of time a connection may be idle. - // - // Expired connections may be closed lazily before reuse. - // - // If d <= 0, connections are not closed due to a connection's idle time. - // Unit: Second - "conn_max_idletime": 3600, - // Sets the maximum amount of time a connection may be reused. - // - // Expired connections may be closed lazily before reuse. - // - // If d <= 0, connections are not closed due to a connection's age. - // Unit: Second - "conn_max_lifetime": 3600, - }, - - // Migration Repository Table - // - // This table keeps track of all the migrations that have already run for - // your application. Using this information, we can determine which of - // the migrations on disk haven't actually been run in the database. - "migrations": "migrations", - - // Redis Databases - // - // Redis is an open source, fast, and advanced key-value store that also - // provides a richer body of commands than a typical key-value system - // such as APC or Memcached. - "redis": map[string]any{ - "default": map[string]any{ - "host": config.Env("REDIS_HOST", ""), - "password": config.Env("REDIS_PASSWORD", ""), - "port": config.Env("REDIS_PORT", 6379), - "database": config.Env("REDIS_DB", 0), - }, - }, - }) -} diff --git a/config/filesystems.go b/config/filesystems.go deleted file mode 100644 index 62f6b0c8..00000000 --- a/config/filesystems.go +++ /dev/null @@ -1,32 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("filesystems", map[string]any{ - // Default Filesystem Disk - // - // Here you may specify the default filesystem disk that should be used - // by the framework. The "local" disk, as well as a variety of cloud - // based disks are available to your application. Just store away! - "default": "local", - - // Filesystem Disks - // - // Here you may configure as many filesystem "disks" as you wish, and you - // may even configure multiple disks of the same driver. Defaults have - // been set up for each driver as an example of the required values. - // - // Supported Drivers: "local", "custom" - "disks": map[string]any{ - "local": map[string]any{ - "driver": "local", - "root": path.Executable("storage"), - }, - }, - }) -} diff --git a/config/hashing.go b/config/hashing.go deleted file mode 100644 index c7979bf4..00000000 --- a/config/hashing.go +++ /dev/null @@ -1,40 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("hashing", map[string]any{ - // Hashing Driver - // - // This option controls the default diver that gets used - // by the framework hash facade. - // Default driver is "bcrypt". - // - // Supported Drivers: "bcrypt", "argon2id" - "driver": "argon2id", - - // Bcrypt Hashing Options - // rounds: The cost factor that should be used to compute the bcrypt hash. - // The cost factor controls how much time is needed to compute a single bcrypt hash. - // The higher the cost factor, the more hashing rounds are done. Increasing the cost - // factor by 1 doubles the necessary time. After a certain point, the returns on - // hashing time versus attacker time are diminishing, so choose your cost factor wisely. - "bcrypt": map[string]any{ - "rounds": 12, - }, - - // Argon2id Hashing Options - // memory: A memory cost, which defines the memory usage, given in kibibytes. - // time: A time cost, which defines the amount of computation - // realized and therefore the execution time, given in number of iterations. - // threads: A parallelism degree, which defines the number of parallel threads. - "argon2id": map[string]any{ - "memory": 65536, - "time": 4, - "threads": 1, - }, - }) -} diff --git a/config/http.go b/config/http.go deleted file mode 100644 index 5eca9837..00000000 --- a/config/http.go +++ /dev/null @@ -1,47 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/contracts/route" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" - ginfacades "github.com/goravel/gin/facades" -) - -func init() { - config := facades.Config() - config.Add("http", map[string]any{ - // HTTP Driver - "default": "gin", - // HTTP Drivers - "drivers": map[string]any{ - "gin": map[string]any{ - // Optional, default is 4096 KB - "body_limit": 1024 * 1024 * 4, - "header_limit": 20480, - "route": func() (route.Route, error) { - return ginfacades.Route("gin"), nil - }, - }, - }, - // HTTP URL - "url": "http://localhost", - // HTTP Host - "host": "", - // HTTP Port - "port": config.Env("APP_PORT", "8888"), - // HTTPS Configuration - "tls": map[string]any{ - // HTTPS Host - "host": "", - // HTTPS Port - "port": config.Env("APP_PORT", "8888"), - // SSL Certificate - "ssl": map[string]any{ - // ca.pem - "cert": config.Env("APP_SSL_CERT", path.Executable("storage/ssl.crt")), - // ca.key - "key": config.Env("APP_SSL_KEY", path.Executable("storage/ssl.key")), - }, - }, - }) -} diff --git a/config/jwt.go b/config/jwt.go deleted file mode 100644 index f4a1031e..00000000 --- a/config/jwt.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("jwt", map[string]any{ - // JWT Authentication Secret - // - // Don't forget to set this in your panel.conf file, as it will be used to sign - // your tokens. A tools command is provided for this: - // `go run . artisan jwt:secret` - "secret": config.Env("JWT_SECRET", ""), - - // JWT time to live - // - // Specify the length of time (in minutes) that the token will be valid for. - // Defaults to 1 hour. - // - // You can also set this to 0, to yield a never expiring token. - // Some people may want this behaviour for e.g. a mobile app. - // This is not particularly recommended, so make sure you have appropriate - // systems in place to revoke the token if necessary. - "ttl": config.Env("JWT_TTL", 60), - - // Refresh time to live - // - // Specify the length of time (in minutes) that the token can be refreshed - // within. I.E. The user can refresh their token within a 2 week window of - // the original token being created until they must re-authenticate. - // Defaults to 2 weeks. - // - // You can also set this to 0, to yield an infinite refresh time. - // Some may want this instead of never expiring tokens for e.g. a mobile app. - // This is not particularly recommended, so make sure you have appropriate - // systems in place to revoke the token if necessary. - "refresh_ttl": config.Env("JWT_REFRESH_TTL", 20160), - }) -} diff --git a/config/logging.go b/config/logging.go deleted file mode 100644 index ed9829c1..00000000 --- a/config/logging.go +++ /dev/null @@ -1,50 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("logging", map[string]any{ - // Default Log Channel - // - // This option defines the default log channel that gets used when writing - // messages to the logs. The name specified in this option should match - // one of the channels defined in the "channels" configuration array. - "default": "stack", - - // Log Channels - // - // Here you may configure the log channels for your application. - // Available Drivers: "single", "daily", "custom", "stack" - // Available Level: "debug", "info", "warning", "error", "fatal", "panic" - "channels": map[string]any{ - "stack": map[string]any{ - "driver": "stack", - "channels": []string{"daily"}, - }, - "single": map[string]any{ - "driver": "single", - "path": path.Executable("storage/logs/panel.log"), - "level": "info", - "print": true, - }, - "daily": map[string]any{ - "driver": "daily", - "path": path.Executable("storage/logs/panel.log"), - "level": "info", - "days": 7, - "print": true, - }, - "http": map[string]any{ - "driver": "daily", - "path": path.Executable("storage/logs/http.log"), - "level": "info", - "days": 7, - "print": false, - }, - }, - }) -} diff --git a/config/mail.go b/config/mail.go deleted file mode 100644 index 0439cd60..00000000 --- a/config/mail.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -import "github.com/goravel/framework/facades" - -func init() { - config := facades.Config() - config.Add("mail", map[string]any{ - // SMTP Host Address - // - // Here you may provide the host address of the SMTP server used by your - // applications. A default option is provided that is compatible with - // the Mailgun mail service which will provide reliable deliveries. - "host": config.Env("MAIL_HOST", ""), - - // SMTP Host Port - // - // This is the SMTP port used by your application to deliver e-mails to - // users of the application. Like the host we have set this value to - // stay compatible with the Mailgun e-mail application by default. - "port": config.Env("MAIL_PORT", 587), - - // -------------------------------------------------------------------------- - // Global "From" Address - // -------------------------------------------------------------------------- - // - // You may wish for all e-mails sent by your application to be sent from - // the same address. Here, you may specify a name and address that is - // used globally for all e-mails that are sent by your application. - "from": map[string]any{ - "address": config.Env("MAIL_FROM_ADDRESS", "hello@example.com"), - "name": config.Env("MAIL_FROM_NAME", "Example"), - }, - - // SMTP Server Username - // - // If your SMTP server requires a username for authentication, you should - // set it here. This will get used to authenticate with your server on - // connection. You may also set the "password" value below this one. - "username": config.Env("MAIL_USERNAME"), - - "password": config.Env("MAIL_PASSWORD"), - }) -} diff --git a/config/panel.go b/config/panel.go deleted file mode 100644 index c9dc0a47..00000000 --- a/config/panel.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("panel", map[string]any{ - "name": "耗子面板", - "version": "v2.2.27", - "ssl": config.Env("APP_SSL", false), - "entrance": config.Env("APP_ENTRANCE", "/"), - }) -} diff --git a/config/queue.go b/config/queue.go deleted file mode 100644 index 27746d34..00000000 --- a/config/queue.go +++ /dev/null @@ -1,23 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("queue", map[string]any{ - // Default Queue Connection Name - "default": "async", - - // Queue Connections - // - // Here you may configure the connection information for each server that is used by your application. - // Drivers: "sync", "async", "custom" - "connections": map[string]any{ - "async": map[string]any{ - "driver": "async", - }, - }, - }) -} diff --git a/config/session.go b/config/session.go deleted file mode 100644 index 0a722d2c..00000000 --- a/config/session.go +++ /dev/null @@ -1,85 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("session", map[string]any{ - // Default Session Driver - // - // This option controls the default session "driver" that will be used on - // requests. By default, we will use the lightweight file session driver, but you - // may specify any of the other wonderful drivers provided here. - // - // Supported: "file" - "driver": config.Env("SESSION_DRIVER", "file"), - - // Session Lifetime - // - // Here you may specify the number of minutes that you wish the session - // to be allowed to remain idle before it expires. If you want them - // to immediately expire when the browser is closed, then you may - // indicate that via the expire_on_close configuration option. - "lifetime": config.Env("SESSION_LIFETIME", 120), - - "expire_on_close": config.Env("SESSION_EXPIRE_ON_CLOSE", false), - - // Session File Location - // - // When using the file session driver, we need a location where the - // session files may be stored. A default has been set for you, but a - // different location may be specified. This is only needed for file sessions. - "files": path.Executable("storage/framework/sessions"), - - // Session Sweeping Lottery - // - // Some session drivers must manually sweep their storage location to get - // rid of old sessions from storage. Here are the chances out of 100 that - // the sweeper will sweep the storage location. The default is 2 out of 100. - "lottery": []int{2, 100}, - - // Session Cookie Name - // - // Here you may change the name of the cookie used to identify a session - // in the application. The name specified here will get used every time - // a new session cookie is created by the framework for every driver. - "cookie": config.Env("SESSION_COOKIE", "panel_session"), - - // Session Cookie Path - // - // The session cookie path determines the path for which the cookie will - // be regarded as available.Typically, this will be the root path of - // your application, but you are free to change this when necessary. - "path": config.Env("SESSION_PATH", "/"), - - // Session Cookie Domain - // - // Here you may change the domain of the cookie used to identify a session - // in your application.This will determine which domains the cookie is - // available to in your application.A sensible default has been set. - "domain": config.Env("SESSION_DOMAIN", ""), - - // HTTPS Only Cookies - // - // By setting this option to true, session cookies will only be sent back - // to the server if the browser has an HTTPS connection. This will keep - // the cookie from being sent to you if it cannot be done securely. - "secure": config.Env("SESSION_SECURE", false), - - // HTTP Access Only - // - // Setting this to true will prevent JavaScript from accessing the value of - // the cookie, and the cookie will only be accessible through the HTTP protocol. - "http_only": config.Env("SESSION_HTTP_ONLY", true), - - // Same-Site Cookies - // - // This option determines how your cookies behave when cross-site requests - // take place, and can be used to mitigate CSRF attacks.By default, we - // will set this value to "lax" since this is a secure default value. - "same_site": config.Env("SESSION_SAME_SITE", "lax"), - }) -} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..719723f4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# docs + +docs 目录存放由 swag 命令生成的 Swagger 接口文档,你也可以放置其他文档。 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index ed3cbbe1..545233ca 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -21,99 +21,52 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/panel/cert/algorithms": { + "/info/checkUpdate": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的算法列表", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取算法列表", + "summary": "检查更新", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/caProviders": { + "/info/countInfo": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的 CA 提供商", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取 CA 提供商", + "summary": "统计信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs": { + "/info/homePlugins": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加证书到面板证书管理", "consumes": [ "application/json" ], @@ -121,82 +74,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertStore" - } - } + "信息服务" ], + "summary": "首页插件", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs/{id}": { + "/info/installedDbAndPhp": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.Cert" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的证书", "consumes": [ "application/json" ], @@ -204,154 +96,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertUpdate" - } - } + "信息服务" ], + "summary": "已安装的数据库和PHP", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/deploy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "部署面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "部署证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertDeploy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/dns": { + "/info/nowMonitor": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 DNS 接口到面板证书管理", "consumes": [ "application/json" ], @@ -359,82 +118,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 DNS 接口", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSStore" - } - } + "信息服务" ], + "summary": "实时监控", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dns/{id}": { + "/info/panel": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertDNS" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 DNS 接口", "consumes": [ "application/json" ], @@ -442,105 +140,43 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSUpdate" - } - } + "信息服务" ], + "summary": "面板信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 DNS 接口", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dnsProviders": { + "/info/restart": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "信息服务" + ], + "summary": "重启面板", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/info/systemInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理支持的 DNS 提供商", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 提供商", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/manualDNS": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取签发证书所需的 DNS 记录", "consumes": [ "application/json" ], @@ -548,53 +184,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "获取手动 DNS 记录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "系统信息", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/acme.DNSRecord" - } - } - } - } - ] + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/obtain": { + "/info/update": { "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "签发面板证书管理的证书", "consumes": [ "application/json" ], @@ -602,112 +206,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "签发证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "更新面板", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/renew": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "续签面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "续签证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Renew" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/users": { + "/info/updateInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取用户列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 ACME 用户到面板证书管理", "consumes": [ "application/json" ], @@ -715,82 +228,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 ACME 用户", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserStore" - } - } + "信息服务" ], + "summary": "版本更新信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/users/{id}": { + "/user/info/{id}": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertUser" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 ACME 用户", "consumes": [ "application/json" ], @@ -798,2461 +250,42 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 ACME 用户", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个容器是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查容器是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个镜像是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查镜像是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个镜像的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看镜像", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有镜像列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取镜像列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理镜像", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/pull": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "拉取一个镜像", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "拉取镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ImagePull" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/kill": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "杀死一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "杀死容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有容器列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取容器列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/logs": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的日志", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器日志", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/connect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "连接一个容器到一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "连接容器到网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/disconnect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "从一个网络断开一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "从网络断开容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个网络是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查网络是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个网络的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看网络", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有网络列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取网络列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理网络", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理容器", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/rename": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重命名一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重命名容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerRename" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重启一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重启容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/search": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "根据容器名称搜索容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "搜索容器", - "parameters": [ - { - "type": "string", - "description": "容器名称", - "name": "name", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "启动一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "启动容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stats": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的状态信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器状态", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "停止一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "停止容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/unpause": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "取消暂停一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "取消暂停容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个卷", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.VolumeCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个卷是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查卷是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个卷的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看卷", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有卷列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取卷列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理卷", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/archive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "压缩文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "压缩文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Archive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/content": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件内容", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/copy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "复制文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "复制文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Copy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "创建文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "删除文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Exist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/download": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载给定路径的文件", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载文件", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件/目录信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录信息", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/list": { - "get": { - "description": "获取给定路径的文件/目录列表", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录列表", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - }, - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/move": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "移动文件/目录到给定路径,等效于重命名", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "移动文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Move" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/permission": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "修改给定路径的文件/目录权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "修改文件/目录权限", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Permission" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/remoteDownload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载远程文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载远程文件", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/save": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "保存给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "保存文件内容", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Save" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/search": { - "post": { - "description": "通过关键词搜索给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "搜索文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Search" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/unArchive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "解压文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "解压文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UnArchive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/upload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "上传文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "上传文件", - "parameters": [ - { - "type": "file", - "description": "file", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "path", - "name": "path", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/install": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "安装插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/isInstalled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "检查插件是否已安装", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "插件列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/uninstall": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "卸载插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/updateShow": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件首页显示状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - }, - { - "type": "boolean", - "description": "request", - "name": "show", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/https": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "获取面板 HTTPS 设置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新面板 HTTPS 设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Https" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "设置列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_setting.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/disable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "禁用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/enable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/isEnabled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "是否启用服务", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/reload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重载服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重启服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启动服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/status": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "服务状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "停止服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/user/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户鉴权" + "用户服务" ], "summary": "用户信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/login": { + "/user/isLogin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户服务" + ], + "summary": "是否登录", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/user/login": { "post": { "consumes": [ "application/json" @@ -3261,7 +294,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登录", "parameters": [ @@ -3271,7 +304,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.Login" + "$ref": "#/definitions/request.UserLogin" } } ], @@ -3279,1725 +312,56 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - }, - "403": { - "description": "用户名或密码错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" - } - }, - "500": { - "description": "系统内部错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/logout": { + "/user/logout": { "post": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登出", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } - }, - "/panel/website/backupList": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站备份列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/types.BackupFile" - } - } - } - } - ] - } - } - } - } - }, - "/panel/website/defaultConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取默认配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存默认配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/deleteBackup": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站备份", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DeleteBackup" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/uploadBackup": { - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "上传网站备份", - "parameters": [ - { - "type": "file", - "description": "备份文件", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "添加网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Add" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Delete" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/types.WebsiteAdd" - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.SaveConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/createBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "创建网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/log": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "清空网站日志", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/resetConfig": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "重置网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/restoreBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "还原网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/status": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站状态", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/updateRemark": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "更新网站备注", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/frp/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "string", - "description": "服务", - "name": "service", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/gitea/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "清空错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "保存配置", - "parameters": [ - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取负载状态", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearSlowLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/extensions": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取扩展列表", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "安装扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "卸载扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/fpmConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取负载状态", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/slowLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/registryConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取注册表配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新注册表配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateRegistryConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/storageConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取存储配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新存储配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateStorageConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "列出所有 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "列出模块", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "添加模块", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Create" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules/{name}": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "删除模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/swagger": { - "get": { - "description": "Swagger UI", - "tags": [ - "Swagger" - ], - "summary": "Swagger UI", - "responses": { - "200": { - "description": "OK" - }, - "500": { - "description": "Internal Server Error" - } - } - } } }, "definitions": { - "acme.DNSParam": { + "request.UserLogin": { "type": "object", + "required": [ + "password", + "username" + ], "properties": { - "access_key": { - "type": "string" + "password": { + "type": "string", + "maxLength": 255, + "minLength": 6 }, - "api_key": { - "type": "string" - }, - "id": { - "type": "string" - }, - "secret_key": { - "type": "string" - }, - "token": { - "type": "string" + "username": { + "type": "string", + "maxLength": 255, + "minLength": 3 } } }, - "acme.DNSRecord": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "controllers.ErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "controllers.SuccessResponse": { + "service.SuccessResponse": { "type": "object", "properties": { "data": {}, @@ -5005,1009 +369,6 @@ const docTemplate = `{ "type": "string" } } - }, - "github_com_TheTNB_panel_app_http_requests_container.ID": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - }, - "service": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_setting.Update": { - "type": "object", - "properties": { - "backup_path": { - "type": "string" - }, - "email": { - "type": "string" - }, - "entrance": { - "type": "string" - }, - "language": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "website_path": { - "type": "string" - } - } - }, - "github_com_goravel_framework_support_carbon.DateTime": { - "type": "object", - "properties": { - "error": {} - } - }, - "models.Cert": { - "type": "object", - "properties": { - "auto_renew": { - "description": "自动续签", - "type": "boolean" - }, - "cert": { - "description": "证书内容", - "type": "string" - }, - "cert_url": { - "description": "证书 URL (续签时使用)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns": { - "$ref": "#/definitions/models.CertDNS" - }, - "dns_id": { - "description": "关联的 DNS ID", - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "key": { - "description": "私钥内容", - "type": "string" - }, - "type": { - "description": "证书类型 (P256, P384, 2048, 4096)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "user": { - "$ref": "#/definitions/models.CertUser" - }, - "user_id": { - "description": "关联的 ACME 用户 ID", - "type": "integer" - }, - "website": { - "$ref": "#/definitions/models.Website" - }, - "website_id": { - "description": "关联的网站 ID", - "type": "integer" - } - } - }, - "models.CertDNS": { - "type": "object", - "properties": { - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns_param": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "description": "备注名称", - "type": "string" - }, - "type": { - "description": "DNS 提供商 (dnspod, tencent, aliyun, cloudflare)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.CertUser": { - "type": "object", - "properties": { - "ca": { - "description": "CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - }, - "private_key": { - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.Website": { - "type": "object", - "properties": { - "cert": { - "$ref": "#/definitions/models.Cert" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "requests.Add": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "requests.Archive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "paths": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "requests.CertDeploy": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertStore": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertUpdate": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.ContainerCreate": { - "type": "object", - "properties": { - "auto_remove": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "cpu_shares": { - "type": "integer" - }, - "cpus": { - "type": "integer" - }, - "entrypoint": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "image": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "memory": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "network": { - "type": "string" - }, - "open_stdin": { - "type": "boolean" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerPort" - } - }, - "privileged": { - "type": "boolean" - }, - "publish_all_ports": { - "type": "boolean" - }, - "restart_policy": { - "type": "string" - }, - "tty": { - "type": "boolean" - }, - "volumes": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerVolume" - } - } - } - }, - "requests.ContainerRename": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "requests.Copy": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.Create": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "requests.DNSStore": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.DNSUpdate": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.Delete": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "path": { - "type": "boolean" - } - } - }, - "requests.DeleteBackup": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - }, - "requests.Exist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Https": { - "type": "object", - "properties": { - "cert": { - "type": "string" - }, - "https": { - "type": "boolean" - }, - "key": { - "type": "string" - } - } - }, - "requests.ImagePull": { - "type": "object", - "properties": { - "auth": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Login": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Move": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.NetworkConnectDisConnect": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "network": { - "type": "string" - } - } - }, - "requests.NetworkCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "ipv4": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "ipv6": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "requests.NotExist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Obtain": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Permission": { - "type": "object", - "properties": { - "group": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.Renew": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Save": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.SaveConfig": { - "type": "object", - "properties": { - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "hsts": { - "type": "boolean" - }, - "http_redirect": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "index": { - "type": "string" - }, - "open_basedir": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "raw": { - "type": "string" - }, - "rewrite": { - "type": "string" - }, - "root": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "ssl_certificate": { - "type": "string" - }, - "ssl_certificate_key": { - "type": "string" - }, - "tls_ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "waf": { - "type": "boolean" - }, - "waf_cache": { - "type": "string" - }, - "waf_cc_deny": { - "type": "string" - }, - "waf_mode": { - "type": "string" - } - } - }, - "requests.Search": { - "type": "object", - "properties": { - "keyword": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UnArchive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UpdateRegistryConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UpdateStorageConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UserStore": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.UserUpdate": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.VolumeCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "types.BackupFile": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "string" - } - } - }, - "types.ContainerNetwork": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "gateway": { - "type": "string" - }, - "ip_range": { - "type": "string" - }, - "subnet": { - "type": "string" - } - } - }, - "types.ContainerPort": { - "type": "object", - "properties": { - "container_end": { - "type": "integer" - }, - "container_start": { - "type": "integer" - }, - "host": { - "type": "string" - }, - "host_end": { - "type": "integer" - }, - "host_start": { - "type": "integer" - }, - "protocol": { - "type": "string" - } - } - }, - "types.ContainerVolume": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "host": { - "type": "string" - }, - "mode": { - "type": "string" - } - } - }, - "types.KV": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "types.WebsiteAdd": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - } - } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index bc9f45d9..5dbe23f4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -14,99 +14,52 @@ }, "basePath": "/api", "paths": { - "/panel/cert/algorithms": { + "/info/checkUpdate": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的算法列表", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取算法列表", + "summary": "检查更新", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/caProviders": { + "/info/countInfo": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的 CA 提供商", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取 CA 提供商", + "summary": "统计信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs": { + "/info/homePlugins": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加证书到面板证书管理", "consumes": [ "application/json" ], @@ -114,82 +67,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertStore" - } - } + "信息服务" ], + "summary": "首页插件", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs/{id}": { + "/info/installedDbAndPhp": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.Cert" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的证书", "consumes": [ "application/json" ], @@ -197,154 +89,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertUpdate" - } - } + "信息服务" ], + "summary": "已安装的数据库和PHP", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/deploy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "部署面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "部署证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertDeploy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/dns": { + "/info/nowMonitor": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 DNS 接口到面板证书管理", "consumes": [ "application/json" ], @@ -352,82 +111,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 DNS 接口", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSStore" - } - } + "信息服务" ], + "summary": "实时监控", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dns/{id}": { + "/info/panel": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertDNS" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 DNS 接口", "consumes": [ "application/json" ], @@ -435,105 +133,43 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSUpdate" - } - } + "信息服务" ], + "summary": "面板信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 DNS 接口", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dnsProviders": { + "/info/restart": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "信息服务" + ], + "summary": "重启面板", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/info/systemInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理支持的 DNS 提供商", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 提供商", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/manualDNS": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取签发证书所需的 DNS 记录", "consumes": [ "application/json" ], @@ -541,53 +177,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "获取手动 DNS 记录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "系统信息", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/acme.DNSRecord" - } - } - } - } - ] + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/obtain": { + "/info/update": { "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "签发面板证书管理的证书", "consumes": [ "application/json" ], @@ -595,112 +199,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "签发证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "更新面板", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/renew": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "续签面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "续签证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Renew" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/users": { + "/info/updateInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取用户列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 ACME 用户到面板证书管理", "consumes": [ "application/json" ], @@ -708,82 +221,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 ACME 用户", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserStore" - } - } + "信息服务" ], + "summary": "版本更新信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/users/{id}": { + "/user/info/{id}": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertUser" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 ACME 用户", "consumes": [ "application/json" ], @@ -791,2461 +243,42 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 ACME 用户", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个容器是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查容器是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个镜像是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查镜像是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个镜像的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看镜像", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有镜像列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取镜像列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理镜像", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/pull": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "拉取一个镜像", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "拉取镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ImagePull" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/kill": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "杀死一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "杀死容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有容器列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取容器列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/logs": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的日志", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器日志", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/connect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "连接一个容器到一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "连接容器到网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/disconnect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "从一个网络断开一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "从网络断开容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个网络是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查网络是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个网络的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看网络", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有网络列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取网络列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理网络", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理容器", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/rename": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重命名一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重命名容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerRename" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重启一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重启容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/search": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "根据容器名称搜索容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "搜索容器", - "parameters": [ - { - "type": "string", - "description": "容器名称", - "name": "name", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "启动一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "启动容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stats": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的状态信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器状态", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "停止一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "停止容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/unpause": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "取消暂停一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "取消暂停容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个卷", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.VolumeCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个卷是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查卷是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个卷的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看卷", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有卷列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取卷列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理卷", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/archive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "压缩文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "压缩文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Archive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/content": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件内容", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/copy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "复制文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "复制文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Copy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "创建文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "删除文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Exist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/download": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载给定路径的文件", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载文件", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件/目录信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录信息", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/list": { - "get": { - "description": "获取给定路径的文件/目录列表", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录列表", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - }, - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/move": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "移动文件/目录到给定路径,等效于重命名", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "移动文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Move" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/permission": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "修改给定路径的文件/目录权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "修改文件/目录权限", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Permission" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/remoteDownload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载远程文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载远程文件", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/save": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "保存给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "保存文件内容", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Save" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/search": { - "post": { - "description": "通过关键词搜索给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "搜索文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Search" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/unArchive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "解压文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "解压文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UnArchive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/upload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "上传文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "上传文件", - "parameters": [ - { - "type": "file", - "description": "file", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "path", - "name": "path", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/install": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "安装插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/isInstalled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "检查插件是否已安装", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "插件列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/uninstall": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "卸载插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/updateShow": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件首页显示状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - }, - { - "type": "boolean", - "description": "request", - "name": "show", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/https": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "获取面板 HTTPS 设置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新面板 HTTPS 设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Https" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "设置列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_setting.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/disable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "禁用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/enable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/isEnabled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "是否启用服务", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/reload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重载服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重启服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启动服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/status": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "服务状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "停止服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/user/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户鉴权" + "用户服务" ], "summary": "用户信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/login": { + "/user/isLogin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户服务" + ], + "summary": "是否登录", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/user/login": { "post": { "consumes": [ "application/json" @@ -3254,7 +287,7 @@ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登录", "parameters": [ @@ -3264,7 +297,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.Login" + "$ref": "#/definitions/request.UserLogin" } } ], @@ -3272,1725 +305,56 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - }, - "403": { - "description": "用户名或密码错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" - } - }, - "500": { - "description": "系统内部错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/logout": { + "/user/logout": { "post": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登出", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } - }, - "/panel/website/backupList": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站备份列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/types.BackupFile" - } - } - } - } - ] - } - } - } - } - }, - "/panel/website/defaultConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取默认配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存默认配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/deleteBackup": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站备份", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DeleteBackup" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/uploadBackup": { - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "上传网站备份", - "parameters": [ - { - "type": "file", - "description": "备份文件", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "添加网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Add" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Delete" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/types.WebsiteAdd" - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.SaveConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/createBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "创建网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/log": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "清空网站日志", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/resetConfig": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "重置网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/restoreBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "还原网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/status": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站状态", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/updateRemark": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "更新网站备注", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/frp/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "string", - "description": "服务", - "name": "service", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/gitea/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "清空错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "保存配置", - "parameters": [ - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取负载状态", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearSlowLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/extensions": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取扩展列表", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "安装扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "卸载扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/fpmConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取负载状态", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/slowLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/registryConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取注册表配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新注册表配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateRegistryConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/storageConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取存储配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新存储配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateStorageConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "列出所有 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "列出模块", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "添加模块", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Create" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules/{name}": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "删除模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/swagger": { - "get": { - "description": "Swagger UI", - "tags": [ - "Swagger" - ], - "summary": "Swagger UI", - "responses": { - "200": { - "description": "OK" - }, - "500": { - "description": "Internal Server Error" - } - } - } } }, "definitions": { - "acme.DNSParam": { + "request.UserLogin": { "type": "object", + "required": [ + "password", + "username" + ], "properties": { - "access_key": { - "type": "string" + "password": { + "type": "string", + "maxLength": 255, + "minLength": 6 }, - "api_key": { - "type": "string" - }, - "id": { - "type": "string" - }, - "secret_key": { - "type": "string" - }, - "token": { - "type": "string" + "username": { + "type": "string", + "maxLength": 255, + "minLength": 3 } } }, - "acme.DNSRecord": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "controllers.ErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "controllers.SuccessResponse": { + "service.SuccessResponse": { "type": "object", "properties": { "data": {}, @@ -4998,1009 +362,6 @@ "type": "string" } } - }, - "github_com_TheTNB_panel_app_http_requests_container.ID": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - }, - "service": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_setting.Update": { - "type": "object", - "properties": { - "backup_path": { - "type": "string" - }, - "email": { - "type": "string" - }, - "entrance": { - "type": "string" - }, - "language": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "website_path": { - "type": "string" - } - } - }, - "github_com_goravel_framework_support_carbon.DateTime": { - "type": "object", - "properties": { - "error": {} - } - }, - "models.Cert": { - "type": "object", - "properties": { - "auto_renew": { - "description": "自动续签", - "type": "boolean" - }, - "cert": { - "description": "证书内容", - "type": "string" - }, - "cert_url": { - "description": "证书 URL (续签时使用)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns": { - "$ref": "#/definitions/models.CertDNS" - }, - "dns_id": { - "description": "关联的 DNS ID", - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "key": { - "description": "私钥内容", - "type": "string" - }, - "type": { - "description": "证书类型 (P256, P384, 2048, 4096)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "user": { - "$ref": "#/definitions/models.CertUser" - }, - "user_id": { - "description": "关联的 ACME 用户 ID", - "type": "integer" - }, - "website": { - "$ref": "#/definitions/models.Website" - }, - "website_id": { - "description": "关联的网站 ID", - "type": "integer" - } - } - }, - "models.CertDNS": { - "type": "object", - "properties": { - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns_param": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "description": "备注名称", - "type": "string" - }, - "type": { - "description": "DNS 提供商 (dnspod, tencent, aliyun, cloudflare)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.CertUser": { - "type": "object", - "properties": { - "ca": { - "description": "CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - }, - "private_key": { - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.Website": { - "type": "object", - "properties": { - "cert": { - "$ref": "#/definitions/models.Cert" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "requests.Add": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "requests.Archive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "paths": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "requests.CertDeploy": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertStore": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertUpdate": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.ContainerCreate": { - "type": "object", - "properties": { - "auto_remove": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "cpu_shares": { - "type": "integer" - }, - "cpus": { - "type": "integer" - }, - "entrypoint": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "image": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "memory": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "network": { - "type": "string" - }, - "open_stdin": { - "type": "boolean" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerPort" - } - }, - "privileged": { - "type": "boolean" - }, - "publish_all_ports": { - "type": "boolean" - }, - "restart_policy": { - "type": "string" - }, - "tty": { - "type": "boolean" - }, - "volumes": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerVolume" - } - } - } - }, - "requests.ContainerRename": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "requests.Copy": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.Create": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "requests.DNSStore": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.DNSUpdate": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.Delete": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "path": { - "type": "boolean" - } - } - }, - "requests.DeleteBackup": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - }, - "requests.Exist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Https": { - "type": "object", - "properties": { - "cert": { - "type": "string" - }, - "https": { - "type": "boolean" - }, - "key": { - "type": "string" - } - } - }, - "requests.ImagePull": { - "type": "object", - "properties": { - "auth": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Login": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Move": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.NetworkConnectDisConnect": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "network": { - "type": "string" - } - } - }, - "requests.NetworkCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "ipv4": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "ipv6": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "requests.NotExist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Obtain": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Permission": { - "type": "object", - "properties": { - "group": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.Renew": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Save": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.SaveConfig": { - "type": "object", - "properties": { - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "hsts": { - "type": "boolean" - }, - "http_redirect": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "index": { - "type": "string" - }, - "open_basedir": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "raw": { - "type": "string" - }, - "rewrite": { - "type": "string" - }, - "root": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "ssl_certificate": { - "type": "string" - }, - "ssl_certificate_key": { - "type": "string" - }, - "tls_ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "waf": { - "type": "boolean" - }, - "waf_cache": { - "type": "string" - }, - "waf_cc_deny": { - "type": "string" - }, - "waf_mode": { - "type": "string" - } - } - }, - "requests.Search": { - "type": "object", - "properties": { - "keyword": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UnArchive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UpdateRegistryConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UpdateStorageConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UserStore": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.UserUpdate": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.VolumeCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "types.BackupFile": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "string" - } - } - }, - "types.ContainerNetwork": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "gateway": { - "type": "string" - }, - "ip_range": { - "type": "string" - }, - "subnet": { - "type": "string" - } - } - }, - "types.ContainerPort": { - "type": "object", - "properties": { - "container_end": { - "type": "integer" - }, - "container_start": { - "type": "integer" - }, - "host": { - "type": "string" - }, - "host_end": { - "type": "integer" - }, - "host_start": { - "type": "integer" - }, - "protocol": { - "type": "string" - } - } - }, - "types.ContainerVolume": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "host": { - "type": "string" - }, - "mode": { - "type": "string" - } - } - }, - "types.KV": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "types.WebsiteAdd": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - } - } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4cd2f3e9..56d9c8be 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,692 +1,25 @@ basePath: /api definitions: - acme.DNSParam: + request.UserLogin: properties: - access_key: + password: + maxLength: 255 + minLength: 6 type: string - api_key: - type: string - id: - type: string - secret_key: - type: string - token: + username: + maxLength: 255 + minLength: 3 type: string + required: + - password + - username type: object - acme.DNSRecord: - properties: - key: - type: string - value: - type: string - type: object - controllers.ErrorResponse: - properties: - message: - type: string - type: object - controllers.SuccessResponse: + service.SuccessResponse: properties: data: {} message: type: string type: object - github_com_TheTNB_panel_app_http_requests_container.ID: - properties: - id: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig: - properties: - config: - type: string - service: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig: - properties: - config: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update: - properties: - auth_user: - type: string - comment: - type: string - hosts_allow: - type: string - name: - type: string - path: - type: string - secret: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig: - properties: - config: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_setting.Update: - properties: - backup_path: - type: string - email: - type: string - entrance: - type: string - language: - type: string - name: - type: string - password: - type: string - port: - type: integer - username: - type: string - website_path: - type: string - type: object - github_com_goravel_framework_support_carbon.DateTime: - properties: - error: {} - type: object - models.Cert: - properties: - auto_renew: - description: 自动续签 - type: boolean - cert: - description: 证书内容 - type: string - cert_url: - description: 证书 URL (续签时使用) - type: string - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - dns: - $ref: '#/definitions/models.CertDNS' - dns_id: - description: 关联的 DNS ID - type: integer - domains: - items: - type: string - type: array - id: - type: integer - key: - description: 私钥内容 - type: string - type: - description: 证书类型 (P256, P384, 2048, 4096) - type: string - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - user: - $ref: '#/definitions/models.CertUser' - user_id: - description: 关联的 ACME 用户 ID - type: integer - website: - $ref: '#/definitions/models.Website' - website_id: - description: 关联的网站 ID - type: integer - type: object - models.CertDNS: - properties: - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - dns_param: - $ref: '#/definitions/acme.DNSParam' - id: - type: integer - name: - description: 备注名称 - type: string - type: - description: DNS 提供商 (dnspod, tencent, aliyun, cloudflare) - type: string - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - type: object - models.CertUser: - properties: - ca: - description: CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) - type: string - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - email: - type: string - hmac_encoded: - type: string - id: - type: integer - key_type: - type: string - kid: - type: string - private_key: - type: string - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - type: object - models.Website: - properties: - cert: - $ref: '#/definitions/models.Cert' - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - id: - type: integer - name: - type: string - path: - type: string - php: - type: integer - remark: - type: string - ssl: - type: boolean - status: - type: boolean - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - type: object - requests.Add: - properties: - db: - type: boolean - db_name: - type: string - db_password: - type: string - db_type: - type: string - db_user: - type: string - domains: - items: - type: string - type: array - name: - type: string - path: - type: string - php: - type: string - ports: - items: - type: integer - type: array - type: object - requests.Archive: - properties: - file: - type: string - paths: - items: - type: string - type: array - type: object - requests.CertDeploy: - properties: - id: - type: integer - website_id: - type: integer - type: object - requests.CertStore: - properties: - auto_renew: - type: boolean - dns_id: - type: integer - domains: - items: - type: string - type: array - type: - type: string - user_id: - type: integer - website_id: - type: integer - type: object - requests.CertUpdate: - properties: - auto_renew: - type: boolean - dns_id: - type: integer - domains: - items: - type: string - type: array - id: - type: integer - type: - type: string - user_id: - type: integer - website_id: - type: integer - type: object - requests.ContainerCreate: - properties: - auto_remove: - type: boolean - command: - items: - type: string - type: array - cpu_shares: - type: integer - cpus: - type: integer - entrypoint: - items: - type: string - type: array - env: - items: - $ref: '#/definitions/types.KV' - type: array - image: - type: string - labels: - items: - $ref: '#/definitions/types.KV' - type: array - memory: - type: integer - name: - type: string - network: - type: string - open_stdin: - type: boolean - ports: - items: - $ref: '#/definitions/types.ContainerPort' - type: array - privileged: - type: boolean - publish_all_ports: - type: boolean - restart_policy: - type: string - tty: - type: boolean - volumes: - items: - $ref: '#/definitions/types.ContainerVolume' - type: array - type: object - requests.ContainerRename: - properties: - id: - type: string - name: - type: string - type: object - requests.Copy: - properties: - source: - type: string - target: - type: string - type: object - requests.Create: - properties: - auth_user: - type: string - comment: - type: string - hosts_allow: - type: string - name: - type: string - path: - type: string - secret: - type: string - type: object - requests.DNSStore: - properties: - data: - $ref: '#/definitions/acme.DNSParam' - name: - type: string - type: - type: string - type: object - requests.DNSUpdate: - properties: - data: - $ref: '#/definitions/acme.DNSParam' - id: - type: integer - name: - type: string - type: - type: string - type: object - requests.Delete: - properties: - db: - type: boolean - id: - type: integer - path: - type: boolean - type: object - requests.DeleteBackup: - properties: - name: - type: string - type: object - requests.Exist: - properties: - path: - type: string - type: object - requests.Https: - properties: - cert: - type: string - https: - type: boolean - key: - type: string - type: object - requests.ImagePull: - properties: - auth: - type: boolean - name: - type: string - password: - type: string - username: - type: string - type: object - requests.Login: - properties: - password: - type: string - username: - type: string - type: object - requests.Move: - properties: - source: - type: string - target: - type: string - type: object - requests.NetworkConnectDisConnect: - properties: - container: - type: string - network: - type: string - type: object - requests.NetworkCreate: - properties: - driver: - type: string - ipv4: - $ref: '#/definitions/types.ContainerNetwork' - ipv6: - $ref: '#/definitions/types.ContainerNetwork' - labels: - items: - $ref: '#/definitions/types.KV' - type: array - name: - type: string - options: - items: - $ref: '#/definitions/types.KV' - type: array - type: object - requests.NotExist: - properties: - path: - type: string - type: object - requests.Obtain: - properties: - id: - type: integer - type: object - requests.Permission: - properties: - group: - type: string - mode: - type: string - owner: - type: string - path: - type: string - type: object - requests.Renew: - properties: - id: - type: integer - type: object - requests.Save: - properties: - content: - type: string - path: - type: string - type: object - requests.SaveConfig: - properties: - domains: - items: - type: string - type: array - hsts: - type: boolean - http_redirect: - type: boolean - id: - type: integer - index: - type: string - open_basedir: - type: boolean - path: - type: string - php: - type: integer - ports: - items: - type: integer - type: array - raw: - type: string - rewrite: - type: string - root: - type: string - ssl: - type: boolean - ssl_certificate: - type: string - ssl_certificate_key: - type: string - tls_ports: - items: - type: integer - type: array - waf: - type: boolean - waf_cache: - type: string - waf_cc_deny: - type: string - waf_mode: - type: string - type: object - requests.Search: - properties: - keyword: - type: string - path: - type: string - type: object - requests.UnArchive: - properties: - file: - type: string - path: - type: string - type: object - requests.UpdateRegistryConfig: - properties: - config: - type: string - type: object - requests.UpdateStorageConfig: - properties: - config: - type: string - type: object - requests.UserStore: - properties: - ca: - type: string - email: - type: string - hmac_encoded: - type: string - key_type: - type: string - kid: - type: string - type: object - requests.UserUpdate: - properties: - ca: - type: string - email: - type: string - hmac_encoded: - type: string - id: - type: integer - key_type: - type: string - kid: - type: string - type: object - requests.VolumeCreate: - properties: - driver: - type: string - labels: - items: - $ref: '#/definitions/types.KV' - type: array - name: - type: string - options: - items: - $ref: '#/definitions/types.KV' - type: array - type: object - types.BackupFile: - properties: - name: - type: string - size: - type: string - type: object - types.ContainerNetwork: - properties: - enabled: - type: boolean - gateway: - type: string - ip_range: - type: string - subnet: - type: string - type: object - types.ContainerPort: - properties: - container_end: - type: integer - container_start: - type: integer - host: - type: string - host_end: - type: integer - host_start: - type: integer - protocol: - type: string - type: object - types.ContainerVolume: - properties: - container: - type: string - host: - type: string - mode: - type: string - type: object - types.KV: - properties: - key: - type: string - value: - type: string - type: object - types.WebsiteAdd: - properties: - db: - type: boolean - db_name: - type: string - db_password: - type: string - db_type: - type: string - db_user: - type: string - domains: - items: - type: string - type: array - name: - type: string - path: - type: string - php: - type: string - ports: - items: - type: integer - type: array - remark: - type: string - ssl: - type: boolean - status: - type: boolean - type: object info: contact: email: admin@haozi.net @@ -697,1967 +30,175 @@ info: title: 耗子面板 API version: "2" paths: - /panel/cert/algorithms: - get: - description: 获取面板证书管理支持的算法列表 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取算法列表 - tags: - - TLS证书 - /panel/cert/caProviders: - get: - description: 获取面板证书管理支持的 CA 提供商 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 CA 提供商 - tags: - - TLS证书 - /panel/cert/certs: - get: - description: 获取面板证书管理的证书列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取证书列表 - tags: - - TLS证书 - post: - consumes: - - application/json - description: 添加证书到面板证书管理 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.CertStore' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加证书 - tags: - - TLS证书 - /panel/cert/certs/{id}: - delete: - consumes: - - application/json - description: 删除面板证书管理的证书 - parameters: - - description: 证书 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除证书 - tags: - - TLS证书 - get: - description: 获取面板证书管理的证书 - parameters: - - description: 证书 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/models.Cert' - type: object - security: - - BearerToken: [] - summary: 获取证书 - tags: - - TLS证书 - put: - consumes: - - application/json - description: 更新面板证书管理的证书 - parameters: - - description: 证书 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.CertUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新证书 - tags: - - TLS证书 - /panel/cert/deploy: - post: - consumes: - - application/json - description: 部署面板证书管理的证书 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.CertDeploy' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 部署证书 - tags: - - TLS证书 - /panel/cert/dns: - get: - description: 获取面板证书管理的 DNS 接口列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 DNS 接口列表 - tags: - - TLS证书 - post: - consumes: - - application/json - description: 添加 DNS 接口到面板证书管理 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.DNSStore' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加 DNS 接口 - tags: - - TLS证书 - /panel/cert/dns/{id}: - delete: - consumes: - - application/json - description: 删除面板证书管理的 DNS 接口 - parameters: - - description: DNS 接口 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除 DNS 接口 - tags: - - TLS证书 - get: - description: 获取面板证书管理的 DNS 接口 - parameters: - - description: DNS 接口 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/models.CertDNS' - type: object - security: - - BearerToken: [] - summary: 获取 DNS 接口 - tags: - - TLS证书 - put: - consumes: - - application/json - description: 更新面板证书管理的 DNS 接口 - parameters: - - description: DNS 接口 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.DNSUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新 DNS 接口 - tags: - - TLS证书 - /panel/cert/dnsProviders: - get: - description: 获取面板证书管理支持的 DNS 提供商 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 DNS 提供商 - tags: - - TLS证书 - /panel/cert/manualDNS: - post: - consumes: - - application/json - description: 获取签发证书所需的 DNS 记录 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Obtain' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - items: - $ref: '#/definitions/acme.DNSRecord' - type: array - type: object - security: - - BearerToken: [] - summary: 获取手动 DNS 记录 - tags: - - TLS证书 - /panel/cert/obtain: - post: - consumes: - - application/json - description: 签发面板证书管理的证书 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Obtain' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 签发证书 - tags: - - TLS证书 - /panel/cert/renew: - post: - consumes: - - application/json - description: 续签面板证书管理的证书 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Renew' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 续签证书 - tags: - - TLS证书 - /panel/cert/users: - get: - description: 获取面板证书管理的 ACME 用户列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取用户列表 - tags: - - TLS证书 - post: - consumes: - - application/json - description: 添加 ACME 用户到面板证书管理 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UserStore' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加 ACME 用户 - tags: - - TLS证书 - /panel/cert/users/{id}: - delete: - consumes: - - application/json - description: 删除面板证书管理的 ACME 用户 - parameters: - - description: 用户 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除 ACME 用户 - tags: - - TLS证书 - get: - description: 获取面板证书管理的 ACME 用户 - parameters: - - description: 用户 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/models.CertUser' - type: object - security: - - BearerToken: [] - summary: 获取 ACME 用户 - tags: - - TLS证书 - put: - consumes: - - application/json - description: 更新面板证书管理的 ACME 用户 - parameters: - - description: 用户 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UserUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新 ACME 用户 - tags: - - TLS证书 - /panel/container/create: - post: - consumes: - - application/json - description: 创建一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.ContainerCreate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建容器 - tags: - - 容器 - /panel/container/exist: - get: - description: 检查一个容器是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查容器是否存在 - tags: - - 容器 - /panel/container/image/exist: - get: - description: 检查一个镜像是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查镜像是否存在 - tags: - - 容器 - /panel/container/image/inspect: - get: - description: 查看一个镜像的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看镜像 - tags: - - 容器 - /panel/container/image/list: - get: - description: 获取所有镜像列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取镜像列表 - tags: - - 容器 - /panel/container/image/prune: - post: - description: 清理无用的镜像 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理镜像 - tags: - - 容器 - /panel/container/image/pull: - post: - consumes: - - application/json - description: 拉取一个镜像 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.ImagePull' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 拉取镜像 - tags: - - 容器 - /panel/container/image/remove: - post: - description: 删除一个镜像 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除镜像 - tags: - - 容器 - /panel/container/inspect: - get: - description: 查看一个容器的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看容器 - tags: - - 容器 - /panel/container/kill: - post: - description: 杀死一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 杀死容器 - tags: - - 容器 - /panel/container/list: - get: - description: 获取所有容器列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取容器列表 - tags: - - 容器 - /panel/container/logs: - get: - description: 查看一个容器的日志 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看容器日志 - tags: - - 容器 - /panel/container/network/connect: - post: - consumes: - - application/json - description: 连接一个容器到一个网络 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NetworkConnectDisConnect' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 连接容器到网络 - tags: - - 容器 - /panel/container/network/create: - post: - consumes: - - application/json - description: 创建一个网络 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NetworkCreate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建网络 - tags: - - 容器 - /panel/container/network/disconnect: - post: - consumes: - - application/json - description: 从一个网络断开一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NetworkConnectDisConnect' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 从网络断开容器 - tags: - - 容器 - /panel/container/network/exist: - get: - description: 检查一个网络是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查网络是否存在 - tags: - - 容器 - /panel/container/network/inspect: - get: - description: 查看一个网络的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看网络 - tags: - - 容器 - /panel/container/network/list: - get: - description: 获取所有网络列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取网络列表 - tags: - - 容器 - /panel/container/network/prune: - post: - description: 清理无用的网络 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理网络 - tags: - - 容器 - /panel/container/network/remove: - post: - description: 删除一个网络 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除网络 - tags: - - 容器 - /panel/container/prune: - post: - description: 清理无用的容器 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理容器 - tags: - - 容器 - /panel/container/remove: - post: - description: 删除一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除容器 - tags: - - 容器 - /panel/container/rename: - post: - description: 重命名一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.ContainerRename' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重命名容器 - tags: - - 容器 - /panel/container/restart: - post: - description: 重启一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重启容器 - tags: - - 容器 - /panel/container/search: - get: - description: 根据容器名称搜索容器 - parameters: - - description: 容器名称 - in: query - name: name - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 搜索容器 - tags: - - 容器 - /panel/container/start: - post: - description: 启动一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 启动容器 - tags: - - 容器 - /panel/container/stats: - get: - description: 查看一个容器的状态信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看容器状态 - tags: - - 容器 - /panel/container/stop: - post: - description: 停止一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 停止容器 - tags: - - 容器 - /panel/container/unpause: - post: - description: 取消暂停一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 取消暂停容器 - tags: - - 容器 - /panel/container/volume/create: - post: - consumes: - - application/json - description: 创建一个卷 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.VolumeCreate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建卷 - tags: - - 容器 - /panel/container/volume/exist: - get: - description: 检查一个卷是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查卷是否存在 - tags: - - 容器 - /panel/container/volume/inspect: - get: - description: 查看一个卷的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看卷 - tags: - - 容器 - /panel/container/volume/list: - get: - description: 获取所有卷列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取卷列表 - tags: - - 容器 - /panel/container/volume/prune: - post: - description: 清理无用的卷 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理卷 - tags: - - 容器 - /panel/container/volume/remove: - post: - description: 删除一个卷 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除卷 - tags: - - 容器 - /panel/file/archive: - post: - consumes: - - application/json - description: 压缩文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Archive' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 压缩文件/目录 - tags: - - 文件 - /panel/file/content: + /info/checkUpdate: get: consumes: - application/json - description: 获取给定路径的文件内容 - parameters: - - in: query - name: path - type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取文件内容 + $ref: '#/definitions/service.SuccessResponse' + summary: 检查更新 tags: - - 文件 - /panel/file/copy: - post: - consumes: - - application/json - description: 复制文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Copy' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 复制文件/目录 - tags: - - 文件 - /panel/file/create: - post: - consumes: - - application/json - description: 创建文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NotExist' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建文件/目录 - tags: - - 文件 - /panel/file/delete: - post: - consumes: - - application/json - description: 删除给定路径的文件/目录 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Exist' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除文件/目录 - tags: - - 文件 - /panel/file/download: + - 信息服务 + /info/countInfo: get: consumes: - application/json - description: 下载给定路径的文件 - parameters: - - in: query - name: path - type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 下载文件 + $ref: '#/definitions/service.SuccessResponse' + summary: 统计信息 tags: - - 文件 - /panel/file/info: + - 信息服务 + /info/homePlugins: get: consumes: - application/json - description: 获取给定路径的文件/目录信息 - parameters: - - in: query - name: path - type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取文件/目录信息 + $ref: '#/definitions/service.SuccessResponse' + summary: 首页插件 tags: - - 文件 - /panel/file/list: + - 信息服务 + /info/installedDbAndPhp: get: consumes: - application/json - description: 获取给定路径的文件/目录列表 - parameters: - - in: query - name: path - type: string - - in: query - name: limit - type: integer - - in: query - name: page - type: integer produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - summary: 获取文件/目录列表 + $ref: '#/definitions/service.SuccessResponse' + summary: 已安装的数据库和PHP tags: - - 文件 - /panel/file/move: - post: - consumes: - - application/json - description: 移动文件/目录到给定路径,等效于重命名 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Move' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 移动文件/目录 - tags: - - 文件 - /panel/file/permission: - post: - consumes: - - application/json - description: 修改给定路径的文件/目录权限 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Permission' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 修改文件/目录权限 - tags: - - 文件 - /panel/file/remoteDownload: - post: - consumes: - - application/json - description: 下载远程文件到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NotExist' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 下载远程文件 - tags: - - 文件 - /panel/file/save: - post: - consumes: - - application/json - description: 保存给定路径的文件内容 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Save' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存文件内容 - tags: - - 文件 - /panel/file/search: - post: - consumes: - - application/json - description: 通过关键词搜索给定路径的文件/目录 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Search' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - summary: 搜索文件/目录 - tags: - - 文件 - /panel/file/unArchive: - post: - consumes: - - application/json - description: 解压文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UnArchive' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 解压文件/目录 - tags: - - 文件 - /panel/file/upload: - post: - consumes: - - application/json - description: 上传文件到给定路径 - parameters: - - description: file - in: formData - name: file - required: true - type: file - - description: path - in: formData - name: path - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 上传文件 - tags: - - 文件 - /panel/plugin/install: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 安装插件 - tags: - - 插件 - /panel/plugin/isInstalled: + - 信息服务 + /info/nowMonitor: get: - parameters: - - description: request - in: query - name: slug - required: true - type: string + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查插件是否已安装 + $ref: '#/definitions/service.SuccessResponse' + summary: 实时监控 tags: - - 插件 - /panel/plugin/list: + - 信息服务 + /info/panel: get: + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 插件列表 + $ref: '#/definitions/service.SuccessResponse' + summary: 面板信息 tags: - - 插件 - /panel/plugin/uninstall: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 卸载插件 - tags: - - 插件 - /panel/plugin/update: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新插件 - tags: - - 插件 - /panel/plugin/updateShow: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - - description: request - in: query - name: show - required: true - type: boolean - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新插件首页显示状态 - tags: - - 插件 - /panel/setting/https: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取面板 HTTPS 设置 - tags: - - 面板设置 + - 信息服务 + /info/restart: post: consumes: - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Https' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新面板 HTTPS 设置 + $ref: '#/definitions/service.SuccessResponse' + summary: 重启面板 tags: - - 面板设置 - /panel/setting/list: + - 信息服务 + /info/systemInfo: get: + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 设置列表 + $ref: '#/definitions/service.SuccessResponse' + summary: 系统信息 tags: - - 面板设置 - /panel/setting/update: + - 信息服务 + /info/update: post: consumes: - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_setting.Update' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新设置 + $ref: '#/definitions/service.SuccessResponse' + summary: 更新面板 tags: - - 面板设置 - /panel/system/service/disable: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 禁用服务 - tags: - - 系统 - /panel/system/service/enable: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 启用服务 - tags: - - 系统 - /panel/system/service/isEnabled: + - 信息服务 + /info/updateInfo: get: - parameters: - - description: request - in: query - name: data - required: true - type: string + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 是否启用服务 + $ref: '#/definitions/service.SuccessResponse' + summary: 版本更新信息 tags: - - 系统 - /panel/system/service/reload: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重载服务 - tags: - - 系统 - /panel/system/service/restart: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重启服务 - tags: - - 系统 - /panel/system/service/start: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 启动服务 - tags: - - 系统 - /panel/system/service/status: + - 信息服务 + /user/info/{id}: get: - parameters: - - description: request - in: query - name: data - required: true - type: string + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 服务状态 - tags: - - 系统 - /panel/system/service/stop: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 停止服务 - tags: - - 系统 - /panel/user/info: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] + $ref: '#/definitions/service.SuccessResponse' summary: 用户信息 tags: - - 用户鉴权 - /panel/user/login: + - 用户服务 + /user/isLogin: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.SuccessResponse' + summary: 是否登录 + tags: + - 用户服务 + /user/login: post: consumes: - application/json @@ -2667,1029 +208,31 @@ paths: name: data required: true schema: - $ref: '#/definitions/requests.Login' + $ref: '#/definitions/request.UserLogin' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - "403": - description: 用户名或密码错误 - schema: - $ref: '#/definitions/controllers.ErrorResponse' - "500": - description: 系统内部错误 - schema: - $ref: '#/definitions/controllers.ErrorResponse' + $ref: '#/definitions/service.SuccessResponse' summary: 登录 tags: - - 用户鉴权 - /panel/user/logout: + - 用户服务 + /user/logout: post: + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] + $ref: '#/definitions/service.SuccessResponse' summary: 登出 tags: - - 用户鉴权 - /panel/website/backupList: - get: - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - items: - $ref: '#/definitions/types.BackupFile' - type: array - type: object - security: - - BearerToken: [] - summary: 获取网站备份列表 - tags: - - 网站 - /panel/website/defaultConfig: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - additionalProperties: - type: string - type: object - type: object - security: - - BearerToken: [] - summary: 获取默认配置 - tags: - - 网站 - post: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - additionalProperties: - type: string - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存默认配置 - tags: - - 网站 - /panel/website/deleteBackup: - delete: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.DeleteBackup' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除网站备份 - tags: - - 网站 - /panel/website/uploadBackup: - put: - consumes: - - application/json - parameters: - - description: 备份文件 - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 上传网站备份 - tags: - - 网站 - /panel/websites: - get: - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取网站列表 - tags: - - 网站 - post: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Add' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加网站 - tags: - - 网站 - /panel/websites/{id}/config: - get: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/types.WebsiteAdd' - type: object - security: - - BearerToken: [] - summary: 获取网站配置 - tags: - - 网站 - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.SaveConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存网站配置 - tags: - - 网站 - /panel/websites/{id}/createBackup: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建网站备份 - tags: - - 网站 - /panel/websites/{id}/log: - delete: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空网站日志 - tags: - - 网站 - /panel/websites/{id}/resetConfig: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重置网站配置 - tags: - - 网站 - /panel/websites/{id}/restoreBackup: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 还原网站备份 - tags: - - 网站 - /panel/websites/{id}/status: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取网站状态 - tags: - - 网站 - /panel/websites/{id}/updateRemark: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新网站备注 - tags: - - 网站 - /panel/websites/delete: - post: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Delete' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除网站 - tags: - - 网站 - /plugins/frp/config: - get: - description: 获取 Frp 配置 - parameters: - - description: 服务 - in: query - name: service - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-Frp - post: - description: 更新 Frp 配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新配置 - tags: - - 插件-Frp - /plugins/gitea/config: - get: - description: 获取 Gitea 配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-Gitea - post: - description: 更新 Gitea 配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新配置 - tags: - - 插件-Gitea - /plugins/openresty/clearErrorLog: - post: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空错误日志 - tags: - - 插件-OpenResty - /plugins/openresty/config: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-OpenResty - post: - parameters: - - description: 配置 - in: body - name: config - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存配置 - tags: - - 插件-OpenResty - /plugins/openresty/errorLog: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取错误日志 - tags: - - 插件-OpenResty - /plugins/openresty/load: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取负载状态 - tags: - - 插件-OpenResty - /plugins/php/{version}/clearErrorLog: - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空错误日志 - tags: - - 插件-PHP - /plugins/php/{version}/clearSlowLog: - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空慢日志 - tags: - - 插件-PHP - /plugins/php/{version}/config: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-PHP - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: 配置 - in: body - name: config - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存配置 - tags: - - 插件-PHP - /plugins/php/{version}/errorLog: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取错误日志 - tags: - - 插件-PHP - /plugins/php/{version}/extensions: - delete: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: slug - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 卸载扩展 - tags: - - 插件-PHP - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取扩展列表 - tags: - - 插件-PHP - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: slug - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 安装扩展 - tags: - - 插件-PHP - /plugins/php/{version}/fpmConfig: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 FPM 配置 - tags: - - 插件-PHP - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: 配置 - in: body - name: config - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存 FPM 配置 - tags: - - 插件-PHP - /plugins/php/{version}/load: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取负载状态 - tags: - - 插件-PHP - /plugins/php/{version}/slowLog: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取慢日志 - tags: - - 插件-PHP - /plugins/podman/registryConfig: - get: - description: 获取 Podman 注册表配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取注册表配置 - tags: - - 插件-Podman - post: - description: 更新 Podman 注册表配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UpdateRegistryConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新注册表配置 - tags: - - 插件-Podman - /plugins/podman/storageConfig: - get: - description: 获取 Podman 存储配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取存储配置 - tags: - - 插件-Podman - post: - description: 更新 Podman 存储配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UpdateStorageConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新存储配置 - tags: - - 插件-Podman - /plugins/rsync/config: - get: - description: 获取 Rsync 配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-Rsync - post: - description: 更新 Rsync 配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新配置 - tags: - - 插件-Rsync - /plugins/rsync/modules: - get: - description: 列出所有 Rsync 模块 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 列出模块 - tags: - - 插件-Rsync - post: - description: 添加 Rsync 模块 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Create' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加模块 - tags: - - 插件-Rsync - /plugins/rsync/modules/{name}: - delete: - description: 删除 Rsync 模块 - parameters: - - description: 模块名称 - in: path - name: name - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除模块 - tags: - - 插件-Rsync - post: - description: 更新 Rsync 模块 - parameters: - - description: 模块名称 - in: path - name: name - required: true - type: string - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新模块 - tags: - - 插件-Rsync - /swagger: - get: - description: Swagger UI - responses: - "200": - description: OK - "500": - description: Internal Server Error - summary: Swagger UI - tags: - - Swagger + - 用户服务 securityDefinitions: BearerToken: in: header diff --git a/embed/frontend/.gitignore b/embed/frontend/.gitignore deleted file mode 100644 index f59ec20a..00000000 --- a/embed/frontend/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/go.mod b/go.mod index c89cc5dc..46260c79 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,30 @@ -module github.com/TheTNB/panel/v2 +module github.com/TheTNB/panel -go 1.22 +go 1.23 require ( - github.com/docker/docker v27.1.2+incompatible + github.com/beevik/ntp v1.4.3 + github.com/docker/docker v27.2.1+incompatible github.com/docker/go-connections v0.5.0 - github.com/gin-contrib/static v1.1.2 + github.com/glebarez/sqlite v1.11.0 + github.com/go-chi/chi/v5 v5.1.0 github.com/go-gormigrate/gormigrate/v2 v2.1.2 - github.com/go-resty/resty/v2 v2.14.0 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.22.1 + github.com/go-rat/chix v1.1.3 + github.com/go-rat/gormstore v1.0.5 + github.com/go-rat/sessions v1.0.9 + github.com/go-rat/utils v1.0.3 + github.com/go-resty/resty/v2 v2.15.0 github.com/go-sql-driver/mysql v1.8.1 - github.com/gookit/validate v1.5.2 - github.com/goravel/framework v1.14.1-0.20240728082300-b71cfeb464af - github.com/goravel/gin v1.2.3-0.20240714200024-34029bdef5d1 + github.com/golang-module/carbon/v2 v2.3.12 + github.com/gookit/color v1.5.4 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-version v1.7.0 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/file v1.1.0 + github.com/knadh/koanf/v2 v2.1.1 github.com/lib/pq v1.10.9 github.com/libdns/alidns v1.0.3 github.com/libdns/cloudflare v0.1.1 @@ -21,207 +32,106 @@ require ( github.com/libdns/libdns v0.2.2 github.com/libdns/tencentcloud v1.0.0 github.com/mholt/acmez/v2 v2.0.2 - github.com/mholt/archiver/v3 v3.5.1 - github.com/shirou/gopsutil v3.21.11+incompatible + github.com/mholt/archiver/v4 v4.0.0-alpha.8 + github.com/sethvargo/go-limiter v1.0.0 + github.com/shirou/gopsutil v2.21.11+incompatible github.com/spf13/cast v1.7.0 github.com/stretchr/testify v1.9.0 github.com/swaggo/http-swagger/v2 v2.0.2 github.com/swaggo/swag v1.16.3 + github.com/urfave/cli/v2 v2.27.4 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.26.0 - golang.org/x/net v0.28.0 - gorm.io/gorm v1.25.11 + golang.org/x/crypto v0.27.0 + golang.org/x/net v0.29.0 + gorm.io/gorm v1.25.12 ) require ( - atomicgo.dev/cursor v0.2.0 // indirect - atomicgo.dev/keyboard v0.2.9 // indirect - atomicgo.dev/schedule v0.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/catppuccin/go v0.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/charmbracelet/bubbles v0.18.0 // indirect - github.com/charmbracelet/bubbletea v0.26.6 // indirect - github.com/charmbracelet/huh v0.5.2 // indirect - github.com/charmbracelet/huh/spinner v0.0.0-20240725212135-67d4a4354ed1 // indirect - github.com/charmbracelet/lipgloss v0.12.1 // indirect - github.com/charmbracelet/x/ansi v0.1.4 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/input v0.1.3 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/bodgit/plumbing v1.2.0 // indirect + github.com/bodgit/sevenzip v1.3.0 // indirect + github.com/bodgit/windows v1.0.0 // indirect + github.com/connesc/cipherio v0.2.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/glebarez/sqlite v1.11.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/spec v0.20.14 // indirect - github.com/go-openapi/swag v0.22.7 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-rat/securecookie v1.0.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/golang-migrate/migrate/v4 v4.17.1 // indirect - github.com/golang-module/carbon/v2 v2.3.12 // indirect - github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/google/wire v0.6.0 // indirect - github.com/gookit/color v1.5.4 // indirect - github.com/gookit/filter v1.2.1 // indirect - github.com/gookit/goutil v0.6.15 // indirect - github.com/goravel/file-rotatelogs/v2 v2.4.2 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jaevor/go-nanoid v1.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.7 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/microsoft/go-mssqldb v1.6.0 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect - github.com/nwaples/rardecode v1.1.0 // indirect + github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc5 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pierrec/lz4/v4 v4.1.16 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pterm/pterm v0.12.79 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rotisserie/eris v0.5.4 // indirect - github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/samber/do/v2 v2.0.0-beta.7 // indirect - github.com/samber/go-type-to-string v1.4.0 // indirect - github.com/savioxavier/termlink v1.3.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect - github.com/unrolled/secure v1.15.0 // indirect - github.com/urfave/cli/v2 v2.27.3 // indirect - github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.22.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.uber.org/atomic v1.11.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.5.7 // indirect - gorm.io/driver/postgres v1.5.9 // indirect - gorm.io/driver/sqlserver v1.5.3 // indirect - gorm.io/plugin/dbresolver v1.5.2 // indirect - gotest.tools/v3 v3.5.0 // indirect - modernc.org/libc v1.37.6 // indirect + gotest.tools/v3 v3.5.1 // indirect + modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect - modernc.org/sqlite v1.28.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.32.0 // indirect ) - -// The current latest version of github.com/mholt/archiver/v3 (v3.5.1) suffers from CVE-2024-0406. -// There is currently a PR in place to resolve it (https://github.com/mholt/archiver/pull/396), -// but it has not had much attention recently. -// Just replace our usage of github.com/mholt/archiver/v3 with github.com/anchore/archiver/v3 (v3.5.2) -// so static vulnerability scanners will be happy. -// This version (probably) fixes CVE-2024-0406, but we are also unaffected by that vulnerability anyway, -// as we do not use [(*archiver.Tar).Unarchive()], so it doesn't really matter. -// What is important, though, is the code changes between github.com/mholt/archiver/v3 v3.5.1 -// and github.com/anchore/archiver/v3 v3.5.2 only touch the [(*archiver.Tar).Unarchive()] path, -// and nothing we use. See https://github.com/mholt/archiver/compare/v3.5.1...anchore:archiver:v3.5.2 -// for more details of the exact difference. -replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 diff --git a/go.sum b/go.sum index d9bedbb6..ba651573 100644 --- a/go.sum +++ b/go.sum @@ -1,142 +1,61 @@ -atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= -atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= -atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= -atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= -atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= -atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= -atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= -atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 h1:t5+QXLCK9SVi0PPdaY0PrFvYUo24KwA0QwxnaHRSVd4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest/adal v0.9.16 h1:P8An8Z9rH1ldbOLdFpxYorgOt2sywL9V24dAwWHPuGc= -github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= -github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= -github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= -github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= -github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= -github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= -github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= -github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA= -github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= -github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= -github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= -github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= +github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= +github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= +github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= +github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY= +github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM= +github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA= +github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= -github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= -github.com/charmbracelet/huh v0.5.2 h1:ofeNkJ4iaFnzv46Njhx896DzLUe/j0L2QAf8znwzX4c= -github.com/charmbracelet/huh v0.5.2/go.mod h1:Sf7dY0oAn6N/e3sXJFtFX9hdQLrUdO3z7AYollG9bAM= -github.com/charmbracelet/huh/spinner v0.0.0-20240725212135-67d4a4354ed1 h1:IjNcc7cCYR0ymVfy4dWvBHE6VSqfpcvHRfWpCWcun0g= -github.com/charmbracelet/huh/spinner v0.0.0-20240725212135-67d4a4354ed1/go.mod h1:9VssyY5pUozMRmDYlLYV20QMMcA2sHg3qnaB6PvdIm8= -github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= -github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= -github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= -github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= -github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= -github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= -github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= +github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= -github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= -github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -146,14 +65,8 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -162,179 +75,141 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4= -github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY= github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= -github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= -github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU= -github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-rat/chix v1.1.3 h1:NeDbmA3OTfhEeizn528dz8z8/lBcsziftgtSarYn7Vo= +github.com/go-rat/chix v1.1.3/go.mod h1:XeC0hmaAXlPB/V8g0QZiIAgnAJO70ZeT+k1Q4Zh9qeI= +github.com/go-rat/gormstore v1.0.5 h1:76umSO6n+zoLnxVryX+6aUu6rIQLveGO8fCu9d5GzTA= +github.com/go-rat/gormstore v1.0.5/go.mod h1:xG/n+Du8baWf7ptiafiHLWBZWVXrYIPC42CsoEbfr4M= +github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv86lBo= +github.com/go-rat/securecookie v1.0.1/go.mod h1:tP/ObWYyjmcpabQ7WTon/i2lBSip/Aolliw2llXuPDU= +github.com/go-rat/sessions v1.0.9 h1:zysrHceTFqyc0qUWn5giLGdbfr/DYHXvFECDBSyv9Gk= +github.com/go-rat/sessions v1.0.9/go.mod h1:Ray/GCbuhm4U9xpjFFSCfOTCEn91puEhAXX5creHE9g= +github.com/go-rat/utils v1.0.3 h1:SqH/O0KYq4SBv8naSAjJA4hC45/d8NLNrTCONfqjbFM= +github.com/go-rat/utils v1.0.3/go.mod h1:4WNPlrF57KmeGZN3HOeBgdBVLJL3xgba4QRP/t6pKrE= +github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90= +github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= -github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang-module/carbon/v2 v2.3.12 h1:VC1DwN1kBwJkh5MjXmTFryjs5g4CWyoM8HAHffZPX/k= github.com/golang-module/carbon/v2 v2.3.12/go.mod h1:HNsedGzXGuNciZImYP2OMnpiwq/vhIstR/vn45ib5cI= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gookit/filter v1.2.1 h1:37XivkBm2E5qe1KaGdJ5ZfF5l9NYdGWfLEeQadJD8O4= -github.com/gookit/filter v1.2.1/go.mod h1:rxynQFr793x+XDwnRmJFEb53zDw0Zqx3OD7TXWoR9mQ= -github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= -github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY= -github.com/gookit/validate v1.5.2 h1:i5I2OQ7WYHFRPRATGu9QarR9snnNHydvwSuHXaRWAV0= -github.com/gookit/validate v1.5.2/go.mod h1:yuPy2WwDlwGRa06fFJ5XIO8QEwhRnTC2LmxmBa5SE14= -github.com/goravel/file-rotatelogs/v2 v2.4.2 h1:g68AzbePXcm0V2CpUMc9j4qVzcDn7+7aoWSjZ51C0m4= -github.com/goravel/file-rotatelogs/v2 v2.4.2/go.mod h1:23VuSW8cBS4ax5cmbV+5AaiLpq25b8UJ96IhbAkdo8I= -github.com/goravel/framework v1.14.1-0.20240728082300-b71cfeb464af h1:ALAKozYauAV6vFwFE7OsM799yUyoJTBGZifWmSoBiVI= -github.com/goravel/framework v1.14.1-0.20240728082300-b71cfeb464af/go.mod h1:ynfAeFhmDC5J67Y6kqw90w/wHNzZvnjfLOkAwr7tPT4= -github.com/goravel/gin v1.2.3-0.20240714200024-34029bdef5d1 h1:3HURtSgEPnX9wy78wx/90cZYBCanEweX+8SpZ5dX0m0= -github.com/goravel/gin v1.2.3-0.20240714200024-34029bdef5d1/go.mod h1:Y2d8KYT/tS5sEMJ3j3ll86kEz2op7vDrbEg6nhURkUQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jaevor/go-nanoid v1.4.0 h1:mPz0oi3CrQyEtRxeRq927HHtZCJAAtZ7zdy7vOkrvWs= +github.com/jaevor/go-nanoid v1.4.0/go.mod h1:GIpPtsvl3eSBsjjIEFQdzzgpi50+Bo1Luk+aYlbJzlc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/file v1.1.0 h1:MTjA+gRrVl1zqgetEAIaXHqYje0XSosxSiMD4/7kz0o= +github.com/knadh/koanf/providers/file v1.1.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -342,12 +217,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= -github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= @@ -362,141 +233,77 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU= github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI= -github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= -github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mholt/acmez/v2 v2.0.2 h1:OmK6xckte2JfKGPz4OAA8aNHTiLvGp8tLzmrd/wfSyw= github.com/mholt/acmez/v2 v2.0.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= -github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= -github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= +github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= -github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= -github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= -github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= -github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= -github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= -github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= -github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= -github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= -github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= -github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= -github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= -github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= -github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/samber/do/v2 v2.0.0-beta.7 h1:tmdLOVSCbTA6uGWLU5poi/nZvMRh5QxXFJ9vHytU+Jk= -github.com/samber/do/v2 v2.0.0-beta.7/go.mod h1:+LpV3vu4L81Q1JMZNSkMvSkW9lt4e5eJoXoZHkeBS4c= -github.com/samber/go-type-to-string v1.4.0 h1:KXphToZgiFdnJQxryU25brhlh/CqY/cwJVeX2rfmow0= -github.com/samber/go-type-to-string v1.4.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= -github.com/savioxavier/termlink v1.3.0 h1:3Gl4FzQjUyiHzmoEDfmWEhgIwDiJY4poOQHP+k8ReA4= -github.com/savioxavier/termlink v1.3.0/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= +github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= +github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U= +github.com/shirou/gopsutil v2.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= @@ -505,97 +312,86 @@ github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/unrolled/secure v1.15.0 h1:q7x+pdp8jAHnbzxu6UheP8fRlG/rwYTb8TPuQ3rn9Og= -github.com/unrolled/secure v1.15.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= -github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= -github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= -go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= +go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -604,196 +400,182 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= -gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= -gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= -gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= -gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= -gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= -gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII= -gorm.io/plugin/dbresolver v1.5.2/go.mod h1:jPh59GOQbO7v7v28ZKZPd45tr+u3vyT+8tHdfdfOWcU= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= -modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/app/global.go b/internal/app/global.go new file mode 100644 index 00000000..62ece90c --- /dev/null +++ b/internal/app/global.go @@ -0,0 +1,29 @@ +package app + +import ( + "github.com/go-chi/chi/v5" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + "github.com/go-rat/sessions" + "github.com/knadh/koanf/v2" + "gorm.io/gorm" + + "github.com/TheTNB/panel/pkg/queue" +) + +var ( + Conf *koanf.Koanf + Http *chi.Mux + Orm *gorm.DB + Validator *validator.Validate + Translator *ut.Translator + Session *sessions.Manager + Queue *queue.Queue +) + +// 面板全局变量 +var ( + Root string + Version string + Locale string +) diff --git a/internal/backup.go b/internal/backup.go deleted file mode 100644 index 1f55f94d..00000000 --- a/internal/backup.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import ( - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Backup interface { - WebsiteList() ([]types.BackupFile, error) - WebSiteBackup(website models.Website) error - WebsiteRestore(website models.Website, backupFile string) error - MysqlList() ([]types.BackupFile, error) - MysqlBackup(database string) error - MysqlRestore(database string, backupFile string) error - PostgresqlList() ([]types.BackupFile, error) - PostgresqlBackup(database string) error - PostgresqlRestore(database string, backupFile string) error -} diff --git a/internal/biz/cert.go b/internal/biz/cert.go new file mode 100644 index 00000000..664a85f5 --- /dev/null +++ b/internal/biz/cert.go @@ -0,0 +1,40 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" +) + +type Cert struct { + ID uint `gorm:"primaryKey" json:"id"` + AccountID uint `gorm:"not null" json:"account_id"` // 关联的 ACME 账户 ID + WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID + DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID + Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096) + Domains []string `gorm:"not null;serializer:json" json:"domains"` + AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签 + CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用) + Cert string `gorm:"not null" json:"cert"` // 证书内容 + Key string `gorm:"not null" json:"key"` // 私钥内容 + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Website *Website `gorm:"foreignKey:WebsiteID" json:"website"` + Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"` + DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"` +} + +type CertRepo interface { + List(page, limit uint) ([]*Cert, int64, error) + Get(id uint) (*Cert, error) + Create(req *request.CertCreate) (*Cert, error) + Update(req *request.CertUpdate) error + Delete(id uint) error + ObtainAuto(id uint) (*acme.Certificate, error) + ObtainManual(id uint) (*acme.Certificate, error) + Renew(id uint) (*acme.Certificate, error) + ManualDNS(id uint) ([]acme.DNSRecord, error) + Deploy(ID, WebsiteID uint) error +} diff --git a/internal/biz/cert_account.go b/internal/biz/cert_account.go new file mode 100644 index 00000000..80482ee3 --- /dev/null +++ b/internal/biz/cert_account.go @@ -0,0 +1,29 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) + +type CertAccount struct { + ID uint `gorm:"primaryKey" json:"id"` + Email string `gorm:"not null" json:"email"` + CA string `gorm:"not null" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) + Kid string `gorm:"not null" json:"kid"` + HmacEncoded string `gorm:"not null" json:"hmac_encoded"` + PrivateKey string `gorm:"not null" json:"private_key"` + KeyType string `gorm:"not null" json:"key_type"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Certs []*Cert `gorm:"foreignKey:AccountID" json:"-"` +} + +type CertAccountRepo interface { + List(page, limit uint) ([]*CertAccount, int64, error) + Get(id uint) (*CertAccount, error) + Create(req *request.CertAccountCreate) (*CertAccount, error) + Update(req *request.CertAccountUpdate) error + Delete(id uint) error +} diff --git a/internal/biz/cert_dns.go b/internal/biz/cert_dns.go new file mode 100644 index 00000000..df84a737 --- /dev/null +++ b/internal/biz/cert_dns.go @@ -0,0 +1,27 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" +) + +type CertDNS struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` // 备注名称 + Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, tencent, aliyun, cloudflare) + Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Certs []*Cert `gorm:"foreignKey:DNSID" json:"-"` +} + +type CertDNSRepo interface { + List(page, limit uint) ([]*CertDNS, int64, error) + Get(id uint) (*CertDNS, error) + Create(req *request.CertDNSCreate) (*CertDNS, error) + Update(req *request.CertDNSUpdate) error + Delete(id uint) error +} diff --git a/internal/biz/container.go b/internal/biz/container.go new file mode 100644 index 00000000..754d0246 --- /dev/null +++ b/internal/biz/container.go @@ -0,0 +1,28 @@ +package biz + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerRepo interface { + ListAll() ([]types.Container, error) + ListByNames(names []string) ([]types.Container, error) + Create(req *request.ContainerCreate) (string, error) + Remove(id string) error + Start(id string) error + Stop(id string) error + Restart(id string) error + Pause(id string) error + Unpause(id string) error + Inspect(id string) (types.ContainerJSON, error) + Kill(id string) error + Rename(id string, newName string) error + Stats(id string) (container.StatsResponseReader, error) + Exist(name string) (bool, error) + Update(id string, config container.UpdateConfig) error + Logs(id string) (string, error) + Prune() error +} diff --git a/internal/biz/container_image.go b/internal/biz/container_image.go new file mode 100644 index 00000000..555f000d --- /dev/null +++ b/internal/biz/container_image.go @@ -0,0 +1,17 @@ +package biz + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerImageRepo interface { + List() ([]image.Summary, error) + Exist(id string) (bool, error) + Pull(req *request.ContainerImagePull) error + Remove(id string) error + Prune() error + Inspect(id string) (types.ImageInspect, error) +} diff --git a/internal/biz/container_network.go b/internal/biz/container_network.go new file mode 100644 index 00000000..48499515 --- /dev/null +++ b/internal/biz/container_network.go @@ -0,0 +1,18 @@ +package biz + +import ( + "github.com/docker/docker/api/types/network" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerNetworkRepo interface { + List() ([]network.Inspect, error) + Create(req *request.ContainerNetworkCreate) (string, error) + Remove(id string) error + Exist(name string) (bool, error) + Inspect(id string) (network.Inspect, error) + Connect(networkID string, containerID string) error + Disconnect(networkID string, containerID string) error + Prune() error +} diff --git a/internal/biz/container_volume.go b/internal/biz/container_volume.go new file mode 100644 index 00000000..3e1659cb --- /dev/null +++ b/internal/biz/container_volume.go @@ -0,0 +1,16 @@ +package biz + +import ( + "github.com/docker/docker/api/types/volume" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerVolumeRepo interface { + List() ([]*volume.Volume, error) + Create(req *request.ContainerVolumeCreate) (volume.Volume, error) + Exist(name string) (bool, error) + Inspect(id string) (volume.Volume, error) + Remove(id string) error + Prune() error +} diff --git a/internal/biz/cron.go b/internal/biz/cron.go new file mode 100644 index 00000000..46b982f1 --- /dev/null +++ b/internal/biz/cron.go @@ -0,0 +1,30 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) + +type Cron struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Status bool `gorm:"not null" json:"status"` + Type string `gorm:"not null" json:"type"` + Time string `gorm:"not null" json:"time"` + Shell string `gorm:"not null" json:"shell"` + Log string `gorm:"not null" json:"log"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type CronRepo interface { + Count() (int64, error) + List(page, limit uint) ([]*Cron, int64, error) + Get(id uint) (*Cron, error) + Create(req *request.CronCreate) error + Update(req *request.CronUpdate) error + Delete(id uint) error + Status(id uint, status bool) error + Log(id uint) (string, error) +} diff --git a/internal/biz/database.go b/internal/biz/database.go new file mode 100644 index 00000000..f43ac4ab --- /dev/null +++ b/internal/biz/database.go @@ -0,0 +1,16 @@ +package biz + +import "github.com/golang-module/carbon/v2" + +type Database struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Type string `gorm:"not null" json:"type"` + Host string `gorm:"not null" json:"host"` + Port int `gorm:"not null" json:"port"` + Username string `gorm:"not null" json:"username"` + Password string `gorm:"not null" json:"password"` + Remark string `gorm:"not null" json:"remark"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} diff --git a/internal/biz/firewall.go b/internal/biz/firewall.go new file mode 100644 index 00000000..73ea74f8 --- /dev/null +++ b/internal/biz/firewall.go @@ -0,0 +1,4 @@ +package biz + +type FirewallRepo interface { +} diff --git a/internal/biz/monitor.go b/internal/biz/monitor.go new file mode 100644 index 00000000..cc60ac6f --- /dev/null +++ b/internal/biz/monitor.go @@ -0,0 +1,22 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/tools" +) + +type Monitor struct { + ID uint `gorm:"primaryKey" json:"id"` + Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type MonitorRepo interface { + GetSetting() (*request.MonitorSetting, error) + UpdateSetting(setting *request.MonitorSetting) error + Clear() error + List(start, end carbon.Carbon) ([]*Monitor, error) +} diff --git a/internal/biz/plugin.go b/internal/biz/plugin.go new file mode 100644 index 00000000..21a5a65a --- /dev/null +++ b/internal/biz/plugin.go @@ -0,0 +1,30 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/pkg/types" +) + +type Plugin struct { + ID uint `gorm:"primaryKey" json:"id"` + Slug string `gorm:"not null;unique" json:"slug"` + Version string `gorm:"not null" json:"version"` + Show bool `gorm:"not null" json:"show"` + ShowOrder int `gorm:"not null" json:"show_order"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type PluginRepo interface { + All() []*types.Plugin + Installed() ([]*Plugin, error) + Get(slug string) (*types.Plugin, error) + GetInstalled(slug string) (*Plugin, error) + GetInstalledAll(cond ...string) ([]*Plugin, error) + IsInstalled(cond ...string) (bool, error) + Install(slug string) error + Uninstall(slug string) error + Update(slug string) error + UpdateShow(slug string, show bool) error +} diff --git a/internal/biz/safe.go b/internal/biz/safe.go new file mode 100644 index 00000000..f9ba6fe5 --- /dev/null +++ b/internal/biz/safe.go @@ -0,0 +1,8 @@ +package biz + +type SafeRepo interface { + GetSSH() (uint, bool, error) + UpdateSSH(port uint, status bool) error + GetPingStatus() (bool, error) + UpdatePingStatus(status bool) error +} diff --git a/internal/biz/setting.go b/internal/biz/setting.go new file mode 100644 index 00000000..d09f3c3c --- /dev/null +++ b/internal/biz/setting.go @@ -0,0 +1,39 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) + +type SettingKey string + +const ( + SettingKeyName SettingKey = "name" + SettingKeyVersion SettingKey = "version" + SettingKeyMonitor SettingKey = "monitor" + SettingKeyMonitorDays SettingKey = "monitor_days" + SettingKeyBackupPath SettingKey = "backup_path" + SettingKeyWebsitePath SettingKey = "website_path" + SettingKeyMysqlRootPassword SettingKey = "mysql_root_password" + SettingKeySshHost SettingKey = "ssh_host" + SettingKeySshPort SettingKey = "ssh_port" + SettingKeySshUser SettingKey = "ssh_user" + SettingKeySshPassword SettingKey = "ssh_password" +) + +type Setting struct { + ID uint `gorm:"primaryKey" json:"id"` + Key SettingKey `gorm:"not null;unique" json:"key"` + Value string `gorm:"not null" json:"value"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type SettingRepo interface { + Get(key SettingKey, defaultValue ...string) (string, error) + Set(key SettingKey, value string) error + Delete(key SettingKey) error + GetPanelSetting() (*request.PanelSetting, error) + UpdatePanelSetting(setting *request.PanelSetting) error +} diff --git a/internal/biz/ssh.go b/internal/biz/ssh.go new file mode 100644 index 00000000..bfa4664d --- /dev/null +++ b/internal/biz/ssh.go @@ -0,0 +1,8 @@ +package biz + +import "github.com/TheTNB/panel/internal/http/request" + +type SSHRepo interface { + GetInfo() (map[string]any, error) + UpdateInfo(req *request.SSHUpdateInfo) error +} diff --git a/internal/biz/task.go b/internal/biz/task.go new file mode 100644 index 00000000..633c72e9 --- /dev/null +++ b/internal/biz/task.go @@ -0,0 +1,30 @@ +package biz + +import "github.com/golang-module/carbon/v2" + +type TaskStatus string + +const ( + TaskStatusWaiting TaskStatus = "waiting" + TaskStatusRunning TaskStatus = "running" + TaskStatusSuccess TaskStatus = "finished" + TaskStatusFailed TaskStatus = "failed" +) + +type Task struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;index" json:"name"` + Status TaskStatus `gorm:"not null;default:'waiting'" json:"status"` + Shell string `gorm:"not null" json:"-"` + Log string `gorm:"not null" json:"log"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type TaskRepo interface { + HasRunningTask() bool + List(page, limit uint) ([]*Task, int64, error) + Get(id uint) (*Task, error) + Delete(id uint) error + UpdateStatus(id uint, status TaskStatus) error +} diff --git a/internal/biz/user.go b/internal/biz/user.go new file mode 100644 index 00000000..a6fad865 --- /dev/null +++ b/internal/biz/user.go @@ -0,0 +1,22 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + "gorm.io/gorm" +) + +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"not null;unique" json:"username"` + Password string `gorm:"not null" json:"password"` + Email string `gorm:"not null" json:"email"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} + +type UserRepo interface { + CheckPassword(username, password string) (*User, error) + Get(id uint) (*User, error) + Save(user *User) error +} diff --git a/internal/biz/website.go b/internal/biz/website.go new file mode 100644 index 00000000..1191c290 --- /dev/null +++ b/internal/biz/website.go @@ -0,0 +1,36 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" +) + +type Website struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Status bool `gorm:"not null;default:true" json:"status"` + Path string `gorm:"not null" json:"path"` + PHP int `gorm:"not null" json:"php"` + SSL bool `gorm:"not null" json:"ssl"` + Remark string `gorm:"not null" json:"remark"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"` +} + +type WebsiteRepo interface { + UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error + Count() (int64, error) + Get(id uint) (*types.WebsiteSetting, error) + List(page, limit uint) ([]*Website, int64, error) + Create(req *request.WebsiteCreate) (*Website, error) + Update(req *request.WebsiteUpdate) error + Delete(req *request.WebsiteDelete) error + ClearLog(id uint) error + UpdateRemark(id uint, remark string) error + ResetConfig(id uint) error + UpdateStatus(id uint, status bool) error +} diff --git a/internal/bootstrap/app.go b/internal/bootstrap/app.go new file mode 100644 index 00000000..9669966c --- /dev/null +++ b/internal/bootstrap/app.go @@ -0,0 +1,21 @@ +package bootstrap + +import ( + "runtime/debug" +) + +func Boot() { + debug.SetGCPercent(10) + debug.SetMemoryLimit(64 << 20) + + initConf() + initGlobal() + initOrm() + runMigrate() + initValidator() + initSession() + initQueue() + go initHttp() + + select {} +} diff --git a/internal/bootstrap/conf.go b/internal/bootstrap/conf.go new file mode 100644 index 00000000..2a5b7d96 --- /dev/null +++ b/internal/bootstrap/conf.go @@ -0,0 +1,24 @@ +package bootstrap + +import ( + "fmt" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + + "github.com/TheTNB/panel/internal/app" +) + +func initConf() { + app.Conf = koanf.New(".") + if err := app.Conf.Load(file.Provider("config/config.yml"), yaml.Parser()); err != nil { + panic(fmt.Sprintf("failed to load config: %v", err)) + } +} + +func initGlobal() { + app.Root = app.Conf.MustString("app.root") + app.Version = app.Conf.MustString("app.version") + app.Locale = app.Conf.MustString("app.locale") +} diff --git a/internal/bootstrap/db.go b/internal/bootstrap/db.go new file mode 100644 index 00000000..9038afda --- /dev/null +++ b/internal/bootstrap/db.go @@ -0,0 +1,40 @@ +package bootstrap + +import ( + "fmt" + + "github.com/glebarez/sqlite" + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/migration" +) + +func initOrm() { + logLevel := logger.Error + if app.Conf.Bool("database.debug") { + logLevel = logger.Info + } + // You can use any other database, like MySQL or PostgreSQL. + db, err := gorm.Open(sqlite.Open("storage/panel.db"), &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), + SkipDefaultTransaction: true, + DisableForeignKeyConstraintWhenMigrating: true, + }) + if err != nil { + panic(fmt.Sprintf("failed to connect database: %v", err)) + } + app.Orm = db +} + +func runMigrate() { + migrator := gormigrate.New(app.Orm, &gormigrate.Options{ + UseTransaction: true, // Note: MySQL not support DDL transaction + ValidateUnknownMigrations: true, + }, migration.Migrations) + if err := migrator.Migrate(); err != nil { + panic(fmt.Sprintf("failed to migrate database: %v", err)) + } +} diff --git a/internal/bootstrap/http.go b/internal/bootstrap/http.go new file mode 100644 index 00000000..5539b480 --- /dev/null +++ b/internal/bootstrap/http.go @@ -0,0 +1,33 @@ +package bootstrap + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/http/middleware" + "github.com/TheTNB/panel/internal/plugin" + "github.com/TheTNB/panel/internal/route" +) + +func initHttp() { + app.Http = chi.NewRouter() + + // add middleware + app.Http.Use(middleware.GlobalMiddleware()...) + + // add route + route.Http(app.Http) + plugin.Boot(app.Http) + + server := &http.Server{ + Addr: app.Conf.MustString("http.address"), + Handler: http.AllowQuerySemicolons(app.Http), + MaxHeaderBytes: 2048 << 20, + } + if err := server.ListenAndServe(); err != nil { + panic(fmt.Sprintf("failed to start http server: %v", err)) + } +} diff --git a/internal/bootstrap/queue.go b/internal/bootstrap/queue.go new file mode 100644 index 00000000..69fec45f --- /dev/null +++ b/internal/bootstrap/queue.go @@ -0,0 +1,11 @@ +package bootstrap + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/pkg/queue" +) + +func initQueue() { + app.Queue = queue.New() + go app.Queue.Run() +} diff --git a/internal/bootstrap/session.go b/internal/bootstrap/session.go new file mode 100644 index 00000000..aeeb8416 --- /dev/null +++ b/internal/bootstrap/session.go @@ -0,0 +1,31 @@ +package bootstrap + +import ( + "fmt" + + "github.com/go-rat/gormstore" + "github.com/go-rat/sessions" + + "github.com/TheTNB/panel/internal/app" +) + +func initSession() { + // initialize session manager + manager, err := sessions.NewManager(&sessions.ManagerOptions{ + Key: app.Conf.String("app.key"), + Lifetime: 120, + GcInterval: 30, + DisableDefaultDriver: true, + }) + if err != nil { + panic(fmt.Sprintf("failed to initialize session manager: %v", err)) + } + + // extend gorm store driver + store := gormstore.New(app.Orm) + if err = manager.Extend("default", store); err != nil { + panic(fmt.Sprintf("failed to extend session manager: %v", err)) + } + + app.Session = manager +} diff --git a/internal/bootstrap/validator.go b/internal/bootstrap/validator.go new file mode 100644 index 00000000..2427fb1c --- /dev/null +++ b/internal/bootstrap/validator.go @@ -0,0 +1,26 @@ +package bootstrap + +import ( + "fmt" + + "github.com/go-playground/locales/zh_Hans_CN" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + "github.com/go-playground/validator/v10/translations/zh" + + "github.com/TheTNB/panel/internal/app" +) + +func initValidator() { + translator := zh_Hans_CN.New() + uni := ut.New(translator, translator) + trans, _ := uni.GetTranslator("zh_Hans_CN") + + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := zh.RegisterDefaultTranslations(validate, trans); err != nil { + panic(fmt.Sprintf("failed to register validator translations: %v", err)) + } + + app.Translator = &trans + app.Validator = validate +} diff --git a/internal/cert.go b/internal/cert.go deleted file mode 100644 index 98e7dcaa..00000000 --- a/internal/cert.go +++ /dev/null @@ -1,27 +0,0 @@ -package internal - -import ( - requests "github.com/TheTNB/panel/v2/app/http/requests/cert" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type Cert interface { - UserStore(request requests.UserStore) error - UserUpdate(request requests.UserUpdate) error - UserShow(ID uint) (models.CertUser, error) - UserDestroy(ID uint) error - DNSStore(request requests.DNSStore) error - DNSUpdate(request requests.DNSUpdate) error - DNSShow(ID uint) (models.CertDNS, error) - DNSDestroy(ID uint) error - CertStore(request requests.CertStore) error - CertUpdate(request requests.CertUpdate) error - CertShow(ID uint) (models.Cert, error) - CertDestroy(ID uint) error - ObtainAuto(ID uint) (acme.Certificate, error) - ObtainManual(ID uint) (acme.Certificate, error) - ManualDNS(ID uint) ([]acme.DNSRecord, error) - Renew(ID uint) (acme.Certificate, error) - Deploy(ID, WebsiteID uint) error -} diff --git a/internal/container.go b/internal/container.go deleted file mode 100644 index 29415e67..00000000 --- a/internal/container.go +++ /dev/null @@ -1,54 +0,0 @@ -package internal - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/volume" - - requests "github.com/TheTNB/panel/v2/app/http/requests/container" - paneltypes "github.com/TheTNB/panel/v2/pkg/types" -) - -type Container interface { - ContainerListAll() ([]types.Container, error) - ContainerListByNames(names []string) ([]types.Container, error) - ContainerCreate(name string, config container.Config, host container.HostConfig, networkConfig network.NetworkingConfig) (string, error) - ContainerRemove(id string) error - ContainerStart(id string) error - ContainerStop(id string) error - ContainerRestart(id string) error - ContainerPause(id string) error - ContainerUnpause(id string) error - ContainerInspect(id string) (types.ContainerJSON, error) - ContainerKill(id string) error - ContainerRename(id string, newName string) error - ContainerStats(id string) (container.StatsResponseReader, error) - ContainerExist(name string) (bool, error) - ContainerUpdate(id string, config container.UpdateConfig) error - ContainerLogs(id string) (string, error) - ContainerPrune() error - NetworkList() ([]network.Inspect, error) - NetworkCreate(config requests.NetworkCreate) (string, error) - NetworkRemove(id string) error - NetworkExist(name string) (bool, error) - NetworkInspect(id string) (network.Inspect, error) - NetworkConnect(networkID string, containerID string) error - NetworkDisconnect(networkID string, containerID string) error - NetworkPrune() error - ImageList() ([]image.Summary, error) - ImageExist(id string) (bool, error) - ImagePull(config requests.ImagePull) error - ImageRemove(id string) error - ImagePrune() error - ImageInspect(id string) (types.ImageInspect, error) - VolumeList() ([]*volume.Volume, error) - VolumeCreate(config requests.VolumeCreate) (volume.Volume, error) - VolumeExist(name string) (bool, error) - VolumeInspect(id string) (volume.Volume, error) - VolumeRemove(id string) error - VolumePrune() error - KVToMap(kvs []paneltypes.KV) map[string]string - KVToSlice(kvs []paneltypes.KV) []string -} diff --git a/internal/cron.go b/internal/cron.go deleted file mode 100644 index 91b792bf..00000000 --- a/internal/cron.go +++ /dev/null @@ -1,8 +0,0 @@ -package internal - -import "github.com/TheTNB/panel/v2/app/models" - -type Cron interface { - AddToSystem(cron models.Cron) error - DeleteFromSystem(cron models.Cron) error -} diff --git a/internal/data/cert.go b/internal/data/cert.go new file mode 100644 index 00000000..208f311c --- /dev/null +++ b/internal/data/cert.go @@ -0,0 +1,273 @@ +package data + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type certRepo struct { + client *acme.Client + websiteRepo biz.WebsiteRepo +} + +func NewCertRepo() biz.CertRepo { + return &certRepo{ + websiteRepo: NewWebsiteRepo(), + } +} + +func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) { + var certs []*biz.Cert + var total int64 + err := app.Orm.Model(&biz.Cert{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error + return certs, total, err +} + +func (r *certRepo) Get(id uint) (*biz.Cert, error) { + cert := new(biz.Cert) + err := app.Orm.Model(&biz.Cert{}).Where("id = ?", id).First(cert).Error + return cert, err +} + +func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) { + cert := &biz.Cert{ + AccountID: req.AccountID, + WebsiteID: req.WebsiteID, + DNSID: req.DNSID, + Type: req.Type, + Domains: req.Domains, + AutoRenew: req.AutoRenew, + } + if err := app.Orm.Create(cert).Error; err != nil { + return nil, err + } + return cert, nil +} + +func (r *certRepo) Update(req *request.CertUpdate) error { + return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Updates(&biz.Cert{ + AccountID: req.AccountID, + WebsiteID: req.WebsiteID, + DNSID: req.DNSID, + Type: req.Type, + Domains: req.Domains, + AutoRenew: req.AutoRenew, + }).Error +} + +func (r *certRepo) Delete(id uint) error { + return app.Orm.Model(&biz.Cert{}).Where("id = ?", id).Delete(&biz.Cert{}).Error +} + +func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + client, err := r.getClient(cert) + if err != nil { + return nil, err + } + + if cert.DNS != nil { + client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) + } else { + if cert.Website == nil { + return nil, errors.New("该证书没有关联网站,无法自动签发") + } else { + for _, domain := range cert.Domains { + if strings.Contains(domain, "*") { + return nil, errors.New("通配符域名无法使用 HTTP 验证") + } + } + conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name) + client.UseHTTP(conf, cert.Website.Path) + } + } + + ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type)) + if err != nil { + return nil, err + } + + cert.CertURL = ssl.URL + cert.Cert = string(ssl.ChainPEM) + cert.Key = string(ssl.PrivateKey) + if err = app.Orm.Save(cert).Error; err != nil { + return nil, err + } + + if cert.Website != nil { + return &ssl, r.Deploy(cert.ID, cert.WebsiteID) + } + + return &ssl, nil +} + +func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + if r.client == nil { + return nil, errors.New("请重新获取 DNS 解析记录") + } + + ssl, err := r.client.ObtainSSLManual() + if err != nil { + return nil, err + } + + cert.CertURL = ssl.URL + cert.Cert = string(ssl.ChainPEM) + cert.Key = string(ssl.PrivateKey) + if err = app.Orm.Save(cert).Error; err != nil { + return nil, err + } + + if cert.Website != nil { + return &ssl, r.Deploy(cert.ID, cert.WebsiteID) + } + + return &ssl, nil +} + +func (r *certRepo) Renew(id uint) (*acme.Certificate, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + client, err := r.getClient(cert) + if err != nil { + return nil, err + } + + if cert.CertURL == "" { + return nil, errors.New("该证书没有签发成功,无法续签") + } + + if cert.DNS != nil { + client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) + } else { + if cert.Website == nil { + return nil, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发") + } else { + for _, domain := range cert.Domains { + if strings.Contains(domain, "*") { + return nil, errors.New("通配符域名无法使用 HTTP 验证") + } + } + conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) + client.UseHTTP(conf, cert.Website.Path) + } + } + + ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type)) + if err != nil { + return nil, err + } + + cert.CertURL = ssl.URL + cert.Cert = string(ssl.ChainPEM) + cert.Key = string(ssl.PrivateKey) + if err = app.Orm.Save(cert).Error; err != nil { + return nil, err + } + + if cert.Website != nil { + return &ssl, r.Deploy(cert.ID, cert.WebsiteID) + } + + return &ssl, nil +} + +func (r *certRepo) ManualDNS(id uint) ([]acme.DNSRecord, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + client, err := r.getClient(cert) + if err != nil { + return nil, err + } + + client.UseManualDns(len(cert.Domains)) + records, err := client.GetDNSRecords(context.Background(), cert.Domains, acme.KeyType(cert.Type)) + if err != nil { + return nil, err + } + + // 15 分钟后清理客户端 + r.client = client + time.AfterFunc(15*time.Minute, func() { + r.client = nil + }) + + return records, nil +} + +func (r *certRepo) Deploy(ID, WebsiteID uint) error { + cert, err := r.Get(ID) + if err != nil { + return err + } + + if cert.Cert == "" || cert.Key == "" { + return errors.New("该证书没有签发成功,无法部署") + } + + website, err := r.websiteRepo.Get(WebsiteID) + if err != nil { + return err + } + + if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil { + return err + } + if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil { + return err + } + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil +} + +func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) { + var ca string + var eab *acme.EAB + switch cert.Account.CA { + case "letsencrypt": + ca = acme.CALetsEncrypt + case "buypass": + ca = acme.CABuypass + case "zerossl": + ca = acme.CAZeroSSL + eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded} + case "sslcom": + ca = acme.CASSLcom + eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded} + case "google": + ca = acme.CAGoogle + eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded} + } + + return acme.NewPrivateKeyAccount(cert.Account.Email, cert.Account.PrivateKey, ca, eab) +} diff --git a/internal/data/cert_account.go b/internal/data/cert_account.go new file mode 100644 index 00000000..d43e5e0b --- /dev/null +++ b/internal/data/cert_account.go @@ -0,0 +1,154 @@ +package data + +import ( + "context" + "errors" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" + "github.com/TheTNB/panel/pkg/cert" +) + +type certAccountRepo struct{} + +func NewCertAccountRepo() biz.CertAccountRepo { + return &certAccountRepo{} +} + +func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, error) { + var accounts []*biz.CertAccount + var total int64 + err := app.Orm.Model(&biz.CertAccount{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&accounts).Error + return accounts, total, err +} + +func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) { + account := new(biz.CertAccount) + err := app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).First(account).Error + return account, err +} + +func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccount, error) { + account := new(biz.CertAccount) + account.CA = req.CA + account.Email = req.Email + account.Kid = req.Kid + account.HmacEncoded = req.HmacEncoded + account.KeyType = req.KeyType + + var err error + var client *acme.Client + switch account.CA { + case "letsencrypt": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType)) + case "buypass": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType)) + case "zerossl": + eab, eabErr := r.getZeroSSLEAB(account.Email) + if eabErr != nil { + return nil, eabErr + } + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType)) + case "sslcom": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + case "google": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + default: + return nil, errors.New("CA 提供商不支持") + } + + if err != nil { + return nil, errors.New("向 CA 注册账号失败,请检查参数是否正确") + } + + privateKey, err := cert.EncodeKey(client.Account.PrivateKey) + if err != nil { + return nil, errors.New("获取私钥失败") + } + account.PrivateKey = string(privateKey) + + if err = app.Orm.Create(account).Error; err != nil { + return nil, err + } + + return account, nil +} + +func (r certAccountRepo) Update(req *request.CertAccountUpdate) error { + account, err := r.Get(req.ID) + if err != nil { + return err + } + + account.CA = req.CA + account.Email = req.Email + account.Kid = req.Kid + account.HmacEncoded = req.HmacEncoded + account.KeyType = req.KeyType + + var client *acme.Client + switch account.CA { + case "letsencrypt": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType)) + case "buypass": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType)) + case "zerossl": + eab, eabErr := r.getZeroSSLEAB(account.Email) + if eabErr != nil { + return eabErr + } + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType)) + case "sslcom": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + case "google": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + default: + return errors.New("CA 提供商不支持") + } + + if err != nil { + return errors.New("向 CA 注册账号失败,请检查参数是否正确") + } + + privateKey, err := cert.EncodeKey(client.Account.PrivateKey) + if err != nil { + return errors.New("获取私钥失败") + } + account.PrivateKey = string(privateKey) + + return app.Orm.Save(account).Error +} + +func (r certAccountRepo) Delete(id uint) error { + return app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).Delete(&biz.CertAccount{}).Error +} + +// getZeroSSLEAB 获取 ZeroSSL EAB +func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) { + type data struct { + Success bool `json:"success"` + EabKid string `json:"eab_kid"` + EabHmacKey string `json:"eab_hmac_key"` + } + client := resty.New() + client.SetTimeout(5 * time.Second) + client.SetRetryCount(2) + + resp, err := client.R().SetFormData(map[string]string{ + "email": email, + }).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email") + if err != nil || !resp.IsSuccess() { + return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") + } + eab := resp.Result().(*data) + if !eab.Success { + return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") + } + + return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil +} diff --git a/internal/data/cert_dns.go b/internal/data/cert_dns.go new file mode 100644 index 00000000..a0c2158e --- /dev/null +++ b/internal/data/cert_dns.go @@ -0,0 +1,57 @@ +package data + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type certDNSRepo struct{} + +func NewCertDNSRepo() biz.CertDNSRepo { + return &certDNSRepo{} +} + +func (r certDNSRepo) List(page, limit uint) ([]*biz.CertDNS, int64, error) { + var certDNS []*biz.CertDNS + var total int64 + err := app.Orm.Model(&biz.CertDNS{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certDNS).Error + return certDNS, total, err +} + +func (r certDNSRepo) Get(id uint) (*biz.CertDNS, error) { + certDNS := new(biz.CertDNS) + err := app.Orm.Model(&biz.CertDNS{}).Where("id = ?", id).First(certDNS).Error + return certDNS, err +} + +func (r certDNSRepo) Create(req *request.CertDNSCreate) (*biz.CertDNS, error) { + certDNS := &biz.CertDNS{ + Name: req.Name, + Type: req.Type, + Data: req.Data, + } + + if err := app.Orm.Create(certDNS).Error; err != nil { + return nil, err + } + + return certDNS, nil +} + +func (r certDNSRepo) Update(req *request.CertDNSUpdate) error { + cert, err := r.Get(req.ID) + if err != nil { + return err + } + + cert.Name = req.Name + cert.Type = req.Type + cert.Data = req.Data + + return app.Orm.Save(cert).Error +} + +func (r certDNSRepo) Delete(id uint) error { + return app.Orm.Model(&biz.CertDNS{}).Where("id = ?", id).Delete(&biz.CertDNS{}).Error +} diff --git a/internal/data/container.go b/internal/data/container.go new file mode 100644 index 00000000..700c0d93 --- /dev/null +++ b/internal/data/container.go @@ -0,0 +1,235 @@ +package data + +import ( + "context" + "fmt" + "io" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + paneltypes "github.com/TheTNB/panel/pkg/types" +) + +type containerRepo struct { + client *client.Client +} + +func NewContainerRepo(sock ...string) biz.ContainerRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerRepo{ + client: cli, + } +} + +// ListAll 列出所有容器 +func (r *containerRepo) ListAll() ([]types.Container, error) { + containers, err := r.client.ContainerList(context.Background(), container.ListOptions{ + All: true, + }) + if err != nil { + return nil, err + } + + return containers, nil +} + +// ListByNames 根据名称列出容器 +func (r *containerRepo) ListByNames(names []string) ([]types.Container, error) { + var options container.ListOptions + options.All = true + if len(names) > 0 { + var array []filters.KeyValuePair + for _, n := range names { + array = append(array, filters.Arg("name", n)) + } + options.Filters = filters.NewArgs(array...) + } + containers, err := r.client.ContainerList(context.Background(), options) + if err != nil { + return nil, err + } + + return containers, nil +} + +// Create 创建容器 +func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { + var hostConf container.HostConfig + var networkConf network.NetworkingConfig + + portMap := make(nat.PortMap) + for _, port := range req.Ports { + if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd { + return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd) + } + if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 { + return "", fmt.Errorf("端口范围不正确") + } + + count := 0 + for host := port.HostStart; host <= port.HostEnd; host++ { + bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host} + portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem} + count++ + } + } + + exposed := make(nat.PortSet) + for port := range portMap { + exposed[port] = struct{}{} + } + + if req.Network != "" { + switch req.Network { + case "host", "none", "bridge": + hostConf.NetworkMode = container.NetworkMode(req.Network) + } + networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}} + } else { + networkConf = network.NetworkingConfig{} + } + + hostConf.Privileged = req.Privileged + hostConf.AutoRemove = req.AutoRemove + hostConf.CPUShares = req.CPUShares + hostConf.PublishAllPorts = req.PublishAllPorts + hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)} + if req.RestartPolicy == "on-failure" { + hostConf.RestartPolicy.MaximumRetryCount = 5 + } + hostConf.NanoCPUs = req.CPUs * 1000000000 + hostConf.Memory = req.Memory * 1024 * 1024 + hostConf.MemorySwap = 0 + hostConf.PortBindings = portMap + hostConf.Binds = []string{} + + volumes := make(map[string]struct{}) + for _, v := range req.Volumes { + volumes[v.Container] = struct{}{} + hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode)) + } + + resp, err := r.client.ContainerCreate(context.Background(), &container.Config{ + Image: req.Image, + Env: paneltypes.KVToSlice(req.Env), + Entrypoint: req.Entrypoint, + Cmd: req.Command, + Labels: paneltypes.KVToMap(req.Labels), + ExposedPorts: exposed, + OpenStdin: req.OpenStdin, + Tty: req.Tty, + Volumes: volumes, + }, &hostConf, &networkConf, nil, req.Name) + if err != nil { + return "", err + } + + return resp.ID, err +} + +// Remove 移除容器 +func (r *containerRepo) Remove(id string) error { + return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{ + Force: true, + }) +} + +// Start 启动容器 +func (r *containerRepo) Start(id string) error { + return r.client.ContainerStart(context.Background(), id, container.StartOptions{}) +} + +// Stop 停止容器 +func (r *containerRepo) Stop(id string) error { + return r.client.ContainerStop(context.Background(), id, container.StopOptions{}) +} + +// Restart 重启容器 +func (r *containerRepo) Restart(id string) error { + return r.client.ContainerRestart(context.Background(), id, container.StopOptions{}) +} + +// Pause 暂停容器 +func (r *containerRepo) Pause(id string) error { + return r.client.ContainerPause(context.Background(), id) +} + +// Unpause 恢复容器 +func (r *containerRepo) Unpause(id string) error { + return r.client.ContainerUnpause(context.Background(), id) +} + +// Inspect 查看容器 +func (r *containerRepo) Inspect(id string) (types.ContainerJSON, error) { + return r.client.ContainerInspect(context.Background(), id) +} + +// Kill 杀死容器 +func (r *containerRepo) Kill(id string) error { + return r.client.ContainerKill(context.Background(), id, "KILL") +} + +// Rename 重命名容器 +func (r *containerRepo) Rename(id string, newName string) error { + return r.client.ContainerRename(context.Background(), id, newName) +} + +// Stats 查看容器状态 +func (r *containerRepo) Stats(id string) (container.StatsResponseReader, error) { + return r.client.ContainerStats(context.Background(), id, false) +} + +// Exist 判断容器是否存在 +func (r *containerRepo) Exist(name string) (bool, error) { + var options container.ListOptions + options.Filters = filters.NewArgs(filters.Arg("name", name)) + containers, err := r.client.ContainerList(context.Background(), options) + if err != nil { + return false, err + } + + return len(containers) > 0, nil +} + +// Update 更新容器 +func (r *containerRepo) Update(id string, config container.UpdateConfig) error { + _, err := r.client.ContainerUpdate(context.Background(), id, config) + return err +} + +// Logs 查看容器日志 +func (r *containerRepo) Logs(id string) (string, error) { + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + } + reader, err := r.client.ContainerLogs(context.Background(), id, options) + if err != nil { + return "", err + } + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return "", err + } + + return string(data), nil +} + +// Prune 清理未使用的容器 +func (r *containerRepo) Prune() error { + _, err := r.client.ContainersPrune(context.Background(), filters.NewArgs()) + return err +} diff --git a/internal/data/container_image.go b/internal/data/container_image.go new file mode 100644 index 00000000..a258d104 --- /dev/null +++ b/internal/data/container_image.go @@ -0,0 +1,97 @@ +package data + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/client" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type containerImageRepo struct { + client *client.Client +} + +func NewContainerImageRepo(sock ...string) biz.ContainerImageRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerImageRepo{ + client: cli, + } +} + +// List 列出镜像 +func (r *containerImageRepo) List() ([]image.Summary, error) { + return r.client.ImageList(context.Background(), image.ListOptions{ + All: true, + }) +} + +// Exist 判断镜像是否存在 +func (r *containerImageRepo) Exist(id string) (bool, error) { + var options image.ListOptions + options.Filters = filters.NewArgs(filters.Arg("reference", id)) + images, err := r.client.ImageList(context.Background(), options) + if err != nil { + return false, err + } + + return len(images) > 0, nil +} + +// Pull 拉取镜像 +func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { + options := image.PullOptions{} + if req.Auth { + authConfig := registry.AuthConfig{ + Username: req.Username, + Password: req.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return err + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + options.RegistryAuth = authStr + } + + out, err := r.client.ImagePull(context.Background(), req.Name, options) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(io.Discard, out) + return err +} + +// Remove 删除镜像 +func (r *containerImageRepo) Remove(id string) error { + _, err := r.client.ImageRemove(context.Background(), id, image.RemoveOptions{ + Force: true, + PruneChildren: true, + }) + return err +} + +// Prune 清理未使用的镜像 +func (r *containerImageRepo) Prune() error { + _, err := r.client.ImagesPrune(context.Background(), filters.NewArgs()) + return err +} + +// Inspect 查看镜像 +func (r *containerImageRepo) Inspect(id string) (types.ImageInspect, error) { + img, _, err := r.client.ImageInspectWithRaw(context.Background(), id) + return img, err +} diff --git a/internal/data/container_network.go b/internal/data/container_network.go new file mode 100644 index 00000000..b44e9985 --- /dev/null +++ b/internal/data/container_network.go @@ -0,0 +1,104 @@ +package data + +import ( + "context" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" +) + +type containerNetworkRepo struct { + client *client.Client +} + +func NewContainerNetworkRepo(sock ...string) biz.ContainerNetworkRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerNetworkRepo{ + client: cli, + } +} + +// List 列出网络 +func (r *containerNetworkRepo) List() ([]network.Inspect, error) { + return r.client.NetworkList(context.Background(), network.ListOptions{}) +} + +// Create 创建网络 +func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (string, error) { + var ipamConfigs []network.IPAMConfig + if req.Ipv4.Enabled { + ipamConfigs = append(ipamConfigs, network.IPAMConfig{ + Subnet: req.Ipv4.Subnet, + Gateway: req.Ipv4.Gateway, + IPRange: req.Ipv4.IPRange, + }) + } + if req.Ipv6.Enabled { + ipamConfigs = append(ipamConfigs, network.IPAMConfig{ + Subnet: req.Ipv6.Subnet, + Gateway: req.Ipv6.Gateway, + IPRange: req.Ipv6.IPRange, + }) + } + + options := network.CreateOptions{ + EnableIPv6: &req.Ipv6.Enabled, + Driver: req.Driver, + Options: types.KVToMap(req.Options), + Labels: types.KVToMap(req.Labels), + } + if len(ipamConfigs) > 0 { + options.IPAM = &network.IPAM{ + Config: ipamConfigs, + } + } + + resp, err := r.client.NetworkCreate(context.Background(), req.Name, options) + return resp.ID, err +} + +// Remove 删除网络 +func (r *containerNetworkRepo) Remove(id string) error { + return r.client.NetworkRemove(context.Background(), id) +} + +// Exist 判断网络是否存在 +func (r *containerNetworkRepo) Exist(name string) (bool, error) { + var options network.ListOptions + options.Filters = filters.NewArgs(filters.Arg("name", name)) + networks, err := r.client.NetworkList(context.Background(), options) + if err != nil { + return false, err + } + + return len(networks) > 0, nil +} + +// Inspect 查看网络 +func (r *containerNetworkRepo) Inspect(id string) (network.Inspect, error) { + return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{}) +} + +// Connect 连接网络 +func (r *containerNetworkRepo) Connect(networkID string, containerID string) error { + return r.client.NetworkConnect(context.Background(), networkID, containerID, nil) +} + +// Disconnect 断开网络 +func (r *containerNetworkRepo) Disconnect(networkID string, containerID string) error { + return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true) +} + +// Prune 清理未使用的网络 +func (r *containerNetworkRepo) Prune() error { + _, err := r.client.NetworksPrune(context.Background(), filters.NewArgs()) + return err +} diff --git a/internal/data/container_volume.go b/internal/data/container_volume.go new file mode 100644 index 00000000..a6db2181 --- /dev/null +++ b/internal/data/container_volume.go @@ -0,0 +1,74 @@ +package data + +import ( + "context" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" +) + +type containerVolumeRepo struct { + client *client.Client +} + +func NewContainerVolumeRepo(sock ...string) biz.ContainerVolumeRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerVolumeRepo{ + client: cli, + } +} + +// List 列出存储卷 +func (r *containerVolumeRepo) List() ([]*volume.Volume, error) { + volumes, err := r.client.VolumeList(context.Background(), volume.ListOptions{}) + if err != nil { + return nil, err + } + return volumes.Volumes, err +} + +// Create 创建存储卷 +func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (volume.Volume, error) { + return r.client.VolumeCreate(context.Background(), volume.CreateOptions{ + Name: req.Name, + Driver: req.Driver, + DriverOpts: types.KVToMap(req.Options), + Labels: types.KVToMap(req.Labels), + }) +} + +// Exist 判断存储卷是否存在 +func (r *containerVolumeRepo) Exist(id string) (bool, error) { + var options volume.ListOptions + options.Filters = filters.NewArgs(filters.Arg("name", id)) + volumes, err := r.client.VolumeList(context.Background(), options) + if err != nil { + return false, err + } + + return len(volumes.Volumes) > 0, nil +} + +// Inspect 查看存储卷 +func (r *containerVolumeRepo) Inspect(id string) (volume.Volume, error) { + return r.client.VolumeInspect(context.Background(), id) +} + +// Remove 删除存储卷 +func (r *containerVolumeRepo) Remove(id string) error { + return r.client.VolumeRemove(context.Background(), id, true) +} + +// Prune 清理未使用的存储卷 +func (r *containerVolumeRepo) Prune() error { + _, err := r.client.VolumesPrune(context.Background(), filters.NewArgs()) + return err +} diff --git a/internal/data/cron.go b/internal/data/cron.go new file mode 100644 index 00000000..df31f2b3 --- /dev/null +++ b/internal/data/cron.go @@ -0,0 +1,253 @@ +package data + +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "strconv" + + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/os" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type cronRepo struct { + settingRepo biz.SettingRepo +} + +func NewCronRepo() biz.CronRepo { + return &cronRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r *cronRepo) Count() (int64, error) { + var count int64 + if err := app.Orm.Model(&biz.Cron{}).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +func (r *cronRepo) List(page, limit uint) ([]*biz.Cron, int64, error) { + var cron []*biz.Cron + var total int64 + err := app.Orm.Model(&biz.Cert{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&cron).Error + return cron, total, err +} + +func (r *cronRepo) Get(id uint) (*biz.Cron, error) { + cron := new(biz.Cron) + if err := app.Orm.Where("id = ?", id).First(cron).Error; err != nil { + return nil, err + } + + return cron, nil +} + +func (r *cronRepo) Create(req *request.CronCreate) error { + if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) { + return errors.New("时间格式错误") + } + + var script string + if req.Type == "backup" { + if len(req.BackupPath) == 0 { + req.BackupPath, _ = r.settingRepo.Get(biz.SettingKeyBackupPath) + if len(req.BackupPath) == 0 { + return errors.New("备份路径不能为空") + } + req.BackupPath = filepath.Join(req.BackupPath, req.BackupType) + } + script = fmt.Sprintf(`#!/bin/bash +export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH + +# 耗子面板 - 数据备份脚本 + +type=%s +path=%s +name=%s +save=%d + +# 执行备份 +panel backup ${type} ${name} ${path} ${save} 2>&1 +`, req.BackupType, req.BackupPath, req.Target, req.Save) + } + if req.Type == "cutoff" { + script = fmt.Sprintf(`#!/bin/bash +export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH + +# 耗子面板 - 日志切割脚本 + +name=%s +save=%d + +# 执行切割 +panel cutoff ${name} ${save} 2>&1 +`, req.Target, req.Save) + } + + shellDir := fmt.Sprintf("%s/server/cron/", app.Root) + shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root) + if !io.Exists(shellDir) { + return errors.New("计划任务目录不存在") + } + if !io.Exists(shellLogDir) { + return errors.New("计划任务日志目录不存在") + } + shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + str.RandomString(16) + if err := io.Write(filepath.Join(shellDir, shellFile+".sh"), script, 0700); err != nil { + return errors.New(err.Error()) + } + if out, err := shell.Execf("dos2unix %s%s.sh", shellDir, shellFile); err != nil { + return errors.New(out) + } + + cron := new(biz.Cron) + cron.Name = req.Name + cron.Type = req.Type + cron.Status = true + cron.Time = req.Time + cron.Shell = shellDir + shellFile + ".sh" + cron.Log = shellLogDir + shellFile + ".log" + + if err := app.Orm.Create(cron).Error; err != nil { + return err + } + if err := r.addToSystem(cron); err != nil { + return err + } + + return nil +} + +func (r *cronRepo) Update(req *request.CronUpdate) error { + cron, err := r.Get(req.ID) + if err != nil { + return err + } + + if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) { + return errors.New("时间格式错误") + } + + if !cron.Status { + return errors.New("计划任务已禁用") + } + + cron.Time = req.Time + cron.Name = req.Name + if err = app.Orm.Save(cron).Error; err != nil { + return err + } + + if err = io.Write(cron.Shell, req.Script, 0700); err != nil { + return err + } + if out, err := shell.Execf("dos2unix %s", cron.Shell); err != nil { + return errors.New(out) + } + + if err = r.deleteFromSystem(cron); err != nil { + return err + } + if cron.Status { + if err = r.addToSystem(cron); err != nil { + return err + } + } + + return nil +} + +func (r *cronRepo) Delete(id uint) error { + cron, err := r.Get(id) + if err != nil { + return err + } + + if err = r.deleteFromSystem(cron); err != nil { + return err + } + if err = io.Remove(cron.Shell); err != nil { + return err + } + + return app.Orm.Delete(cron).Error +} + +func (r *cronRepo) Status(id uint, status bool) error { + cron, err := r.Get(id) + if err != nil { + return err + } + + if err = r.deleteFromSystem(cron); err != nil { + return err + } + if status { + return r.addToSystem(cron) + } + + cron.Status = status + + return app.Orm.Save(cron).Error +} + +func (r *cronRepo) Log(id uint) (string, error) { + cron, err := r.Get(id) + if err != nil { + return "", err + } + + if !io.Exists(cron.Log) { + return "", errors.New("日志文件不存在") + } + + log, err := shell.Execf("tail -n 1000 '%s'", cron.Log) + if err != nil { + return "", err + } + + return log, nil +} + +// addToSystem 添加到系统 +func (r *cronRepo) addToSystem(cron *biz.Cron) error { + if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { + return err + } + + return r.restartCron() +} + +// deleteFromSystem 从系统中删除 +func (r *cronRepo) deleteFromSystem(cron *biz.Cron) error { + if _, err := shell.Execf(`( crontab -l | grep -v -F "%s %s >> %s 2>&1" ) | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { + return err + } + + return r.restartCron() +} + +// restartCron 重启 cron 服务 +func (r *cronRepo) restartCron() error { + if os.IsRHEL() { + return systemctl.Restart("crond") + } + + if os.IsDebian() || os.IsUbuntu() { + return systemctl.Restart("cron") + } + + return errors.New("不支持的系统") +} diff --git a/internal/data/monitor.go b/internal/data/monitor.go new file mode 100644 index 00000000..48c53048 --- /dev/null +++ b/internal/data/monitor.go @@ -0,0 +1,67 @@ +package data + +import ( + "errors" + + "github.com/golang-module/carbon/v2" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type monitorRepo struct { + settingRepo biz.SettingRepo +} + +func NewMonitorRepo() biz.MonitorRepo { + return &monitorRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r monitorRepo) GetSetting() (*request.MonitorSetting, error) { + monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor) + if err != nil { + return nil, err + } + monitorDays, err := r.settingRepo.Get(biz.SettingKeyMonitorDays) + if err != nil { + return nil, err + } + + setting := new(request.MonitorSetting) + setting.Enabled = cast.ToBool(monitor) + setting.Days = cast.ToInt(monitorDays) + + return setting, nil +} + +func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error { + if err := r.settingRepo.Set(biz.SettingKeyMonitor, cast.ToString(setting.Enabled)); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil { + return err + } + + return nil +} + +func (r monitorRepo) Clear() error { + return app.Orm.Delete(&biz.Monitor{}).Error +} + +func (r monitorRepo) List(start, end carbon.Carbon) ([]*biz.Monitor, error) { + var monitors []*biz.Monitor + if err := app.Orm.Where("created_at BETWEEN ? AND ?", start, end).Find(&monitors).Error; err != nil { + return nil, err + } + + if len(monitors) == 0 { + return nil, errors.New("没有找到数据") + } + + return monitors, nil +} diff --git a/internal/data/plugin.go b/internal/data/plugin.go new file mode 100644 index 00000000..b3dbe655 --- /dev/null +++ b/internal/data/plugin.go @@ -0,0 +1,226 @@ +package data + +import ( + "errors" + "fmt" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/job" + "github.com/TheTNB/panel/pkg/pluginloader" + "github.com/TheTNB/panel/pkg/types" +) + +type pluginRepo struct { + taskRepo biz.TaskRepo +} + +func NewPluginRepo() biz.PluginRepo { + return &pluginRepo{ + taskRepo: NewTaskRepo(), + } +} + +func (r *pluginRepo) All() []*types.Plugin { + return pluginloader.All() +} + +func (r *pluginRepo) Installed() ([]*biz.Plugin, error) { + var plugins []*biz.Plugin + if err := app.Orm.Find(&plugins).Error; err != nil { + return nil, err + } + + return plugins, nil + +} + +func (r *pluginRepo) Get(slug string) (*types.Plugin, error) { + return pluginloader.Get(slug) +} + +func (r *pluginRepo) GetInstalled(slug string) (*biz.Plugin, error) { + plugin := new(biz.Plugin) + if err := app.Orm.Where("slug = ?", slug).First(plugin).Error; err != nil { + return nil, err + } + + return plugin, nil +} + +func (r *pluginRepo) GetInstalledAll(cond ...string) ([]*biz.Plugin, error) { + var plugins []*biz.Plugin + if err := app.Orm.Where(cond).Find(&plugins).Error; err != nil { + return nil, err + } + + return plugins, nil +} + +func (r *pluginRepo) IsInstalled(cond ...string) (bool, error) { + var count int64 + if err := app.Orm.Model(&biz.Plugin{}).Where(cond).Count(&count).Error; err != nil { + return false, err + } + + return count > 0, nil +} + +func (r *pluginRepo) Install(slug string) error { + plugin, err := r.Get(slug) + if err != nil { + return err + } + installedPlugins, err := r.Installed() + if err != nil { + return err + } + + if installed, _ := r.IsInstalled("slug = ?", slug); installed { + return errors.New("插件已安装") + } + + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + pluginsMap[p.Slug] = true + } + + for _, require := range plugin.Requires { + _, requireFound := pluginsMap[require] + if !requireFound { + return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") + } + } + + for _, exclude := range plugin.Excludes { + _, excludeFound := pluginsMap[exclude] + if excludeFound { + return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") + } + } + + task := new(biz.Task) + task.Name = "安装插件 " + plugin.Name + task.Status = biz.TaskStatusWaiting + task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", plugin.Install, plugin.Slug) + task.Log = "/tmp/" + plugin.Slug + ".log" + + if err = app.Orm.Create(task).Error; err != nil { + return err + } + err = app.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ + task.ID, + }) + + return err +} + +func (r *pluginRepo) Uninstall(slug string) error { + plugin, err := r.Get(slug) + if err != nil { + return err + } + installedPlugins, err := r.Installed() + if err != nil { + return err + } + + if installed, _ := r.IsInstalled("slug = ?", slug); !installed { + return errors.New("插件未安装") + } + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + pluginsMap[p.Slug] = true + } + + for _, require := range plugin.Requires { + _, requireFound := pluginsMap[require] + if !requireFound { + return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") + } + } + + for _, exclude := range plugin.Excludes { + _, excludeFound := pluginsMap[exclude] + if excludeFound { + return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") + } + } + + task := new(biz.Task) + task.Name = "卸载插件 " + plugin.Name + task.Status = biz.TaskStatusWaiting + task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", plugin.Uninstall, plugin.Slug) + task.Log = "/tmp/" + plugin.Slug + ".log" + + if err = app.Orm.Create(task).Error; err != nil { + return err + } + err = app.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ + task.ID, + }) + + return err +} + +func (r *pluginRepo) Update(slug string) error { + plugin, err := r.Get(slug) + if err != nil { + return err + } + installedPlugins, err := r.Installed() + if err != nil { + return err + } + + if installed, _ := r.IsInstalled("slug = ?", slug); !installed { + return errors.New("插件未安装") + } + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + pluginsMap[p.Slug] = true + } + + for _, require := range plugin.Requires { + _, requireFound := pluginsMap[require] + if !requireFound { + return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") + } + } + + for _, exclude := range plugin.Excludes { + _, excludeFound := pluginsMap[exclude] + if excludeFound { + return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") + } + } + + task := new(biz.Task) + task.Name = "更新插件 " + plugin.Name + task.Status = biz.TaskStatusWaiting + task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", plugin.Update, plugin.Slug) + task.Log = "/tmp/" + plugin.Slug + ".log" + + if err = app.Orm.Create(task).Error; err != nil { + return err + } + err = app.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ + task.ID, + }) + + return err +} + +func (r *pluginRepo) UpdateShow(slug string, show bool) error { + plugin, err := r.GetInstalled(slug) + if err != nil { + return err + } + + plugin.Show = show + + return app.Orm.Save(plugin).Error +} diff --git a/internal/data/safe.go b/internal/data/safe.go new file mode 100644 index 00000000..26cd66a7 --- /dev/null +++ b/internal/data/safe.go @@ -0,0 +1,120 @@ +package data + +import ( + "errors" + "strings" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/os" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type safeRepo struct { + ssh string +} + +func NewSafeRepo() biz.SafeRepo { + var ssh string + if os.IsRHEL() { + ssh = "sshd" + } else { + ssh = "ssh" + } + + return &safeRepo{ + ssh: ssh, + } +} + +func (r *safeRepo) GetSSH() (uint, bool, error) { + out, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") + if err != nil { + return 0, false, err + } + + running, err := systemctl.Status(r.ssh) + if err != nil { + return 0, false, err + } + + return cast.ToUint(out), running, nil +} + +func (r *safeRepo) UpdateSSH(port uint, status bool) error { + oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") + if err != nil { + return err + } + + _, _ = shell.Execf("sed -i 's/#Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) + _, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) + + if !status { + return systemctl.Stop(r.ssh) + } + + return systemctl.Restart(r.ssh) +} + +func (r *safeRepo) GetPingStatus() (bool, error) { + if os.IsRHEL() { + out, err := shell.Execf(`firewall-cmd --list-all`) + if err != nil { + return true, errors.New(out) + } + + if !strings.Contains(out, `rule protocol value="icmp" drop`) { + return true, nil + } else { + return false, nil + } + } else { + config, err := io.Read("/etc/ufw/before.rules") + if err != nil { + return true, err + } + if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") { + return true, nil + } else { + return false, nil + } + } +} + +func (r *safeRepo) UpdatePingStatus(status bool) error { + var out string + var err error + if os.IsRHEL() { + if status { + out, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`) + } else { + out, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`) + } + } else { + if status { + out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`) + } else { + out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`) + } + } + + if err != nil { + return errors.New(out) + } + + if os.IsRHEL() { + out, err = shell.Execf(`firewall-cmd --reload`) + } else { + out, err = shell.Execf(`ufw reload`) + } + + if err != nil { + return errors.New(out) + } + + return nil +} diff --git a/internal/data/setting.go b/internal/data/setting.go new file mode 100644 index 00000000..42b0b68e --- /dev/null +++ b/internal/data/setting.go @@ -0,0 +1,68 @@ +package data + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type settingRepo struct{} + +func NewSettingRepo() biz.SettingRepo { + return &settingRepo{} +} + +func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil { + return "", err + } + + if setting.Value == "" && len(defaultValue) > 0 { + return defaultValue[0], nil + } + + return setting.Value, nil +} + +func (r *settingRepo) Set(key biz.SettingKey, value string) error { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil { + return err + } + + setting.Value = value + return app.Orm.Save(setting).Error +} + +func (r *settingRepo) Delete(key biz.SettingKey) error { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", key).Delete(setting).Error; err != nil { + return err + } + + return nil +} + +func (r *settingRepo) GetPanelSetting() (*request.PanelSetting, error) { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(setting).Error; err != nil { + return nil, err + } + + // TODO fix + + return &request.PanelSetting{ + Name: setting.Value, + }, nil +} + +func (r *settingRepo) UpdatePanelSetting(setting *request.PanelSetting) error { + if err := r.Set(biz.SettingKeyName, setting.Name); err != nil { + return err + } + + // TODO fix + + return nil +} diff --git a/internal/data/ssh.go b/internal/data/ssh.go new file mode 100644 index 00000000..165ba55e --- /dev/null +++ b/internal/data/ssh.go @@ -0,0 +1,54 @@ +package data + +import ( + "errors" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type sshRepo struct { + settingRepo biz.SettingRepo +} + +func NewSSHRepo() biz.SSHRepo { + return &sshRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r *sshRepo) GetInfo() (map[string]any, error) { + host, _ := r.settingRepo.Get(biz.SettingKeySshHost) + port, _ := r.settingRepo.Get(biz.SettingKeySshPort) + user, _ := r.settingRepo.Get(biz.SettingKeySshUser) + password, _ := r.settingRepo.Get(biz.SettingKeySshPassword) + if len(host) == 0 || len(user) == 0 || len(password) == 0 { + return nil, errors.New("SSH 配置不完整") + } + + return map[string]any{ + "host": host, + "port": cast.ToInt(port), + "user": user, + "password": password, + }, nil +} + +func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error { + if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeySshPort, req.Port); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil { + return err + } + + return nil +} diff --git a/internal/data/task.go b/internal/data/task.go new file mode 100644 index 00000000..82fdc5bd --- /dev/null +++ b/internal/data/task.go @@ -0,0 +1,39 @@ +package data + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" +) + +type taskRepo struct{} + +func NewTaskRepo() biz.TaskRepo { + return &taskRepo{} +} + +func (r *taskRepo) HasRunningTask() bool { + var count int64 + app.Orm.Model(&biz.Task{}).Where("status = ?", biz.TaskStatusRunning).Or("status = ?", biz.TaskStatusWaiting).Count(&count) + return count > 0 +} + +func (r *taskRepo) List(page, limit uint) ([]*biz.Task, int64, error) { + var tasks []*biz.Task + var total int64 + err := app.Orm.Model(&biz.Task{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&tasks).Error + return tasks, total, err +} + +func (r *taskRepo) Get(id uint) (*biz.Task, error) { + task := new(biz.Task) + err := app.Orm.Model(&biz.Task{}).Where("id = ?", id).First(task).Error + return task, err +} + +func (r *taskRepo) Delete(id uint) error { + return app.Orm.Model(&biz.Task{}).Where("id = ?", id).Delete(&biz.Task{}).Error +} + +func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error { + return app.Orm.Model(&biz.Task{}).Where("id = ?", id).Update("status", status).Error +} diff --git a/internal/data/user.go b/internal/data/user.go new file mode 100644 index 00000000..79b45b13 --- /dev/null +++ b/internal/data/user.go @@ -0,0 +1,51 @@ +package data + +import ( + "errors" + + "github.com/go-rat/utils/hash" + "gorm.io/gorm" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" +) + +type userRepo struct { + hasher hash.Hasher +} + +func NewUserRepo() biz.UserRepo { + return &userRepo{ + hasher: hash.NewArgon2id(), + } +} + +func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) { + user := new(biz.User) + if err := app.Orm.Where("username = ?", username).First(user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户名或密码错误") + } else { + return nil, err + } + } + + if !r.hasher.Check(password, user.Password) { + return nil, errors.New("用户名或密码错误") + } + + return user, nil +} + +func (r *userRepo) Get(id uint) (*biz.User, error) { + user := new(biz.User) + if err := app.Orm.First(user, id).Error; err != nil { + return nil, err + } + + return user, nil +} + +func (r *userRepo) Save(user *biz.User) error { + return app.Orm.Save(user).Error +} diff --git a/internal/data/website.go b/internal/data/website.go new file mode 100644 index 00000000..31e62190 --- /dev/null +++ b/internal/data/website.go @@ -0,0 +1,784 @@ +package data + +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "slices" + "strconv" + "strings" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/embed" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/cert" + "github.com/TheTNB/panel/pkg/db" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" + "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" +) + +type websiteRepo struct { + settingRepo biz.SettingRepo +} + +func NewWebsiteRepo() biz.WebsiteRepo { + return &websiteRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r *websiteRepo) UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error { + if err := io.Write(filepath.Join(app.Root, "server/openresty/html/index.html"), req.Index, 0644); err != nil { + return err + } + if err := io.Write(filepath.Join(app.Root, "server/openresty/html/stop.html"), req.Stop, 0644); err != nil { + return err + } + + return systemctl.Reload("openresty") +} + +func (r *websiteRepo) Count() (int64, error) { + var count int64 + if err := app.Orm.Model(&biz.Website{}).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(website).Error; err != nil { + return nil, err + } + + config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { + return nil, err + } + + setting := new(types.WebsiteSetting) + setting.Name = website.Name + setting.Path = website.Path + setting.SSL = website.SSL + setting.PHP = strconv.Itoa(website.PHP) + setting.Raw = config + + portStr := str.Cut(config, "# port标记位开始", "# port标记位结束") + matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1) + for _, match := range matches { + if len(match) < 2 { + continue + } + // 跳过 ipv6 + if strings.Contains(match[1], "[::]") { + continue + } + + // 处理 443 ssl 之类的情况 + ports := strings.Fields(match[1]) + if len(ports) == 0 { + continue + } + if !slices.Contains(setting.Ports, ports[0]) { + setting.Ports = append(setting.Ports, ports[0]) + } + if len(ports) > 1 && ports[1] == "ssl" { + setting.SSLPorts = append(setting.SSLPorts, ports[0]) + } else if len(ports) > 1 && ports[1] == "quic" { + setting.QUICPorts = append(setting.QUICPorts, ports[0]) + } + } + serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束") + match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName) + if len(match) > 1 { + setting.Domains = strings.Split(match[1], " ") + } + root := str.Cut(config, "# root标记位开始", "# root标记位结束") + match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root) + if len(match) > 1 { + setting.Root = match[1] + } + index := str.Cut(config, "# index标记位开始", "# index标记位结束") + match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index) + if len(match) > 1 { + setting.Index = match[1] + } + + if io.Exists(filepath.Join(setting.Root, ".user.ini")) { + userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini")) + if strings.Contains(userIni, "open_basedir") { + setting.OpenBasedir = true + } + } + + crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")) + setting.SSLCertificate = crt + key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")) + setting.SSLCertificateKey = key + if setting.SSL { + ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束") + setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位") + setting.HSTS = strings.Contains(ssl, "# hsts标记位") + setting.OCSP = strings.Contains(ssl, "# ocsp标记位") + } + + // 解析证书信息 + if decode, err := cert.ParseCert(crt); err == nil { + setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05") + setting.SSLNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05") + setting.SSLIssuer = decode.Issuer.CommonName + setting.SSLOCSPServer = decode.OCSPServer + setting.SSLDNSNames = decode.DNSNames + } + + waf := str.Cut(config, "# waf标记位开始", "# waf标记位结束") + setting.Waf = strings.Contains(waf, "waf on;") + match = regexp.MustCompile(`waf_mode\s+([^;]*);?`).FindStringSubmatch(waf) + if len(match) > 1 { + setting.WafMode = match[1] + } + match = regexp.MustCompile(`waf_cc_deny\s+([^;]*);?`).FindStringSubmatch(waf) + if len(match) > 1 { + setting.WafCcDeny = match[1] + } + match = regexp.MustCompile(`waf_cache\s+([^;]*);?`).FindStringSubmatch(waf) + if len(match) > 1 { + setting.WafCache = match[1] + } + + rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) + setting.Rewrite = rewrite + log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name) + setting.Log = log + + return setting, err +} + +func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) { + var websites []*biz.Website + var total int64 + + if err := app.Orm.Model(&biz.Website{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := app.Orm.Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&websites).Error; err != nil { + return nil, 0, err + } + + return websites, total, nil +} + +func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { + w := &biz.Website{ + Name: req.Name, + Status: true, + Path: req.Path, + PHP: cast.ToInt(req.PHP), + SSL: false, + } + if err := app.Orm.Create(w).Error; err != nil { + return nil, err + } + + if err := io.Mkdir(req.Path, 0755); err != nil { + return nil, err + } + + index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html")) + if err != nil { + return nil, fmt.Errorf("获取index模板文件失败: %w", err) + } + if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil { + return nil, err + } + + notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html")) + if err != nil { + return nil, fmt.Errorf("获取404模板文件失败: %w", err) + } + if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil { + return nil, err + } + + portList := "" + domainList := "" + portUsed := make(map[uint]bool) + domainUsed := make(map[string]bool) + + for i, port := range req.Ports { + if _, ok := portUsed[port]; !ok { + if i == len(req.Ports)-1 { + portList += " listen " + cast.ToString(port) + ";\n" + portList += " listen [::]:" + cast.ToString(port) + ";" + } else { + portList += " listen " + cast.ToString(port) + ";\n" + portList += " listen [::]:" + cast.ToString(port) + ";\n" + } + portUsed[port] = true + } + } + for _, domain := range req.Domains { + if _, ok := domainUsed[domain]; !ok { + domainList += " " + domain + domainUsed[domain] = true + } + } + + nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! +# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 +server +{ + # port标记位开始 +%s + # port标记位结束 + # server_name标记位开始 + server_name%s; + # server_name标记位结束 + # index标记位开始 + index index.php index.html; + # index标记位结束 + # root标记位开始 + root %s; + # root标记位结束 + + # ssl标记位开始 + # ssl标记位结束 + + # php标记位开始 + include enable-php-%s.conf; + # php标记位结束 + + # waf标记位开始 + waf off; + waf_rule_path %s/server/openresty/ngx_waf/assets/rules/; + waf_mode DYNAMIC; + waf_cc_deny rate=1000r/m duration=60m; + waf_cache capacity=50; + # waf标记位结束 + + # 错误页配置,可自行设置 + error_page 404 /404.html; + #error_page 502 /502.html; + + # acme证书签发配置,不可修改 + include %s/server/vhost/acme/%s.conf; + + # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 + include %s/server/vhost/rewrite/%s.conf; + + # 面板默认禁止访问部分敏感目录,可自行修改 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) + { + return 404; + } + # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 + location ~ .*\.(js|css)$ + { + expires 1h; + error_log /dev/null; + access_log /dev/null; + } + + access_log %s/wwwlogs/%s.log; + error_log %s/wwwlogs/%s.log; +} +`, portList, domainList, req.Path, req.PHP, app.Root, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name) + + if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".key"), "", 0644); err != nil { + return nil, err + } + + if err = io.Chmod(req.Path, 0755); err != nil { + return nil, err + } + if err = io.Chown(req.Path, "www", "www"); err != nil { + return nil, err + } + + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return nil, err + } + + rootPassword, err := r.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + if err == nil && req.DB && req.DBType == "mysql" { + mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") + if err != nil { + return nil, err + } + if err = mysql.DatabaseCreate(req.DBName); err != nil { + return nil, err + } + if err = mysql.UserCreate(req.DBUser, req.DBPassword); err != nil { + return nil, err + } + if err = mysql.PrivilegesGrant(req.DBUser, req.DBName); err != nil { + return nil, err + } + } + if req.DB && req.DBType == "postgresql" { + _, _ = shell.Execf(`echo "CREATE DATABASE '%s';" | su - postgres -c "psql"`, req.DBName) + _, _ = shell.Execf(`echo "CREATE USER '%s' WITH PASSWORD '%s';" | su - postgres -c "psql"`, req.DBUser, req.DBPassword) + _, _ = shell.Execf(`echo "ALTER DATABASE '%s' OWNER TO '%s';" | su - postgres -c "psql"`, req.DBName, req.DBUser) + _, _ = shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE '%s' TO '%s';" | su - postgres -c "psql"`, req.DBName, req.DBUser) + userConfig := "host " + req.DBName + " " + req.DBUser + " 127.0.0.1/32 scram-sha-256" + _, _ = shell.Execf(`echo "`+userConfig+`" >> %s/server/postgresql/data/pg_hba.conf`, app.Root) + _ = systemctl.Reload("postgresql") + } + + return w, nil +} + +func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { + website := new(biz.Website) + if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil { + return err + } + + if !website.Status { + return errors.New("网站已停用,请先启用") + } + + // 原文 + raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { + return err + } + if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) { + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil { + return err + } + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil + } + + // 目录 + path := req.Path + if !io.Exists(path) { + return errors.New("网站目录不存在") + } + website.Path = path + + // 域名 + domain := "server_name" + domains := req.Domains + for _, v := range domains { + if v == "" { + continue + } + domain += " " + v + } + domain += ";" + domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束") + if len(strings.TrimSpace(domainConfigOld)) == 0 { + return errors.New("配置文件中缺少server_name标记位") + } + raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1) + + // 端口 + var portConf strings.Builder + ports := req.Ports + for _, port := range ports { + https := "" + quic := false + if slices.Contains(req.SSLPorts, port) { + https = " ssl" + if slices.Contains(req.QUICPorts, port) { + quic = true + } + } + + portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https)) + portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https)) + if quic { + portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic")) + portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic")) + } + } + portConf.WriteString(" ") + portConfNew := portConf.String() + portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束") + if len(strings.TrimSpace(portConfOld)) == 0 { + return errors.New("配置文件中缺少port标记位") + } + raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1) + + // 运行目录 + root := str.Cut(raw, "# root标记位开始", "# root标记位结束") + if len(strings.TrimSpace(root)) == 0 { + return errors.New("配置文件中缺少root标记位") + } + match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root) + if len(match) != 2 { + return errors.New("配置文件中root标记位格式错误") + } + rootNew := strings.Replace(root, match[1], req.Root, -1) + raw = strings.Replace(raw, root, rootNew, -1) + + // 默认文件 + index := str.Cut(raw, "# index标记位开始", "# index标记位结束") + if len(strings.TrimSpace(index)) == 0 { + return errors.New("配置文件中缺少index标记位") + } + match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index) + if len(match) != 2 { + return errors.New("配置文件中index标记位格式错误") + } + indexNew := strings.Replace(index, match[1], req.Index, -1) + raw = strings.Replace(raw, index, indexNew, -1) + + // 防跨站 + if !strings.HasSuffix(req.Root, "/") { + req.Root += "/" + } + if req.OpenBasedir { + if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil { + return err + } + } else { + if io.Exists(req.Root + ".user.ini") { + if err = io.Remove(req.Root + ".user.ini"); err != nil { + return err + } + } + } + + // WAF + wafStr := "off" + if req.Waf { + wafStr = "on" + } + wafConfig := fmt.Sprintf(`# waf标记位开始 + waf %s; + waf_rule_path %s/server/openresty/ngx_waf/assets/rules/; + waf_mode %s; + waf_cc_deny %s; + waf_cache %s; + `, wafStr, app.Root, req.WafMode, req.WafCcDeny, req.WafCache) + wafConfigOld := str.Cut(raw, "# waf标记位开始", "# waf标记位结束") + if len(strings.TrimSpace(wafConfigOld)) != 0 { + raw = strings.Replace(raw, wafConfigOld, "", -1) + } + raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1) + + // SSL + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil { + return err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil { + return err + } + website.SSL = req.SSL + if req.SSL { + if _, err = cert.ParseCert(req.SSLCertificate); err != nil { + return errors.New("TLS证书格式错误") + } + if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil { + return errors.New("TLS私钥格式错误") + } + sslConfig := fmt.Sprintf(`# ssl标记位开始 + ssl_certificate %s/server/vhost/ssl/%s.pem; + ssl_certificate_key %s/server/vhost/ssl/%s.key; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_early_data on; + `, app.Root, website.Name, app.Root, website.Name) + if req.HTTPRedirect { + sslConfig += `# http重定向标记位开始 + if ($server_port !~ 443){ + return 301 https://$host$request_uri; + } + error_page 497 https://$host$request_uri; + # http重定向标记位结束 + ` + } + if req.HSTS { + sslConfig += `# hsts标记位开始 + add_header Strict-Transport-Security "max-age=63072000" always; + # hsts标记位结束 + ` + } + if req.OCSP { + sslConfig += `# ocsp标记位开始 + ssl_stapling on; + ssl_stapling_verify on; + # ocsp标记位结束 + ` + } + sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") + if len(strings.TrimSpace(sslConfigOld)) != 0 { + raw = strings.Replace(raw, sslConfigOld, "", -1) + } + raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1) + } else { + sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") + if len(strings.TrimSpace(sslConfigOld)) != 0 { + raw = strings.Replace(raw, sslConfigOld, "\n ", -1) + } + } + + if website.PHP != req.PHP { + website.PHP = req.PHP + phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束") + phpConfig := ` + include enable-php-` + strconv.Itoa(website.PHP) + `.conf; + ` + if len(strings.TrimSpace(phpConfigOld)) != 0 { + raw = strings.Replace(raw, phpConfigOld, phpConfig, -1) + } + } + + if err = app.Orm.Save(website).Error; err != nil { + return err + } + + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + return err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil { + return err + } + + err = systemctl.Reload("openresty") + if err != nil { + _, err = shell.Execf("openresty -t") + } + + return err +} + +func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { + website := new(biz.Website) + if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil { + return err + } + + if website.Cert != nil { + return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书") + } + + if err := app.Orm.Delete(website).Error; err != nil { + return err + } + + _ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")) + + if req.Path { + _ = io.Remove(website.Path) + } + if req.DB { + rootPassword, err := r.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + if err != nil { + return err + } + mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") + if err != nil { + return err + } + _ = mysql.DatabaseDrop(website.Name) + _ = mysql.UserDrop(website.Name) + _, _ = shell.Execf(`echo "DROP DATABASE IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) + _, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) + } + + err := systemctl.Reload("openresty") + if err != nil { + _, err = shell.Execf("openresty -t") + } + + return err +} + +func (r *websiteRepo) ClearLog(id uint) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(website).Error; err != nil { + return err + } + + _, err := shell.Execf(`echo "" > %s/wwwlogs/%s.log`, app.Root, website.Name) + return err +} + +func (r *websiteRepo) UpdateRemark(id uint, remark string) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(website).Error; err != nil { + return err + } + + website.Remark = remark + return app.Orm.Save(website).Error +} + +func (r *websiteRepo) ResetConfig(id uint) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(&website).Error; err != nil { + return err + } + + website.Status = true + website.SSL = false + if err := app.Orm.Save(website).Error; err != nil { + return err + } + + raw := fmt.Sprintf(` +# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! +# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 +server +{ + # port标记位开始 + listen 80; + # port标记位结束 + # server_name标记位开始 + server_name localhost; + # server_name标记位结束 + # index标记位开始 + index index.php index.html; + # index标记位结束 + # root标记位开始 + root %s; + # root标记位结束 + + # ssl标记位开始 + # ssl标记位结束 + + # php标记位开始 + include enable-php-%d.conf; + # php标记位结束 + + # waf标记位开始 + waf off; + waf_rule_path %s/server/openresty/ngx_waf/assets/rules/; + waf_mode DYNAMIC; + waf_cc_deny rate=1000r/m duration=60m; + waf_cache capacity=50; + # waf标记位结束 + + # 错误页配置,可自行设置 + error_page 404 /404.html; + #error_page 502 /502.html; + + # acme证书签发配置,不可修改 + include %s/server/vhost/acme/%s.conf; + + # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 + include %s/server/vhost/rewrite/%s.conf; + + # 面板默认禁止访问部分敏感目录,可自行修改 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) + { + return 404; + } + # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 + location ~ .*\.(js|css)$ + { + expires 1h; + error_log /dev/null; + access_log /dev/null; + } + + access_log %s/wwwlogs/%s.log; + error_log %s/wwwlogs/%s.log; +} + +`, website.Path, website.PHP, app.Root, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name) + if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + return nil + } + if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil { + return nil + } + if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil { + return nil + } + if err := systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil +} + +func (r *websiteRepo) UpdateStatus(id uint, status bool) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(&website).Error; err != nil { + return err + } + + website.Status = status + if err := app.Orm.Save(website).Error; err != nil { + return err + } + + raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { + return err + } + + // 运行目录 + rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束") + match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig) + if len(match) == 2 { + if website.Status { + root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig) + raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1])) + } else { + raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/openresty/html;\n # root %s;\n ", app.Root, match[1])) + } + } + + // 默认文件 + indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束") + match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig) + if len(match) == 2 { + if website.Status { + index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig) + raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ") + } else { + raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ") + } + } + + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + return err + } + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil +} diff --git a/embed/embed.go b/internal/embed/embed.go similarity index 100% rename from embed/embed.go rename to internal/embed/embed.go diff --git a/storage/logs/.gitignore b/internal/embed/frontend/.gitignore similarity index 100% rename from storage/logs/.gitignore rename to internal/embed/frontend/.gitignore diff --git a/embed/website/404.html b/internal/embed/website/404.html similarity index 100% rename from embed/website/404.html rename to internal/embed/website/404.html diff --git a/embed/website/index.html b/internal/embed/website/index.html similarity index 100% rename from embed/website/index.html rename to internal/embed/website/index.html diff --git a/internal/http/middleware/middleware.go b/internal/http/middleware/middleware.go new file mode 100644 index 00000000..7c563bf5 --- /dev/null +++ b/internal/http/middleware/middleware.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "net/http" + + "github.com/go-chi/chi/v5/middleware" + sessionmiddleware "github.com/go-rat/sessions/middleware" + + "github.com/TheTNB/panel/internal/app" +) + +// GlobalMiddleware is a collection of global middleware that will be applied to every request. +func GlobalMiddleware() []func(http.Handler) http.Handler { + return []func(http.Handler) http.Handler{ + sessionmiddleware.StartSession(app.Session), + //middleware.SupressNotFound(app.Http),// bug https://github.com/go-chi/chi/pull/940 + middleware.CleanPath, + middleware.StripSlashes, + middleware.Logger, + middleware.Recoverer, + middleware.Compress(5), + } +} diff --git a/internal/http/middleware/must_login.go b/internal/http/middleware/must_login.go new file mode 100644 index 00000000..77bbd4e5 --- /dev/null +++ b/internal/http/middleware/must_login.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "context" + "net/http" + + "github.com/go-rat/chix" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" +) + +// MustLogin 确保已登录 +func MustLogin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session, err := app.Session.GetSession(r) + if err != nil { + render := chix.NewRender(w) + render.Status(http.StatusInternalServerError) + render.JSON(chix.M{ + "message": err.Error(), + }) + } + + if session.Missing("user_id") { + render := chix.NewRender(w) + render.Status(http.StatusUnauthorized) + render.JSON(chix.M{ + "message": "会话已过期,请重新登录", + }) + return + } + + userID := cast.ToUint(session.Get("user_id")) + if userID == 0 { + render := chix.NewRender(w) + render.Status(http.StatusUnauthorized) + render.JSON(chix.M{ + "message": "会话无效,请重新登录", + }) + return + } + + r = r.WithContext(context.WithValue(r.Context(), "user_id", userID)) // nolint:staticcheck + next.ServeHTTP(w, r) + }) +} diff --git a/internal/http/middleware/throttle.go b/internal/http/middleware/throttle.go new file mode 100644 index 00000000..25acfb07 --- /dev/null +++ b/internal/http/middleware/throttle.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "net/http" + "time" + + "github.com/sethvargo/go-limiter/httplimit" + "github.com/sethvargo/go-limiter/memorystore" +) + +// Throttle 限流器 +func Throttle(tokens uint64, interval time.Duration) func(next http.Handler) http.Handler { + store, err := memorystore.New(&memorystore.Config{ + Tokens: tokens, + Interval: interval, + }) + if err != nil { + panic(err) + } + + limiter, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) + if err != nil { + panic(err) + } + + return limiter.Handle +} diff --git a/internal/http/request/cert.go b/internal/http/request/cert.go new file mode 100644 index 00000000..2fe9f9e7 --- /dev/null +++ b/internal/http/request/cert.go @@ -0,0 +1,25 @@ +package request + +type CertCreate struct { + Type string `form:"type" json:"type"` + Domains []string `form:"domains" json:"domains"` + AutoRenew bool `form:"auto_renew" json:"auto_renew"` + AccountID uint `form:"account_id" json:"account_id"` + DNSID uint `form:"dns_id" json:"dns_id"` + WebsiteID uint `form:"website_id" json:"website_id"` +} + +type CertUpdate struct { + ID uint `form:"id" json:"id"` + Type string `form:"type" json:"type"` + Domains []string `form:"domains" json:"domains"` + AutoRenew bool `form:"auto_renew" json:"auto_renew"` + AccountID uint `form:"account_id" json:"account_id"` + DNSID uint `form:"dns_id" json:"dns_id"` + WebsiteID uint `form:"website_id" json:"website_id"` +} + +type CertDeploy struct { + ID uint `form:"id" json:"id"` + WebsiteID uint `form:"website_id" json:"website_id"` +} diff --git a/internal/http/request/cert_account.go b/internal/http/request/cert_account.go new file mode 100644 index 00000000..25468628 --- /dev/null +++ b/internal/http/request/cert_account.go @@ -0,0 +1,18 @@ +package request + +type CertAccountCreate struct { + CA string `form:"ca" json:"ca"` + Email string `form:"email" json:"email"` + Kid string `form:"kid" json:"kid"` + HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` + KeyType string `form:"key_type" json:"key_type"` +} + +type CertAccountUpdate struct { + ID uint `form:"id" json:"id"` + CA string `form:"ca" json:"ca"` + Email string `form:"email" json:"email"` + Kid string `form:"kid" json:"kid"` + HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` + KeyType string `form:"key_type" json:"key_type"` +} diff --git a/internal/http/request/cert_dns.go b/internal/http/request/cert_dns.go new file mode 100644 index 00000000..c01d58fe --- /dev/null +++ b/internal/http/request/cert_dns.go @@ -0,0 +1,16 @@ +package request + +import "github.com/TheTNB/panel/pkg/acme" + +type CertDNSCreate struct { + Type string `form:"type" json:"type"` + Name string `form:"name" json:"name"` + Data acme.DNSParam `form:"data" json:"data"` +} + +type CertDNSUpdate struct { + ID uint `form:"id" json:"id"` + Type string `form:"type" json:"type"` + Name string `form:"name" json:"name"` + Data acme.DNSParam `form:"data" json:"data"` +} diff --git a/internal/http/request/common.go b/internal/http/request/common.go new file mode 100644 index 00000000..b6fe9c3a --- /dev/null +++ b/internal/http/request/common.go @@ -0,0 +1,5 @@ +package request + +type ID struct { + ID uint `json:"id" form:"id" query:"id" validate:"required,number"` +} diff --git a/internal/http/request/container.go b/internal/http/request/container.go new file mode 100644 index 00000000..ba3e1c79 --- /dev/null +++ b/internal/http/request/container.go @@ -0,0 +1,33 @@ +package request + +import "github.com/TheTNB/panel/pkg/types" + +type ContainerID struct { + ID string `json:"id" form:"id"` +} + +type ContainerRename struct { + ID string `form:"id" json:"id"` + Name string `form:"name" json:"name"` +} + +type ContainerCreate struct { + Name string `form:"name" json:"name"` + Image string `form:"image" json:"image"` + Ports []types.ContainerPort `form:"ports" json:"ports"` + Network string `form:"network" json:"network"` + Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` + Labels []types.KV `form:"labels" json:"labels"` + Env []types.KV `form:"env" json:"env"` + Entrypoint []string `form:"entrypoint" json:"entrypoint"` + Command []string `form:"command" json:"command"` + RestartPolicy string `form:"restart_policy" json:"restart_policy"` + AutoRemove bool `form:"auto_remove" json:"auto_remove"` + Privileged bool `form:"privileged" json:"privileged"` + OpenStdin bool `form:"openStdin" json:"open_stdin"` + PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` + Tty bool `form:"tty" json:"tty"` + CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` + CPUs int64 `form:"cpus" json:"cpus"` + Memory int64 `form:"memory" json:"memory"` +} diff --git a/internal/http/request/container_image.go b/internal/http/request/container_image.go new file mode 100644 index 00000000..e37f9959 --- /dev/null +++ b/internal/http/request/container_image.go @@ -0,0 +1,12 @@ +package request + +type ContainerImageID struct { + ID string `json:"id" form:"id"` +} + +type ContainerImagePull struct { + Name string `form:"name" json:"name"` + Auth bool `form:"auth" json:"auth"` + Username string `form:"username" json:"username"` + Password string `form:"password" json:"password"` +} diff --git a/internal/http/request/container_network.go b/internal/http/request/container_network.go new file mode 100644 index 00000000..78ce93ce --- /dev/null +++ b/internal/http/request/container_network.go @@ -0,0 +1,21 @@ +package request + +import "github.com/TheTNB/panel/pkg/types" + +type ContainerNetworkID struct { + ID string `json:"id" form:"id"` +} + +type ContainerNetworkCreate struct { + Name string `form:"name" json:"name"` + Driver string `form:"driver" json:"driver"` + Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"` + Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"` + Labels []types.KV `form:"labels" json:"labels"` + Options []types.KV `form:"options" json:"options"` +} + +type ContainerNetworkConnect struct { + Network string `form:"network" json:"network"` + Container string `form:"container" json:"container"` +} diff --git a/internal/http/request/container_volume.go b/internal/http/request/container_volume.go new file mode 100644 index 00000000..fab0f60d --- /dev/null +++ b/internal/http/request/container_volume.go @@ -0,0 +1,14 @@ +package request + +import "github.com/TheTNB/panel/pkg/types" + +type ContainerVolumeID struct { + ID string `json:"id" form:"id"` +} + +type ContainerVolumeCreate struct { + Name string `form:"name" json:"name"` + Driver string `form:"driver" json:"driver"` + Labels []types.KV `form:"labels" json:"labels"` + Options []types.KV `form:"options" json:"options"` +} diff --git a/internal/http/request/cron.go b/internal/http/request/cron.go new file mode 100644 index 00000000..02ca39c3 --- /dev/null +++ b/internal/http/request/cron.go @@ -0,0 +1,24 @@ +package request + +type CronCreate struct { + Name string `form:"name" json:"name"` + Type string `form:"type" json:"type"` + Time string `form:"time" json:"time"` + Script string `form:"script" json:"script"` + BackupType string `form:"backup_type" json:"backup_type"` + BackupPath string `form:"backup_path" json:"backup_path"` + Target string `form:"target" json:"target"` + Save int `form:"save" json:"save"` +} + +type CronUpdate struct { + ID uint `form:"id" json:"id"` + Name string `form:"name" json:"name"` + Time string `form:"time" json:"time"` + Script string `form:"script" json:"script"` +} + +type CronStatus struct { + ID uint `form:"id" json:"id"` + Status bool `form:"status" json:"status"` +} diff --git a/internal/http/request/file.go b/internal/http/request/file.go new file mode 100644 index 00000000..141dd062 --- /dev/null +++ b/internal/http/request/file.go @@ -0,0 +1,54 @@ +package request + +type FilePath struct { + Path string `json:"path" form:"path"` +} + +type FileCreate struct { + Dir bool `json:"dir" form:"dir"` + Path string `json:"path" form:"path"` +} + +type FileSave struct { + Path string `form:"path" json:"path"` + Content string `form:"content" json:"content"` +} + +type FileUpload struct { + Path string `json:"path" form:"path"` + File []byte `json:"file" form:"file"` +} + +type FileMove struct { + Source string `form:"source" json:"source"` + Target string `form:"target" json:"target"` + Force bool `form:"force" json:"force"` +} + +type FileCopy struct { + Source string `form:"source" json:"source"` + Target string `form:"target" json:"target"` + Force bool `form:"force" json:"force"` +} + +type FilePermission struct { + Path string `form:"path" json:"path"` + Mode string `form:"mode" json:"mode"` + Owner string `form:"owner" json:"owner"` + Group string `form:"group" json:"group"` +} + +type FileCompress struct { + Paths []string `form:"paths" json:"paths"` + File string `form:"file" json:"file"` +} + +type FileUnCompress struct { + File string `form:"file" json:"file"` + Path string `form:"path" json:"path"` +} + +type FileSearch struct { + Path string `form:"path" json:"path"` + KeyWord string `form:"keyword" json:"keyword"` +} diff --git a/internal/http/request/firewall.go b/internal/http/request/firewall.go new file mode 100644 index 00000000..f20fcb31 --- /dev/null +++ b/internal/http/request/firewall.go @@ -0,0 +1,10 @@ +package request + +type FirewallStatus struct { + Status bool `json:"status" form:"status"` +} + +type FirewallCreateRule struct { + Port uint `json:"port"` + Protocol string `json:"protocol"` +} diff --git a/internal/http/request/monitor.go b/internal/http/request/monitor.go new file mode 100644 index 00000000..82a1543d --- /dev/null +++ b/internal/http/request/monitor.go @@ -0,0 +1,11 @@ +package request + +type MonitorSetting struct { + Enabled bool `json:"enabled"` + Days int `json:"days"` +} + +type MonitorList struct { + Start int64 `json:"start"` + End int64 `json:"end"` +} diff --git a/internal/http/request/paginate.go b/internal/http/request/paginate.go new file mode 100644 index 00000000..eb200cf4 --- /dev/null +++ b/internal/http/request/paginate.go @@ -0,0 +1,32 @@ +package request + +import ( + "net/http" +) + +type Paginate struct { + Page uint `json:"page" form:"page" query:"page" validate:"required,number,gte=1"` + Limit uint `json:"limit" form:"limit" query:"limit" validate:"required,number,gte=1,lte=1000"` +} + +func (r *Paginate) Messages(_ *http.Request) map[string]string { + return map[string]string{ + "Page.gte": "页码必须大于或等于1", + "Limit.gte": "每页数量必须大于或等于1", + "Limit.lte": "每页数量必须小于或等于1000", + "Page.number": "页码必须是数字", + "Limit.number": "每页数量必须是数字", + "Page.required": "页码不能为空", + "Limit.required": "每页数量不能为空", + } +} + +func (r *Paginate) Prepare(_ *http.Request) error { + if r.Page == 0 { + r.Page = 1 + } + if r.Limit == 0 { + r.Limit = 10 + } + return nil +} diff --git a/internal/http/request/plugin.go b/internal/http/request/plugin.go new file mode 100644 index 00000000..e23f9cc4 --- /dev/null +++ b/internal/http/request/plugin.go @@ -0,0 +1,10 @@ +package request + +type PluginSlug struct { + Slug string `json:"slug" form:"slug"` +} + +type PluginUpdateShow struct { + Slug string `json:"slug" form:"slug"` + Show bool `json:"show" form:"show"` +} diff --git a/internal/http/request/request.go b/internal/http/request/request.go new file mode 100644 index 00000000..ffb7fc89 --- /dev/null +++ b/internal/http/request/request.go @@ -0,0 +1,21 @@ +package request + +import ( + "net/http" +) + +type WithAuthorize interface { + Authorize(r *http.Request) error +} + +type WithPrepare interface { + Prepare(r *http.Request) error +} + +type WithRules interface { + Rules(r *http.Request) map[string]string +} + +type WithMessages interface { + Messages(r *http.Request) map[string]string +} diff --git a/internal/http/request/safe.go b/internal/http/request/safe.go new file mode 100644 index 00000000..56f65090 --- /dev/null +++ b/internal/http/request/safe.go @@ -0,0 +1,10 @@ +package request + +type SafeUpdateSSH struct { + Port uint `json:"port" form:"port"` + Status bool `json:"status" form:"status"` +} + +type SafeUpdatePingStatus struct { + Status bool `json:"status" form:"status"` +} diff --git a/internal/http/request/setting.go b/internal/http/request/setting.go new file mode 100644 index 00000000..9575e6ba --- /dev/null +++ b/internal/http/request/setting.go @@ -0,0 +1,16 @@ +package request + +type PanelSetting struct { + Name string `json:"name"` + Language string `json:"language"` + Entrance string `json:"entrance"` + WebsitePath string `json:"website_path"` + BackupPath string `json:"backup_path"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Port string `json:"port"` + HTTPS bool `json:"https"` + Cert string `json:"cert"` + Key string `json:"key"` +} diff --git a/internal/http/request/ssh.go b/internal/http/request/ssh.go new file mode 100644 index 00000000..b49c8cbe --- /dev/null +++ b/internal/http/request/ssh.go @@ -0,0 +1,8 @@ +package request + +type SSHUpdateInfo struct { + Host string `json:"host" form:"host"` + Port string `json:"port" form:"port"` + User string `json:"user" form:"user"` + Password string `json:"password" form:"password"` +} diff --git a/internal/http/request/systemctl.go b/internal/http/request/systemctl.go new file mode 100644 index 00000000..bb3c2ee2 --- /dev/null +++ b/internal/http/request/systemctl.go @@ -0,0 +1,5 @@ +package request + +type SystemctlService struct { + Service string `json:"service"` +} diff --git a/internal/http/request/user.go b/internal/http/request/user.go new file mode 100644 index 00000000..401e010d --- /dev/null +++ b/internal/http/request/user.go @@ -0,0 +1,14 @@ +package request + +type UserLogin struct { + Username string `json:"username" form:"username" validate:"required,min=3,max=255"` + Password string `json:"password" form:"password" validate:"required,min=6,max=255"` +} + +type UserID struct { + ID uint `uri:"id" validate:"required,number"` +} + +type AddUser struct { + Name string `json:"name" form:"name" validate:"required,min=3,max=255" comment:"用户名"` +} diff --git a/internal/http/request/website.go b/internal/http/request/website.go new file mode 100644 index 00000000..d38a4bf2 --- /dev/null +++ b/internal/http/request/website.go @@ -0,0 +1,76 @@ +package request + +import "net/http" + +type WebsiteDefaultConfig struct { + Index string `json:"index" form:"index"` + Stop string `json:"stop" form:"stop"` +} + +type WebsiteCreate struct { + Name string `form:"name" json:"name"` + Domains []string `form:"domains" json:"domains"` + Ports []uint `form:"ports" json:"ports"` + Path string `form:"path" json:"path"` + PHP string `form:"php" json:"php"` + DB bool `form:"db" json:"db"` + DBType string `form:"db_type" json:"db_type"` + DBName string `form:"db_name" json:"db_name"` + DBUser string `form:"db_user" json:"db_user"` + DBPassword string `form:"db_password" json:"db_password"` +} + +type WebsiteDelete struct { + ID uint `form:"id" json:"id"` + Path bool `form:"path" json:"path"` + DB bool `form:"db" json:"db"` +} + +type WebsiteUpdate struct { + ID uint `form:"id" json:"id"` + Domains []string `form:"domains" json:"domains"` + Ports []uint `form:"ports" json:"ports"` + SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"` + QUICPorts []uint `form:"quic_ports" json:"quic_ports"` + OCSP bool `form:"ocsp" json:"ocsp"` + HSTS bool `form:"hsts" json:"hsts"` + SSL bool `form:"ssl" json:"ssl"` + HTTPRedirect bool `form:"http_redirect" json:"http_redirect"` + OpenBasedir bool `form:"open_basedir" json:"open_basedir"` + Waf bool `form:"waf" json:"waf"` + WafCache string `form:"waf_cache" json:"waf_cache"` + WafMode string `form:"waf_mode" json:"waf_mode"` + WafCcDeny string `form:"waf_cc_deny" json:"waf_cc_deny"` + Index string `form:"index" json:"index"` + Path string `form:"path" json:"path"` + Root string `form:"root" json:"root"` + Raw string `form:"raw" json:"raw"` + Rewrite string `form:"rewrite" json:"rewrite"` + PHP int `form:"php" json:"php"` + SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` + SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` +} + +func (r *WebsiteUpdate) Prepare(_ *http.Request) error { + if r.WafMode == "" { + r.WafMode = "DYNAMIC" + } + if r.WafCcDeny == "" { + r.WafCcDeny = "rate=1000r/m duration=60m" + } + if r.WafCache == "" { + r.WafCache = "capacity=50" + } + + return nil +} + +type WebsiteUpdateRemark struct { + ID uint `form:"id" json:"id"` + Remark string `form:"remark" json:"remark"` +} + +type WebsiteUpdateStatus struct { + ID uint `json:"id" form:"id"` + Status bool `json:"status" form:"status"` +} diff --git a/internal/job/process_task.go b/internal/job/process_task.go new file mode 100644 index 00000000..380fe1ce --- /dev/null +++ b/internal/job/process_task.go @@ -0,0 +1,52 @@ +package job + +import ( + "errors" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/pkg/shell" +) + +// ProcessTask 处理面板任务 +type ProcessTask struct { + taskRepo biz.TaskRepo + taskID uint +} + +// NewProcessTask 实例化 ProcessTask +func NewProcessTask(taskRepo biz.TaskRepo) *ProcessTask { + return &ProcessTask{ + taskRepo: taskRepo, + } +} + +func (receiver *ProcessTask) Handle(args ...any) error { + taskID, ok := args[0].(uint) + if !ok { + return errors.New("参数错误") + } + receiver.taskID = taskID + + task, err := receiver.taskRepo.Get(taskID) + if err != nil { + return err + } + + if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusRunning); err != nil { + return err + } + + if _, err = shell.Execf(task.Shell); err != nil { // nolint: govet + return err + } + + if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusSuccess); err != nil { + return err + } + + return nil +} + +func (receiver *ProcessTask) ErrHandle(err error) { + _ = receiver.taskRepo.UpdateStatus(receiver.taskID, biz.TaskStatusFailed) +} diff --git a/internal/migration/migration.go b/internal/migration/migration.go new file mode 100644 index 00000000..466458e4 --- /dev/null +++ b/internal/migration/migration.go @@ -0,0 +1,5 @@ +package migration + +import "github.com/go-gormigrate/gormigrate/v2" + +var Migrations []*gormigrate.Migration diff --git a/internal/migration/v1.go b/internal/migration/v1.go new file mode 100644 index 00000000..8bcbdbc8 --- /dev/null +++ b/internal/migration/v1.go @@ -0,0 +1,44 @@ +package migration + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" + + "github.com/TheTNB/panel/internal/biz" +) + +func init() { + Migrations = append(Migrations, &gormigrate.Migration{ + ID: "20240812-init", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + &biz.Cert{}, + &biz.CertDNS{}, + &biz.CertAccount{}, + &biz.Cron{}, + &biz.Database{}, + &biz.Monitor{}, + &biz.Plugin{}, + &biz.Setting{}, + &biz.Task{}, + &biz.User{}, + &biz.Website{}, + ) + }, + Rollback: func(tx *gorm.DB) error { + return tx.Migrator().DropTable( + &biz.Cert{}, + &biz.CertDNS{}, + &biz.CertAccount{}, + &biz.Cron{}, + &biz.Database{}, + &biz.Monitor{}, + &biz.Plugin{}, + &biz.Setting{}, + &biz.Task{}, + &biz.User{}, + &biz.Website{}, + ) + }, + }) +} diff --git a/internal/php.go b/internal/php.go deleted file mode 100644 index 7667dea7..00000000 --- a/internal/php.go +++ /dev/null @@ -1,25 +0,0 @@ -package internal - -import ( - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PHP interface { - Status() (bool, error) - Reload() error - Start() error - Stop() error - Restart() error - GetConfig() (string, error) - SaveConfig(config string) error - GetFPMConfig() (string, error) - SaveFPMConfig(config string) error - Load() ([]types.NV, error) - GetErrorLog() (string, error) - GetSlowLog() (string, error) - ClearErrorLog() error - ClearSlowLog() error - GetExtensions() ([]types.PHPExtension, error) - InstallExtension(slug string) error - UninstallExtension(slug string) error -} diff --git a/internal/plugin.go b/internal/plugin.go deleted file mode 100644 index 9170e9f4..00000000 --- a/internal/plugin.go +++ /dev/null @@ -1,16 +0,0 @@ -package internal - -import ( - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Plugin interface { - AllInstalled() ([]models.Plugin, error) - All() []*types.Plugin - GetBySlug(slug string) *types.Plugin - GetInstalledBySlug(slug string) models.Plugin - Install(slug string) error - Uninstall(slug string) error - Update(slug string) error -} diff --git a/internal/plugin/init.go b/internal/plugin/init.go new file mode 100644 index 00000000..f334855d --- /dev/null +++ b/internal/plugin/init.go @@ -0,0 +1,12 @@ +package plugin + +import ( + "github.com/go-chi/chi/v5" + + _ "github.com/TheTNB/panel/internal/plugin/openresty" + "github.com/TheTNB/panel/pkg/pluginloader" +) + +func Boot(r chi.Router) { + pluginloader.Boot(r) +} diff --git a/internal/plugin/openresty/init.go b/internal/plugin/openresty/init.go new file mode 100644 index 00000000..2de36be7 --- /dev/null +++ b/internal/plugin/openresty/init.go @@ -0,0 +1,22 @@ +package openresty + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/pluginloader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + pluginloader.Register(&types.Plugin{ + Slug: "openresty", + Name: "OpenResty", + Route: func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("Hello, World!")) + }) + }, + }) +} diff --git a/internal/plugin/openresty/service.go b/internal/plugin/openresty/service.go new file mode 100644 index 00000000..83ccc157 --- /dev/null +++ b/internal/plugin/openresty/service.go @@ -0,0 +1 @@ +package openresty diff --git a/internal/route/http.go b/internal/route/http.go new file mode 100644 index 00000000..6a94c34f --- /dev/null +++ b/internal/route/http.go @@ -0,0 +1,300 @@ +package route + +import ( + "io/fs" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + httpSwagger "github.com/swaggo/http-swagger/v2" + + _ "github.com/TheTNB/panel/docs" + "github.com/TheTNB/panel/internal/embed" + "github.com/TheTNB/panel/internal/http/middleware" + "github.com/TheTNB/panel/internal/service" +) + +func Http(r chi.Router) { + r.Route("/api", func(r chi.Router) { + r.Route("/user", func(r chi.Router) { + user := service.NewUserService() + r.With(middleware.Throttle(5, time.Minute)).Post("/login", user.Login) + r.Post("/logout", user.Logout) + r.Get("/isLogin", user.IsLogin) + r.With(middleware.MustLogin).Get("/info", user.Info) + }) + + r.Route("/info", func(r chi.Router) { + info := service.NewInfoService() + r.Get("/panel", info.Panel) + r.With(middleware.MustLogin).Get("/homePlugins", info.HomePlugins) + r.With(middleware.MustLogin).Get("/nowMonitor", info.NowMonitor) + r.With(middleware.MustLogin).Get("/systemInfo", info.SystemInfo) + r.With(middleware.MustLogin).Get("/countInfo", info.CountInfo) + r.With(middleware.MustLogin).Get("/installedDbAndPhp", info.InstalledDbAndPhp) + r.With(middleware.MustLogin).Get("/checkUpdate", info.CheckUpdate) + r.With(middleware.MustLogin).Get("/updateInfo", info.UpdateInfo) + r.With(middleware.MustLogin).Post("/update", info.Update) + r.With(middleware.MustLogin).Post("/restart", info.Restart) + }) + + r.Route("/task", func(r chi.Router) { + // TODO 修改前端 + r.Use(middleware.MustLogin) + task := service.NewTaskService() + r.Get("/status", task.Status) + r.Get("/", task.List) + r.Get("/{id}", task.Get) // TODO 修改前端 + r.Delete("/{id}", task.Delete) // TODO 修改前端 + }) + + r.Route("/website", func(r chi.Router) { + // TODO 修改前端 + r.Use(middleware.MustLogin) + // r.Use(middleware.MustInstallWebServer) + website := service.NewWebsiteService() + r.Get("/defaultConfig", website.GetDefaultConfig) + r.Post("/defaultConfig", website.UpdateDefaultConfig) + r.Get("/", website.List) + r.Post("/", website.Create) + r.Get("/{id}", website.Get) + r.Put("/{id}", website.Update) + r.Delete("/{id}", website.Delete) + r.Delete("/{id}/log", website.ClearLog) + r.Post("/{id}/updateRemark", website.UpdateRemark) + r.Post("/{id}/resetConfig", website.ResetConfig) + r.Post("/{id}/status", website.UpdateStatus) + }) + + // TODO + r.Route("/backup", func(r chi.Router) { + r.Use(middleware.MustLogin) + backup := service.NewBackupService() + r.Get("/backup", backup.List) + r.Post("/create", backup.Create) + r.Post("/update", backup.Update) + r.Get("/{id}", backup.Get) + r.Delete("/{id}", backup.Delete) + r.Delete("/{id}/restore", backup.Restore) + }) + + r.Route("/cert", func(r chi.Router) { + r.Use(middleware.MustLogin) + cert := service.NewCertService() + r.Get("/caProviders", cert.CAProviders) + r.Get("/dnsProviders", cert.DNSProviders) + r.Get("/algorithms", cert.Algorithms) + r.Route("/cert", func(r chi.Router) { + r.Get("/", cert.List) + r.Post("/", cert.Create) + r.Put("/{id}", cert.Update) + r.Get("/{id}", cert.Get) + r.Delete("/{id}", cert.Delete) + r.Post("/{id}/obtain", cert.Obtain) + r.Post("/{id}/renew", cert.Renew) + r.Post("/{id}/manualDNS", cert.ManualDNS) + r.Post("/{id}/deploy", cert.Deploy) + }) + r.Route("/dns", func(r chi.Router) { + certDNS := service.NewCertDNSService() + r.Get("/", certDNS.List) + r.Post("/", certDNS.Create) + r.Put("/{id}", certDNS.Update) + r.Get("/{id}", certDNS.Get) + r.Delete("/{id}", certDNS.Delete) + }) + r.Route("/account", func(r chi.Router) { + certAccount := service.NewCertAccountService() + r.Get("/", certAccount.List) + r.Post("/", certAccount.Create) + r.Put("/{id}", certAccount.Update) + r.Get("/{id}", certAccount.Get) + r.Delete("/{id}", certAccount.Delete) + }) + }) + + r.Route("/plugin", func(r chi.Router) { + r.Use(middleware.MustLogin) + plugin := service.NewPluginService() + r.Get("/list", plugin.List) + r.Post("/install", plugin.Install) + r.Post("/uninstall", plugin.Uninstall) + r.Post("/update", plugin.Update) + r.Post("/updateShow", plugin.UpdateShow) + r.Get("/isInstalled", plugin.IsInstalled) + }) + + r.Route("/cron", func(r chi.Router) { + r.Use(middleware.MustLogin) + cron := service.NewCronService() + r.Get("/", cron.List) + r.Post("/", cron.Create) + r.Put("/{id}", cron.Update) + r.Get("/{id}", cron.Get) + r.Delete("/{id}", cron.Delete) + r.Post("/{id}/status", cron.Status) + r.Get("/{id}/log", cron.Log) + }) + + r.Route("/safe", func(r chi.Router) { + r.Use(middleware.MustLogin) + safe := service.NewSafeService() + r.Get("/ssh", safe.GetSSH) + r.Post("/ssh", safe.UpdateSSH) + r.Get("/ping", safe.GetPingStatus) + r.Post("/ping", safe.UpdatePingStatus) + }) + + r.Route("/firewall", func(r chi.Router) { + r.Use(middleware.MustLogin) + firewall := service.NewFirewallService() + r.Get("/status", firewall.GetStatus) + r.Post("/status", firewall.UpdateStatus) + r.Get("/rule", firewall.GetRules) + r.Post("/rule", firewall.CreateRule) + r.Delete("/rule", firewall.DeleteRule) + }) + + r.Route("/ssh", func(r chi.Router) { + r.Use(middleware.MustLogin) + ssh := service.NewSSHService() + r.Get("/info", ssh.GetInfo) + r.Post("/info", ssh.UpdateInfo) + r.Get("/session", ssh.Session) + }) + + r.Route("/container", func(r chi.Router) { + r.Use(middleware.MustLogin) + r.Route("/container", func(r chi.Router) { + container := service.NewContainerService() + r.Get("/", container.List) + r.Get("/search", container.Search) + r.Post("/create", container.Create) + r.Post("/remove", container.Remove) + r.Post("/start", container.Start) + r.Post("/stop", container.Stop) + r.Post("/restart", container.Restart) + r.Post("/pause", container.Pause) + r.Post("/unpause", container.Unpause) + r.Get("/inspect", container.Inspect) + r.Post("/kill", container.Kill) + r.Post("/rename", container.Rename) + r.Get("/stats", container.Stats) + r.Get("/exist", container.Exist) + r.Get("/logs", container.Logs) + r.Post("/prune", container.Prune) + }) + r.Route("/network", func(r chi.Router) { + containerNetwork := service.NewContainerNetworkService() + r.Get("/list", containerNetwork.List) + r.Post("/create", containerNetwork.Create) + r.Post("/remove", containerNetwork.Remove) + r.Get("/exist", containerNetwork.Exist) + r.Get("/inspect", containerNetwork.Inspect) + r.Post("/connect", containerNetwork.Connect) + r.Post("/disconnect", containerNetwork.Disconnect) + r.Post("/prune", containerNetwork.Prune) + }) + r.Route("/image", func(r chi.Router) { + containerImage := service.NewContainerImageService() + r.Get("/list", containerImage.List) + r.Get("/exist", containerImage.Exist) + r.Post("/pull", containerImage.Pull) + r.Post("/remove", containerImage.Remove) + r.Get("/inspect", containerImage.Inspect) + r.Post("/prune", containerImage.Prune) + }) + r.Route("/volume", func(r chi.Router) { + containerVolume := service.NewContainerVolumeService() + r.Get("/list", containerVolume.List) + r.Post("/create", containerVolume.Create) + r.Get("/exist", containerVolume.Exist) + r.Post("/remove", containerVolume.Remove) + r.Get("/inspect", containerVolume.Inspect) + r.Post("/prune", containerVolume.Prune) + }) + }) + + r.Route("/file", func(r chi.Router) { + r.Use(middleware.MustLogin) + file := service.NewFileService() + r.Post("/create", file.Create) + r.Get("/content", file.Content) + r.Post("/save", file.Save) + r.Post("/delete", file.Delete) + r.Post("/upload", file.Upload) + r.Post("/move", file.Move) + r.Post("/copy", file.Copy) + r.Get("/download", file.Download) + r.Post("/remoteDownload", file.RemoteDownload) + r.Get("/info", file.Info) + r.Post("/permission", file.Permission) + r.Post("/compress", file.Compress) + r.Post("/unCompress", file.UnCompress) + r.Post("/search", file.Search) + r.Get("/list", file.List) + }) + + r.Route("/monitor", func(r chi.Router) { + r.Use(middleware.MustLogin) + monitor := service.NewMonitorService() + r.Get("/setting", monitor.GetSetting) + r.Post("/setting", monitor.UpdateSetting) + r.Post("/clear", monitor.Clear) + r.Get("/list", monitor.List) + }) + + r.Route("/setting", func(r chi.Router) { + r.Use(middleware.MustLogin) + setting := service.NewSettingService() + r.Get("/", setting.Get) + r.Post("/", setting.Update) + }) + + r.Route("/systemctl", func(r chi.Router) { + r.Use(middleware.MustLogin) + systemctl := service.NewSystemctlService() + r.Get("/status", systemctl.Status) + r.Get("/isEnabled", systemctl.IsEnabled) + r.Post("/enable", systemctl.Enable) + r.Post("/disable", systemctl.Disable) + r.Post("/restart", systemctl.Restart) + r.Post("/reload", systemctl.Reload) + r.Post("/start", systemctl.Start) + r.Post("/stop", systemctl.Stop) + }) + + }) + + r.With(middleware.MustLogin).Mount("/swagger", httpSwagger.Handler()) + r.NotFound(func(writer http.ResponseWriter, request *http.Request) { + frontend, _ := fs.Sub(embed.PublicFS, "frontend") + spaHandler := func(fs http.FileSystem) http.HandlerFunc { + fileServer := http.FileServer(fs) + return func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + f, err := fs.Open(path) + if err != nil { + indexFile, err := fs.Open("index.html") + if err != nil { + http.NotFound(w, r) + return + } + defer indexFile.Close() + + fi, err := indexFile.Stat() + if err != nil { + http.NotFound(w, r) + return + } + + http.ServeContent(w, r, "index.html", fi.ModTime(), indexFile) + return + } + defer f.Close() + fileServer.ServeHTTP(w, r) + } + } + spaHandler(http.FS(frontend)).ServeHTTP(writer, request) + }) +} diff --git a/internal/service/backup.go b/internal/service/backup.go new file mode 100644 index 00000000..2f27ff27 --- /dev/null +++ b/internal/service/backup.go @@ -0,0 +1,34 @@ +package service + +import "net/http" + +type BackupService struct { +} + +func NewBackupService() *BackupService { + return &BackupService{} +} + +func (s *BackupService) List(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Update(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Get(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Delete(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Restore(w http.ResponseWriter, r *http.Request) { + +} diff --git a/internal/service/base.go b/internal/service/base.go new file mode 100644 index 00000000..0409ee03 --- /dev/null +++ b/internal/service/base.go @@ -0,0 +1,138 @@ +package service + +import ( + "errors" + "fmt" + "net/http" + "slices" + "strings" + + "github.com/go-playground/validator/v10" + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/http/request" +) + +// SuccessResponse 通用成功响应 +type SuccessResponse struct { + Message string `json:"message"` + Data any `json:"data"` +} + +// ErrorResponse 通用错误响应 +type ErrorResponse struct { + Message string `json:"message"` +} + +// Success 响应成功 +func Success(w http.ResponseWriter, data any) { + render := chix.NewRender(w) + defer render.Release() + render.JSON(&SuccessResponse{ + Message: "success", + Data: data, + }) +} + +// Error 响应错误 +func Error(w http.ResponseWriter, code int, message string) { + render := chix.NewRender(w) + defer render.Release() + render.Status(code) + render.JSON(&ErrorResponse{ + Message: message, + }) +} + +// ErrorSystem 响应系统错误 +func ErrorSystem(w http.ResponseWriter) { + render := chix.NewRender(w) + defer render.Release() + render.Status(http.StatusInternalServerError) + render.JSON(&ErrorResponse{ + Message: "系统内部错误", + }) +} + +// Bind 验证并绑定请求参数 +func Bind[T any](r *http.Request) (*T, error) { + req := new(T) + + // 绑定参数 + binder := chix.NewBind(r) + defer binder.Release() + if err := binder.URI(req); err != nil { + return nil, err + } + if err := binder.Query(req); err != nil { + return nil, err + } + if slices.Contains([]string{"POST", "PUT", "PATCH"}, strings.ToUpper(r.Method)) { + if err := binder.Body(req); err != nil { + return nil, err + } + } + + // 准备验证 + if reqWithPrepare, ok := any(req).(request.WithPrepare); ok { + if err := reqWithPrepare.Prepare(r); err != nil { + return nil, err + } + } + if reqWithAuthorize, ok := any(req).(request.WithAuthorize); ok { + if err := reqWithAuthorize.Authorize(r); err != nil { + return nil, err + } + } + if reqWithRules, ok := any(req).(request.WithRules); ok { + if rules := reqWithRules.Rules(r); rules != nil { + app.Validator.RegisterStructValidationMapRules(rules, req) + } + } + + // 验证参数 + err := app.Validator.Struct(req) + if err == nil { + return req, nil + } + + // 翻译错误信息 + var errs validator.ValidationErrors + if errors.As(err, &errs) { + for _, e := range errs { + if reqWithMessages, ok := any(req).(request.WithMessages); ok { + if msg, found := reqWithMessages.Messages(r)[fmt.Sprintf("%s.%s", e.Field(), e.Tag())]; found { + return nil, errors.New(msg) + } + } + return nil, errors.New(e.Translate(*app.Translator)) // nolint:staticcheck + } + } + + return nil, err +} + +// Paginate 取分页条目 +func Paginate[T any](r *http.Request, allItems []T) (pagedItems []T, total uint) { + req, err := Bind[request.Paginate](r) + if err != nil { + req.Page = 1 + req.Limit = 10 + } + total = uint(len(allItems)) + startIndex := (req.Page - 1) * req.Limit + endIndex := req.Page * req.Limit + + if total == 0 { + return []T{}, 0 + } + if startIndex > total { + return []T{}, total + } + if endIndex > total { + endIndex = total + } + + return allItems[startIndex:endIndex], total +} diff --git a/internal/service/cert.go b/internal/service/cert.go new file mode 100644 index 00000000..9df4c330 --- /dev/null +++ b/internal/service/cert.go @@ -0,0 +1,341 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" +) + +type CertService struct { + certRepo biz.CertRepo +} + +func NewCertService() *CertService { + return &CertService{ + certRepo: data.NewCertRepo(), + } +} + +// CAProviders +// +// @Summary 获取 CA 提供商 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/caProviders [get] +func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) { + Success(w, []map[string]string{ + { + "name": "Let's Encrypt", + "ca": "letsencrypt", + }, + { + "name": "ZeroSSL", + "ca": "zerossl", + }, + { + "name": "SSL.com", + "ca": "sslcom", + }, + { + "name": "Google", + "ca": "google", + }, + { + "name": "Buypass", + "ca": "buypass", + }, + }) + +} + +// DNSProviders +// +// @Summary 获取 DNS 提供商 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/dnsProviders [get] +func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) { + Success(w, []map[string]any{ + { + "name": "DNSPod", + "dns": acme.DnsPod, + }, + { + "name": "腾讯云", + "dns": acme.Tencent, + }, + { + "name": "阿里云", + "dns": acme.AliYun, + }, + { + "name": "CloudFlare", + "dns": acme.CloudFlare, + }, + }) + +} + +// Algorithms +// +// @Summary 获取算法列表 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/algorithms [get] +func (s *CertService) Algorithms(w http.ResponseWriter, r *http.Request) { + Success(w, []map[string]any{ + { + "name": "EC256", + "key": acme.KeyEC256, + }, + { + "name": "EC384", + "key": acme.KeyEC384, + }, + { + "name": "RSA2048", + "key": acme.KeyRSA2048, + }, + { + "name": "RSA4096", + "key": acme.KeyRSA4096, + }, + }) + +} + +// List +// +// @Summary 证书列表 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/cert [get] +func (s *CertService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certs, total, err := s.certRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": certs, + }) +} + +// Create +// +// @Summary 创建证书 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/cert [post] +func (s *CertService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cert, err := s.certRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, cert) +} + +// Update +// +// @Summary 更新证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/cert/{id} [post] +func (s *CertService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Get +// +// @Summary 获取证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/cert/{id} [get] +func (s *CertService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cert, err := s.certRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, cert) +} + +// Delete +// +// @Summary 删除证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/cert/{id} [delete] +func (s *CertService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + err = s.certRepo.Delete(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Obtain +// +// @Summary 签发证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/obtain [post] +func (s *CertService) Obtain(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cert, err := s.certRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if cert.DNS != nil || cert.Website != nil { + _, err = s.certRepo.ObtainAuto(req.ID) + } else { + _, err = s.certRepo.ObtainManual(req.ID) + } + + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Renew +// +// @Summary 续签证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/renew [post] +func (s *CertService) Renew(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + _, err = s.certRepo.Renew(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// ManualDNS +// +// @Summary 手动 DNS +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/manualDNS [post] +func (s *CertService) ManualDNS(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + dns, err := s.certRepo.ManualDNS(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, dns) +} + +// Deploy +// +// @Summary 部署证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Param websiteID query int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/deploy [post] +func (s *CertService) Deploy(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertDeploy](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + err = s.certRepo.Deploy(req.ID, req.WebsiteID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/cert_account.go b/internal/service/cert_account.go new file mode 100644 index 00000000..29419578 --- /dev/null +++ b/internal/service/cert_account.go @@ -0,0 +1,102 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type CertAccountService struct { + certAccountRepo biz.CertAccountRepo +} + +func NewCertAccountService() *CertAccountService { + return &CertAccountService{ + certAccountRepo: data.NewCertAccountRepo(), + } +} + +func (s *CertAccountService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, total, err := s.certAccountRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": certDNS, + }) +} + +func (s *CertAccountService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertAccountCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + account, err := s.certAccountRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, account) +} + +func (s *CertAccountService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertAccountUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certAccountRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CertAccountService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + account, err := s.certAccountRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, account) +} + +func (s *CertAccountService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certAccountRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/cert_dns.go b/internal/service/cert_dns.go new file mode 100644 index 00000000..e6306c68 --- /dev/null +++ b/internal/service/cert_dns.go @@ -0,0 +1,102 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type CertDNSService struct { + certDNSRepo biz.CertDNSRepo +} + +func NewCertDNSService() *CertDNSService { + return &CertDNSService{ + certDNSRepo: data.NewCertDNSRepo(), + } +} + +func (s *CertDNSService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, total, err := s.certDNSRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": certDNS, + }) +} + +func (s *CertDNSService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertDNSCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, err := s.certDNSRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, certDNS) +} + +func (s *CertDNSService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertDNSUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certDNSRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CertDNSService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, err := s.certDNSRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, certDNS) +} + +func (s *CertDNSService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certDNSRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container.go b/internal/service/container.go new file mode 100644 index 00000000..1653ba6d --- /dev/null +++ b/internal/service/container.go @@ -0,0 +1,279 @@ +package service + +import ( + "net/http" + "strings" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerService struct { + containerRepo biz.ContainerRepo +} + +func NewContainerService() *ContainerService { + return &ContainerService{ + containerRepo: data.NewContainerRepo(), + } +} + +func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) { + containers, err := s.containerRepo.ListAll() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + } + + paged, total := Paginate(r, containers) + items := make([]any, len(paged)) + for _, item := range paged { + var name string + if len(item.Names) > 0 { + name = item.Names[0] + } + items = append(items, map[string]any{ + "id": item.ID, + "name": strings.TrimLeft(name, "/"), + "image": item.Image, + "image_id": item.ImageID, + "command": item.Command, + "created": carbon.CreateFromTimestamp(item.Created).ToDateTimeString(), + "ports": item.Ports, + "labels": item.Labels, + "state": item.State, + "status": item.Status, + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerService) Search(w http.ResponseWriter, r *http.Request) { + name := strings.Fields(r.FormValue("name")) + containers, err := s.containerRepo.ListByNames(name) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": len(containers), + "items": containers, + }) +} + +func (s *ContainerService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + id, err := s.containerRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, id) +} + +func (s *ContainerService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Start(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Start(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Stop(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Stop(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Restart(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Restart(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Pause(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Pause(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Unpause(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Unpause(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + container, err := s.containerRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, container) +} + +func (s *ContainerService) Kill(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Kill(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Rename(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerRename](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Rename(req.ID, req.Name); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Stats(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + stats, err := s.containerRepo.Stats(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, stats) +} + +func (s *ContainerService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerService) Logs(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + logs, err := s.containerRepo.Logs(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, logs) +} + +func (s *ContainerService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container_image.go b/internal/service/container_image.go new file mode 100644 index 00000000..2cba1daf --- /dev/null +++ b/internal/service/container_image.go @@ -0,0 +1,122 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/str" +) + +type ContainerImageService struct { + containerImageRepo biz.ContainerImageRepo +} + +func NewContainerImageService() *ContainerImageService { + return &ContainerImageService{ + containerImageRepo: data.NewContainerImageRepo(), + } +} + +func (s *ContainerImageService) List(w http.ResponseWriter, r *http.Request) { + images, err := s.containerImageRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, images) + + items := make([]any, len(paged)) + for _, item := range paged { + items = append(items, map[string]any{ + "id": item.ID, + "created": carbon.CreateFromTimestamp(item.Created).ToDateTimeString(), + "containers": item.Containers, + "size": str.FormatBytes(float64(item.Size)), + "labels": item.Labels, + "repo_tags": item.RepoTags, + "repo_digests": item.RepoDigests, + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerImageService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImageID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerImageRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerImageService) Pull(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImagePull](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerImageRepo.Pull(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerImageService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImageID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerImageRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerImageService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImageID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + inspect, err := s.containerImageRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, inspect) +} + +func (s *ContainerImageService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerImageRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container_network.go b/internal/service/container_network.go new file mode 100644 index 00000000..c5b7d286 --- /dev/null +++ b/internal/service/container_network.go @@ -0,0 +1,170 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerNetworkService struct { + containerNetworkRepo biz.ContainerNetworkRepo +} + +func NewContainerNetworkService() *ContainerNetworkService { + return &ContainerNetworkService{ + containerNetworkRepo: data.NewContainerNetworkRepo(), + } +} + +func (s *ContainerNetworkService) List(w http.ResponseWriter, r *http.Request) { + networks, err := s.containerNetworkRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, networks) + + items := make([]any, len(paged)) + for _, item := range paged { + var ipamConfig []any + for _, v := range item.IPAM.Config { + ipamConfig = append(ipamConfig, map[string]any{ + "subnet": v.Subnet, + "gateway": v.Gateway, + "ip_range": v.IPRange, + "aux_address": v.AuxAddress, + }) + } + items = append(items, map[string]any{ + "id": item.ID, + "name": item.Name, + "driver": item.Driver, + "ipv6": item.EnableIPv6, + "scope": item.Scope, + "internal": item.Internal, + "attachable": item.Attachable, + "ingress": item.Ingress, + "labels": item.Labels, + "options": item.Options, + "ipam": map[string]any{ + "config": ipamConfig, + "driver": item.IPAM.Driver, + "options": item.IPAM.Options, + }, + "created": carbon.CreateFromStdTime(item.Created).ToDateTimeString(), + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerNetworkService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + id, err := s.containerNetworkRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, id) +} + +func (s *ContainerNetworkService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerNetworkRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerNetworkService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerNetworkRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerNetworkService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + network, err := s.containerNetworkRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, network) +} + +func (s *ContainerNetworkService) Connect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkConnect](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerNetworkRepo.Connect(req.Network, req.Container); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerNetworkService) Disconnect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkConnect](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerNetworkRepo.Disconnect(req.Network, req.Container); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerNetworkService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerNetworkRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container_volume.go b/internal/service/container_volume.go new file mode 100644 index 00000000..6bbe8704 --- /dev/null +++ b/internal/service/container_volume.go @@ -0,0 +1,133 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/str" +) + +type ContainerVolumeService struct { + containerVolumeRepo biz.ContainerVolumeRepo +} + +func NewContainerVolumeService() *ContainerVolumeService { + return &ContainerVolumeService{ + containerVolumeRepo: data.NewContainerVolumeRepo(), + } +} + +func (s *ContainerVolumeService) List(w http.ResponseWriter, r *http.Request) { + volumes, err := s.containerVolumeRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, volumes) + + items := make([]any, len(paged)) + for _, item := range paged { + var usage any + if item.UsageData != nil { + usage = map[string]any{ + "ref_count": item.UsageData.RefCount, + "size": str.FormatBytes(float64(item.UsageData.Size)), + } + } + items = append(items, map[string]any{ + "id": item.Name, + "created": carbon.Parse(item.CreatedAt).ToDateTimeString(), + "driver": item.Driver, + "mount": item.Mountpoint, + "labels": item.Labels, + "options": item.Options, + "scope": item.Scope, + "status": item.Status, + "usage": usage, + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerVolumeService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + volume, err := s.containerVolumeRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, volume.Name) + +} + +func (s *ContainerVolumeService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerVolumeRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerVolumeService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerVolumeRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerVolumeService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + volume, err := s.containerVolumeRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, volume) +} + +func (s *ContainerVolumeService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerVolumeRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/cron.go b/internal/service/cron.go new file mode 100644 index 00000000..67b6b1f7 --- /dev/null +++ b/internal/service/cron.go @@ -0,0 +1,132 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type CronService struct { + cronRepo biz.CronRepo +} + +func NewCronService() *CronService { + return &CronService{ + cronRepo: data.NewCronRepo(), + } +} + +func (s *CronService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cron, total, err := s.cronRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": cron, + }) +} + +func (s *CronService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CronCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Create(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CronUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cron, err := s.cronRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, cron) +} + +func (s *CronService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Status(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CronStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Status(req.ID, req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Log(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + log, err := s.cronRepo.Log(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, log) +} diff --git a/internal/service/file.go b/internal/service/file.go new file mode 100644 index 00000000..10fef5d0 --- /dev/null +++ b/internal/service/file.go @@ -0,0 +1,355 @@ +//go:build linux + +package service + +import ( + "fmt" + "net/http" + stdos "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/os" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" +) + +type FileService struct { +} + +func NewFileService() *FileService { + return &FileService{} +} + +func (s *FileService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCreate](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if !req.Dir { + if out, err := shell.Execf("touch %s", req.Path); err != nil { + Error(w, http.StatusInternalServerError, out) + return + } + } else { + if err = io.Mkdir(req.Path, 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Content(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if fileInfo.IsDir() { + Error(w, http.StatusInternalServerError, "目标路径不是文件") + return + } + if fileInfo.Size() > 10*1024*1024 { + Error(w, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑") + return + } + + content, err := io.Read(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, content) +} + +func (s *FileService) Save(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSave](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, req.Content, fileInfo.Mode()); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Remove(req.Path); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUpload](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, string(req.File), 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileMove](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Mv(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCopy](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Cp(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Download(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if info.IsDir() { + Error(w, http.StatusInternalServerError, "不能下载目录") + return + } + + render := chix.NewRender(w, r) + defer render.Release() + render.Download(req.Path, info.Name()) +} + +func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) { + // TODO: 未实现 +} + +func (s *FileService) Info(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "name": info.Name(), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) +} + +func (s *FileService) Permission(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePermission](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + // 解析成8进制 + mode, err := strconv.ParseUint(req.Mode, 8, 64) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Chmod(req.Path, stdos.FileMode(mode)); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if err = io.Chown(req.Path, req.Owner, req.Group); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Compress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Compress(req.Paths, req.File, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.File, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUnCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.UnCompress(req.File, req.Path, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSearch](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paths := make(map[string]stdos.FileInfo) + err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error { + if err != nil { + return err + } + if strings.Contains(info.Name(), req.KeyWord) { + paths[path] = info + } + return nil + }) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, paths) +} + +func (s *FileService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfoList, err := io.ReadDir(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + var paths []any + for _, fileInfo := range fileInfoList { + info, _ := fileInfo.Info() + stat := info.Sys().(*syscall.Stat_t) + + paths = append(paths, map[string]any{ + "name": info.Name(), + "full": filepath.Join(req.Path, info.Name()), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": os.GetUser(stat.Uid), + "group": os.GetGroup(stat.Gid), + "uid": stat.Uid, + "gid": stat.Gid, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(filepath.Join(req.Path, info.Name())), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) + } + + paged, total := Paginate(r, paths) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +// setPermission +func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) { + _ = io.Chmod(path, mode) + _ = io.Chown(path, owner, group) +} diff --git a/internal/service/file_windows.go b/internal/service/file_windows.go new file mode 100644 index 00000000..91744258 --- /dev/null +++ b/internal/service/file_windows.go @@ -0,0 +1,352 @@ +//go:build !linux + +package service + +import ( + "fmt" + "net/http" + stdos "os" + "path/filepath" + "strconv" + "strings" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" +) + +type FileService struct { +} + +func NewFileService() *FileService { + return &FileService{} +} + +func (s *FileService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCreate](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if !req.Dir { + if out, err := shell.Execf("touch %s", req.Path); err != nil { + Error(w, http.StatusInternalServerError, out) + return + } + } else { + if err = io.Mkdir(req.Path, 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Content(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if fileInfo.IsDir() { + Error(w, http.StatusInternalServerError, "目标路径不是文件") + return + } + if fileInfo.Size() > 10*1024*1024 { + Error(w, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑") + return + } + + content, err := io.Read(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, content) +} + +func (s *FileService) Save(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSave](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, req.Content, fileInfo.Mode()); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Remove(req.Path); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUpload](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, string(req.File), 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileMove](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Mv(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCopy](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Cp(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Download(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if info.IsDir() { + Error(w, http.StatusInternalServerError, "不能下载目录") + return + } + + render := chix.NewRender(w, r) + defer render.Release() + render.Download(req.Path, info.Name()) +} + +func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) { + // TODO: 未实现 +} + +func (s *FileService) Info(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "name": info.Name(), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) +} + +func (s *FileService) Permission(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePermission](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + // 解析成8进制 + mode, err := strconv.ParseUint(req.Mode, 8, 64) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Chmod(req.Path, stdos.FileMode(mode)); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if err = io.Chown(req.Path, req.Owner, req.Group); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Compress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Compress(req.Paths, req.File, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.File, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUnCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.UnCompress(req.File, req.Path, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSearch](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paths := make(map[string]stdos.FileInfo) + err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error { + if err != nil { + return err + } + if strings.Contains(info.Name(), req.KeyWord) { + paths[path] = info + } + return nil + }) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, paths) +} + +func (s *FileService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfoList, err := io.ReadDir(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + var paths []any + for _, fileInfo := range fileInfoList { + info, _ := fileInfo.Info() + + paths = append(paths, map[string]any{ + "name": info.Name(), + "full": filepath.Join(req.Path, info.Name()), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": "", + "group": "", + "uid": 0, + "gid": 0, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(filepath.Join(req.Path, info.Name())), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) + } + + paged, total := Paginate(r, paths) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +// setPermission +func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) { + _ = io.Chmod(path, mode) + _ = io.Chown(path, owner, group) +} diff --git a/internal/service/firewall.go b/internal/service/firewall.go new file mode 100644 index 00000000..b4f6a925 --- /dev/null +++ b/internal/service/firewall.go @@ -0,0 +1,104 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/firewall" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type FirewallService struct { + firewall *firewall.Firewall +} + +func NewFirewallService() *FirewallService { + + return &FirewallService{ + firewall: firewall.NewFirewall(), + } +} + +func (s *FirewallService) GetStatus(w http.ResponseWriter, r *http.Request) { + running, err := s.firewall.Status() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, running) +} + +func (s *FirewallService) UpdateStatus(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if req.Status { + err = systemctl.Start("firewalld") + if err == nil { + err = systemctl.Enable("firewalld") + } + } else { + err = systemctl.Stop("firewalld") + if err == nil { + err = systemctl.Disable("firewalld") + } + } + + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) { + rules, err := s.firewall.ListRule() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, rules) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallCreateRule](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.firewall.Port(firewall.FireInfo{Port: req.Port, Protocol: req.Protocol}, "add"); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallCreateRule](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.firewall.Port(firewall.FireInfo{Port: req.Port, Protocol: req.Protocol}, "remove"); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/info.go b/internal/service/info.go new file mode 100644 index 00000000..0f665c88 --- /dev/null +++ b/internal/service/info.go @@ -0,0 +1,387 @@ +package service + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/go-rat/chix" + "github.com/hashicorp/go-version" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/pkg/db" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/tools" + "github.com/TheTNB/panel/pkg/types" +) + +type InfoService struct { + taskRepo biz.TaskRepo + websiteRepo biz.WebsiteRepo + pluginRepo biz.PluginRepo + settingRepo biz.SettingRepo + cronRepo biz.CronRepo +} + +func NewInfoService() *InfoService { + return &InfoService{ + taskRepo: data.NewTaskRepo(), + websiteRepo: data.NewWebsiteRepo(), + pluginRepo: data.NewPluginRepo(), + settingRepo: data.NewSettingRepo(), + cronRepo: data.NewCronRepo(), + } +} + +// Panel +// +// @Summary 面板信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/panel [get] +func (s *InfoService) Panel(w http.ResponseWriter, r *http.Request) { + name, _ := s.settingRepo.Get(biz.SettingKeyName) + if name == "" { + name = "耗子面板" + } + + Success(w, chix.M{ + "name": name, + "language": app.Conf.MustString("app.locale"), + }) +} + +// HomePlugins +// +// @Summary 首页插件 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/homePlugins [get] +func (s *InfoService) HomePlugins(w http.ResponseWriter, r *http.Request) { + Success(w, nil) +} + +// NowMonitor +// +// @Summary 实时监控 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/nowMonitor [get] +func (s *InfoService) NowMonitor(w http.ResponseWriter, r *http.Request) { + Success(w, tools.GetMonitoringInfo()) +} + +// SystemInfo +// +// @Summary 系统信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/systemInfo [get] +func (s *InfoService) SystemInfo(w http.ResponseWriter, r *http.Request) { + monitorInfo := tools.GetMonitoringInfo() + + Success(w, chix.M{ + "os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion, + "uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400), + "panel_version": app.Conf.MustString("app.version"), + }) +} + +// CountInfo +// +// @Summary 统计信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/countInfo [get] +func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) { + websiteCount, err := s.websiteRepo.Count() + if err != nil { + Error(w, http.StatusInternalServerError, "获取网站数量失败") + return + } + + mysqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "mysql%") + postgresqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "postgresql%") + + type database struct { + Name string `json:"name"` + } + var databaseCount int64 + if mysqlInstalled { + rootPassword, _ := s.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock") + if err == nil { + defer mysql.Close() + if err = mysql.Ping(); err != nil { + databaseCount = -1 + } else { + rows, err := mysql.Query("SHOW DATABASES") + if err != nil { + databaseCount = -1 + } else { + defer rows.Close() + var databases []database + for rows.Next() { + var d database + if err := rows.Scan(&d.Name); err != nil { + continue + } + if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" { + continue + } + + databases = append(databases, d) + } + databaseCount = int64(len(databases)) + } + } + } + } + if postgresqlInstalled { + postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432) + if err == nil { + defer postgres.Close() + if err = postgres.Ping(); err != nil { + databaseCount = -1 + } else { + rows, err := postgres.Query("SELECT datname FROM pg_database WHERE datistemplate = false") + if err != nil { + databaseCount = -1 + } else { + defer rows.Close() + var databases []database + for rows.Next() { + var d database + if err = rows.Scan(&d.Name); err != nil { + continue + } + if d.Name == "postgres" || d.Name == "template0" || d.Name == "template1" { + continue + } + databases = append(databases, d) + } + databaseCount = int64(len(databases)) + } + } + } + } + + var ftpCount int64 + ftpInstalled, _ := s.pluginRepo.IsInstalled("slug = ?", "pureftpd") + if ftpInstalled { + listRaw, err := shell.Execf("pure-pw list") + if len(listRaw) != 0 && err == nil { + listArr := strings.Split(listRaw, "\n") + ftpCount = int64(len(listArr)) + } + } + + cronCount, err := s.cronRepo.Count() + if err != nil { + cronCount = -1 + } + + Success(w, chix.M{ + "website": websiteCount, + "database": databaseCount, + "ftp": ftpCount, + "cron": cronCount, + }) +} + +// InstalledDbAndPhp +// +// @Summary 已安装的数据库和PHP +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/installedDbAndPhp [get] +func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) { + mysqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "mysql%") + postgresqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "postgresql%") + php, _ := s.pluginRepo.GetInstalledAll("slug like ?", "php%") + + var phpData []types.LV + var dbData []types.LV + phpData = append(phpData, types.LV{Value: "0", Label: "不使用"}) + dbData = append(dbData, types.LV{Value: "0", Label: "不使用"}) + for _, p := range php { + // 过滤 phpmyadmin + match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug) + if len(match) == 0 { + continue + } + + plugin, _ := s.pluginRepo.Get(p.Slug) + phpData = append(phpData, types.LV{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: plugin.Name}) + } + + if mysqlInstalled { + dbData = append(dbData, types.LV{Value: "mysql", Label: "MySQL"}) + } + if postgresqlInstalled { + dbData = append(dbData, types.LV{Value: "postgresql", Label: "PostgreSQL"}) + } + + Success(w, chix.M{ + "php": phpData, + "db": dbData, + }) +} + +// CheckUpdate +// +// @Summary 检查更新 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/checkUpdate [get] +func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) { + current := app.Conf.MustString("app.version") + latest, err := tools.GetLatestPanelVersion() + if err != nil { + Error(w, http.StatusInternalServerError, "获取最新版本失败") + return + } + + v1, err := version.NewVersion(current) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + v2, err := version.NewVersion(latest.Version) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + if v1.GreaterThanOrEqual(v2) { + Success(w, chix.M{ + "update": false, + }) + return + } + + Success(w, chix.M{ + "update": true, + }) +} + +// UpdateInfo +// +// @Summary 版本更新信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/updateInfo [get] +func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) { + current := app.Conf.MustString("app.version") + latest, err := tools.GetLatestPanelVersion() + if err != nil { + Error(w, http.StatusInternalServerError, "获取最新版本失败") + return + } + + v1, err := version.NewVersion(current) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + v2, err := version.NewVersion(latest.Version) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + if v1.GreaterThanOrEqual(v2) { + Error(w, http.StatusInternalServerError, "当前版本已是最新版本") + return + } + + versions, err := tools.GenerateVersions(current, latest.Version) + if err != nil { + Error(w, http.StatusInternalServerError, "获取更新信息失败") + return + } + + var versionInfo []tools.PanelInfo + for _, v := range versions { + info, err := tools.GetPanelVersion(v) + if err != nil { + continue + } + + versionInfo = append(versionInfo, info) + } + + Success(w, versionInfo) +} + +// Update +// +// @Summary 更新面板 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/update [post] +func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) { + if s.taskRepo.HasRunningTask() { + Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止更新") + return + } + if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil { + types.Status = types.StatusFailed + Error(w, http.StatusInternalServerError, fmt.Sprintf("面板数据库异常,已终止操作:%s", err.Error())) + return + } + + panel, err := tools.GetLatestPanelVersion() + if err != nil { + Error(w, http.StatusInternalServerError, "获取最新版本失败") + return + } + + types.Status = types.StatusUpgrade + if err = tools.UpdatePanel(panel); err != nil { + types.Status = types.StatusFailed + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + types.Status = types.StatusNormal + tools.RestartPanel() + Success(w, nil) +} + +// Restart +// +// @Summary 重启面板 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/restart [post] +func (s *InfoService) Restart(w http.ResponseWriter, r *http.Request) { + if s.taskRepo.HasRunningTask() { + Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止重启") + return + } + + tools.RestartPanel() + Success(w, nil) +} diff --git a/app/http/controllers/monitor_controller.go b/internal/service/monitor.go similarity index 50% rename from app/http/controllers/monitor_controller.go rename to internal/service/monitor.go index 4eb4140b..3030e5b4 100644 --- a/app/http/controllers/monitor_controller.go +++ b/internal/service/monitor.go @@ -1,103 +1,73 @@ -package controllers +package service import ( "fmt" + "net/http" - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/spf13/cast" + "github.com/golang-module/carbon/v2" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" ) -type MonitorController struct { - setting internal.Setting +type MonitorService struct { + settingRepo biz.SettingRepo + monitorRepo biz.MonitorRepo } -func NewMonitorController() *MonitorController { - return &MonitorController{ - setting: services.NewSettingImpl(), +func NewMonitorService() *MonitorService { + return &MonitorService{ + settingRepo: data.NewSettingRepo(), + monitorRepo: data.NewMonitorRepo(), } } -// Switch 监控开关 -func (r *MonitorController) Switch(ctx http.Context) http.Response { - value := ctx.Request().InputBool("monitor") - err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value)) +func (s *MonitorService) GetSetting(w http.ResponseWriter, r *http.Request) { + setting, err := s.monitorRepo.GetSetting() if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "monitor": value, - "error": err.Error(), - }).Info("更新监控开关失败") - return h.ErrorSystem(ctx) + Error(w, http.StatusInternalServerError, err.Error()) + return } - return h.Success(ctx, nil) + Success(w, setting) } -// SaveDays 保存监控天数 -func (r *MonitorController) SaveDays(ctx http.Context) http.Response { - days := ctx.Request().Input("days") - err := r.setting.Set(models.SettingKeyMonitorDays, days) +func (s *MonitorService) UpdateSetting(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.MonitorSetting](r) if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "days": days, - "error": err.Error(), - }).Info("更新监控开关失败") - return h.ErrorSystem(ctx) + Error(w, http.StatusUnprocessableEntity, err.Error()) + return } - return h.Success(ctx, nil) + if err = s.monitorRepo.UpdateSetting(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) } -// SwitchAndDays 监控开关和监控天数 -func (r *MonitorController) SwitchAndDays(ctx http.Context) http.Response { - monitor := r.setting.Get(models.SettingKeyMonitor) - monitorDays := r.setting.Get(models.SettingKeyMonitorDays) +func (s *MonitorService) Clear(w http.ResponseWriter, r *http.Request) { + if err := s.monitorRepo.Clear(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } - return h.Success(ctx, http.Json{ - "switch": cast.ToBool(monitor), - "days": cast.ToInt(monitorDays), - }) + Success(w, nil) } -// Clear 清空监控数据 -func (r *MonitorController) Clear(ctx http.Context) http.Response { - _, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{}) +func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.MonitorList](r) if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "error": err.Error(), - }).Info("清空监控数据失败") - return h.ErrorSystem(ctx) + Error(w, http.StatusUnprocessableEntity, err.Error()) + return } - return h.Success(ctx, nil) -} - -// List 监控数据列表 -func (r *MonitorController) List(ctx http.Context) http.Response { - start := ctx.Request().InputInt64("start") - end := ctx.Request().InputInt64("end") - startTime := carbon.FromTimestampMilli(start) - endTime := carbon.FromTimestampMilli(end) - - var monitors []models.Monitor - err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors) + monitors, err := s.monitorRepo.List(carbon.CreateFromTimestampMilli(req.Start), carbon.CreateFromTimestampMilli(req.End)) if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "start": startTime.ToDateTimeString(), - "end": endTime.ToDateTimeString(), - "error": err.Error(), - }).Info("获取监控数据失败") - return h.ErrorSystem(ctx) - } - - if len(monitors) == 0 { - return h.Error(ctx, http.StatusNotFound, "监控数据为空") + Error(w, http.StatusInternalServerError, err.Error()) + return } type load struct { @@ -182,5 +152,5 @@ func (r *MonitorController) List(ctx http.Context) http.Response { bytesRecv2 = 0 } - return h.Success(ctx, data) + Success(w, data) } diff --git a/internal/service/plugin.go b/internal/service/plugin.go new file mode 100644 index 00000000..1309283c --- /dev/null +++ b/internal/service/plugin.go @@ -0,0 +1,160 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type PluginService struct { + pluginRepo biz.PluginRepo +} + +func NewPluginService() *PluginService { + return &PluginService{ + pluginRepo: data.NewPluginRepo(), + } +} + +func (s *PluginService) List(w http.ResponseWriter, r *http.Request) { + plugins := s.pluginRepo.All() + installedPlugins, err := s.pluginRepo.Installed() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + installedPluginsMap := make(map[string]*biz.Plugin) + + for _, p := range installedPlugins { + installedPluginsMap[p.Slug] = p + } + + type plugin struct { + Name string `json:"name"` + Description string `json:"description"` + Slug string `json:"slug"` + Version string `json:"version"` + Requires []string `json:"requires"` + Excludes []string `json:"excludes"` + Installed bool `json:"installed"` + InstalledVersion string `json:"installed_version"` + Show bool `json:"show"` + } + + var pluginArr []plugin + for _, item := range plugins { + installed, installedVersion, show := false, "", false + if _, ok := installedPluginsMap[item.Slug]; ok { + installed = true + installedVersion = installedPluginsMap[item.Slug].Version + show = installedPluginsMap[item.Slug].Show + } + pluginArr = append(pluginArr, plugin{ + Name: item.Name, + Description: item.Description, + Slug: item.Slug, + Version: item.Version, + Requires: item.Requires, + Excludes: item.Excludes, + Installed: installed, + InstalledVersion: installedVersion, + Show: show, + }) + } + + paged, total := Paginate(r, pluginArr) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *PluginService) Install(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.Install(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) Uninstall(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.Uninstall(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.Update(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) UpdateShow(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginUpdateShow](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.UpdateShow(req.Slug, req.Show); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) IsInstalled(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + plugin, err := s.pluginRepo.Get(req.Slug) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + installed, err := s.pluginRepo.IsInstalled(req.Slug) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "name": plugin.Name, + "installed": installed, + }) +} diff --git a/internal/service/safe.go b/internal/service/safe.go new file mode 100644 index 00000000..8e3f5312 --- /dev/null +++ b/internal/service/safe.go @@ -0,0 +1,73 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type SafeService struct { + safeRepo biz.SafeRepo +} + +func NewSafeService() *SafeService { + return &SafeService{ + safeRepo: data.NewSafeRepo(), + } +} + +func (s *SafeService) GetSSH(w http.ResponseWriter, r *http.Request) { + port, status, err := s.safeRepo.GetSSH() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + Success(w, chix.M{ + "port": port, + "status": status, + }) +} + +func (s *SafeService) UpdateSSH(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SafeUpdateSSH](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.safeRepo.UpdateSSH(req.Port, req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *SafeService) GetPingStatus(w http.ResponseWriter, r *http.Request) { + status, err := s.safeRepo.GetPingStatus() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, status) +} + +func (s *SafeService) UpdatePingStatus(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SafeUpdatePingStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.safeRepo.UpdatePingStatus(req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/setting.go b/internal/service/setting.go new file mode 100644 index 00000000..f30d2ac8 --- /dev/null +++ b/internal/service/setting.go @@ -0,0 +1,44 @@ +package service + +import ( + "net/http" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type SettingService struct { + settingRepo biz.SettingRepo +} + +func NewSettingService() *SettingService { + return &SettingService{ + settingRepo: data.NewSettingRepo(), + } +} + +func (s *SettingService) Get(w http.ResponseWriter, r *http.Request) { + setting, err := s.settingRepo.GetPanelSetting() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, setting) +} + +func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PanelSetting](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.settingRepo.UpdatePanelSetting(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/ssh.go b/internal/service/ssh.go new file mode 100644 index 00000000..b3b700a9 --- /dev/null +++ b/internal/service/ssh.go @@ -0,0 +1,127 @@ +package service + +import ( + "bytes" + "context" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/ssh" +) + +type SSHService struct { + sshRepo biz.SSHRepo +} + +func NewSSHService() *SSHService { + return &SSHService{ + sshRepo: data.NewSSHRepo(), + } +} + +func (s *SSHService) GetInfo(w http.ResponseWriter, r *http.Request) { + info, err := s.sshRepo.GetInfo() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, info) +} + +func (s *SSHService) UpdateInfo(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SSHUpdateInfo](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.sshRepo.UpdateInfo(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } +} + +func (s *SSHService) Session(w http.ResponseWriter, r *http.Request) { + info, err := s.sshRepo.GetInfo() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + upGrader := websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + ws, err := upGrader.Upgrade(w, r, nil) + if err != nil { + ErrorSystem(w) + return + } + defer ws.Close() + + config := ssh.ClientConfigPassword( + cast.ToString(info["host"])+":"+cast.ToString(info["port"]), + cast.ToString(info["user"]), + cast.ToString(info["password"]), + ) + client, err := ssh.NewSSHClient(config) + + if err != nil { + _ = ws.WriteControl(websocket.CloseMessage, + []byte(err.Error()), time.Now().Add(time.Second)) + ErrorSystem(w) + return + } + defer client.Close() + + turn, err := ssh.NewTurn(ws, client) + if err != nil { + _ = ws.WriteControl(websocket.CloseMessage, + []byte(err.Error()), time.Now().Add(time.Second)) + ErrorSystem(w) + return + } + defer turn.Close() + + var bufPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, + } + var logBuff = bufPool.Get().(*bytes.Buffer) + logBuff.Reset() + defer bufPool.Put(logBuff) + + sshCtx, cancel := context.WithCancel(context.Background()) + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + defer wg.Done() + if err = turn.LoopRead(logBuff, sshCtx); err != nil { + ErrorSystem(w) + return + } + }() + go func() { + defer wg.Done() + if err = turn.SessionWait(); err != nil { + ErrorSystem(w) + return + } + cancel() + }() + wg.Wait() + +} diff --git a/internal/service/systemctl.go b/internal/service/systemctl.go new file mode 100644 index 00000000..0b299efc --- /dev/null +++ b/internal/service/systemctl.go @@ -0,0 +1,138 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type SystemctlService struct { +} + +func NewSystemctlService() *SystemctlService { + return &SystemctlService{} +} + +func (s *SystemctlService) Status(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + status, err := systemctl.Status(req.Service) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", req.Service)) + return + } + + Success(w, status) +} + +func (s *SystemctlService) IsEnabled(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + enabled, err := systemctl.IsEnabled(req.Service) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", req.Service)) + return + } + + Success(w, enabled) +} + +func (s *SystemctlService) Enable(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Enable(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Disable(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Disable(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Restart(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Restart(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Reload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Reload(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Start(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Start(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Stop(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Stop(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} diff --git a/internal/service/task.go b/internal/service/task.go new file mode 100644 index 00000000..49fc1269 --- /dev/null +++ b/internal/service/task.go @@ -0,0 +1,116 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/shell" +) + +type TaskService struct { + taskRepo biz.TaskRepo +} + +func NewTaskService() *TaskService { + return &TaskService{ + taskRepo: data.NewTaskRepo(), + } +} + +// Status +// +// @Summary 是否有任务正在运行 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /tasks/status [get] +func (s *TaskService) Status(w http.ResponseWriter, r *http.Request) { + Success(w, chix.M{ + "task": s.taskRepo.HasRunningTask(), + }) +} + +// List +// +// @Summary 任务列表 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /tasks [get] +func (s *TaskService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + tasks, total, err := s.taskRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": tasks, + }) +} + +// Get +// +// @Summary 任务详情 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /task/log [get] +func (s *TaskService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + task, err := s.taskRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + log, err := shell.Execf(`tail -n 500 '%s'`, task.Log) + if err == nil { + task.Log = log + } + + Success(w, task) +} + +// Delete +// +// @Summary 删除任务 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /task/delete [post] +func (s *TaskService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + err = s.taskRepo.Delete(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/user.go b/internal/service/user.go new file mode 100644 index 00000000..ab093318 --- /dev/null +++ b/internal/service/user.go @@ -0,0 +1,117 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type UserService struct { + repo biz.UserRepo +} + +func NewUserService() *UserService { + return &UserService{ + repo: data.NewUserRepo(), + } +} + +// Login +// +// @Summary 登录 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Param data body request.UserLogin true "request" +// @Success 200 {object} SuccessResponse +// @Router /user/login [post] +func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { + sess, err := app.Session.GetSession(r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + req, err := Bind[request.UserLogin](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + user, err := s.repo.CheckPassword(req.Username, req.Password) + if err != nil { + Error(w, http.StatusForbidden, err.Error()) + return + } + + sess.Put("user_id", user.ID) + Success(w, nil) +} + +// Logout +// +// @Summary 登出 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /user/logout [post] +func (s *UserService) Logout(w http.ResponseWriter, r *http.Request) { + sess, err := app.Session.GetSession(r) + if err == nil { + sess.Forget("user_id") + } + Success(w, nil) +} + +// IsLogin +// +// @Summary 是否登录 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /user/isLogin [get] +func (s *UserService) IsLogin(w http.ResponseWriter, r *http.Request) { + sess, err := app.Session.GetSession(r) + if err != nil { + Success(w, false) + return + } + Success(w, sess.Has("user_id")) +} + +// Info +// +// @Summary 用户信息 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /user/info/{id} [get] +func (s *UserService) Info(w http.ResponseWriter, r *http.Request) { + userID := cast.ToUint(r.Context().Value("user_id")) + if userID == 0 { + ErrorSystem(w) + return + } + + user, err := s.repo.Get(userID) + if err != nil { + ErrorSystem(w) + return + } + + Success(w, chix.M{ + "id": user.ID, + "role": []string{"admin"}, + "username": user.Username, + "email": user.Email, + }) +} diff --git a/internal/service/website.go b/internal/service/website.go new file mode 100644 index 00000000..1b1e2aed --- /dev/null +++ b/internal/service/website.go @@ -0,0 +1,301 @@ +package service + +import ( + "net/http" + "path/filepath" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" +) + +type WebsiteService struct { + websiteRepo biz.WebsiteRepo + settingRepo biz.SettingRepo +} + +func NewWebsiteService() *WebsiteService { + return &WebsiteService{ + websiteRepo: data.NewWebsiteRepo(), + settingRepo: data.NewSettingRepo(), + } +} + +// GetDefaultConfig +// +// @Summary 获取默认配置 +// @Tags 网站服务 +// @Produce json +// @Success 200 {object} SuccessResponse{data=map[string]string} +// @Router /panel/website/defaultConfig [get] +func (s *WebsiteService) GetDefaultConfig(w http.ResponseWriter, r *http.Request) { + index, err := io.Read(filepath.Join(app.Root, "server/openresty/html/index.html")) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + stop, err := io.Read(filepath.Join(app.Root, "server/openresty/html/stop.html")) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "index": index, + "stop": stop, + }) +} + +// UpdateDefaultConfig +// +// @Summary 更新默认配置 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param data body map[string]string true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/website/defaultConfig [post] +func (s *WebsiteService) UpdateDefaultConfig(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteDefaultConfig](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.UpdateDefaultConfig(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// List +// +// @Summary 网站列表 +// @Tags 网站服务 +// @Produce json +// @Param data query commonrequests.Paginate true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites [get] +func (s *WebsiteService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + websites, total, err := s.websiteRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": websites, + }) +} + +// Create +// +// @Summary 创建网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param data body requests.Add true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites [post] +func (s *WebsiteService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if len(req.Path) == 0 { + req.Path, _ = s.settingRepo.Get(biz.SettingKeyWebsitePath) + req.Path = filepath.Join(req.Path, req.Name) + } + + if _, err = s.websiteRepo.Create(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Get +// +// @Summary 获取网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd} +// @Router /panel/websites/{id}/config [get] +func (s *WebsiteService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + config, err := s.websiteRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, config) +} + +// Update +// +// @Summary 更新网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Param data body requests.SaveConfig true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/config [post] +func (s *WebsiteService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Delete +// +// @Summary 删除网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param data body requests.Delete true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/delete [post] +func (s *WebsiteService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteDelete](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.Delete(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// ClearLog +// +// @Summary 清空网站日志 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/log [delete] +func (s *WebsiteService) ClearLog(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.ClearLog(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// UpdateRemark +// +// @Summary 更新网站备注 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/updateRemark [post] +func (s *WebsiteService) UpdateRemark(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteUpdateRemark](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.UpdateRemark(req.ID, req.Remark); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// ResetConfig +// +// @Summary 重置网站配置 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/resetConfig [post] +func (s *WebsiteService) ResetConfig(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.ResetConfig(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// UpdateStatus +// +// @Summary 更新网站状态 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/status [post] +func (s *WebsiteService) UpdateStatus(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteUpdateStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.UpdateStatus(req.ID, req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/services/backup.go b/internal/services/backup.go deleted file mode 100644 index 6144b8e2..00000000 --- a/internal/services/backup.go +++ /dev/null @@ -1,331 +0,0 @@ -// Package services 备份服务 -package services - -import ( - "errors" - "os" - "path/filepath" - "strings" - - "github.com/goravel/framework/support/carbon" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type BackupImpl struct { - setting internal.Setting -} - -func NewBackupImpl() *BackupImpl { - return &BackupImpl{ - setting: NewSettingImpl(), - } -} - -// WebsiteList 网站备份列表 -func (s *BackupImpl) WebsiteList() ([]types.BackupFile, error) { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return []types.BackupFile{}, nil - } - - backupPath += "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return []types.BackupFile{}, err - } - } - - files, err := os.ReadDir(backupPath) - if err != nil { - return []types.BackupFile{}, err - } - var backupList []types.BackupFile - for _, file := range files { - info, err := file.Info() - if err != nil { - continue - } - backupList = append(backupList, types.BackupFile{ - Name: file.Name(), - Size: str.FormatBytes(float64(info.Size())), - }) - } - - return backupList, nil -} - -// WebSiteBackup 网站备份 -func (s *BackupImpl) WebSiteBackup(website models.Website) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return errors.New("未正确配置备份路径") - } - - backupPath += "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - - backupFile := backupPath + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip" - if _, err := shell.Execf(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil { - return err - } - - return nil -} - -// WebsiteRestore 网站恢复 -func (s *BackupImpl) WebsiteRestore(website models.Website, backupFile string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return errors.New("未正确配置备份路径") - } - - backupPath += "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - - backupFile = backupPath + "/" + backupFile - if !io.Exists(backupFile) { - return errors.New("备份文件不存在") - } - - if err := io.Remove(website.Path); err != nil { - return err - } - if err := io.UnArchive(backupFile, website.Path); err != nil { - return err - } - if err := io.Chmod(website.Path, 0755); err != nil { - return err - } - if err := io.Chown(website.Path, "www", "www"); err != nil { - return err - } - - return nil -} - -// MysqlList MySQL备份列表 -func (s *BackupImpl) MysqlList() ([]types.BackupFile, error) { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return []types.BackupFile{}, nil - } - - backupPath += "/mysql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return []types.BackupFile{}, err - } - } - - files, err := os.ReadDir(backupPath) - if err != nil { - return []types.BackupFile{}, err - } - var backupList []types.BackupFile - for _, file := range files { - info, err := file.Info() - if err != nil { - continue - } - backupList = append(backupList, types.BackupFile{ - Name: file.Name(), - Size: str.FormatBytes(float64(info.Size())), - }) - } - - return backupList, nil -} - -// MysqlBackup MySQL备份 -func (s *BackupImpl) MysqlBackup(database string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/mysql" - rootPassword := s.setting.Get(models.SettingKeyMysqlRootPassword) - backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - err := os.Setenv("MYSQL_PWD", rootPassword) - if err != nil { - return err - } - - if _, err := shell.Execf("mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile); err != nil { - return err - } - if _, err := shell.Execf("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile); err != nil { - return err - } - if err := io.Remove(backupPath + "/" + backupFile); err != nil { - return err - } - - return os.Unsetenv("MYSQL_PWD") -} - -// MysqlRestore MySQL恢复 -func (s *BackupImpl) MysqlRestore(database string, backupFile string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/mysql" - rootPassword := s.setting.Get(models.SettingKeyMysqlRootPassword) - backupFullPath := filepath.Join(backupPath, backupFile) - if !io.Exists(backupFullPath) { - return errors.New("备份文件不存在") - } - - if err := os.Setenv("MYSQL_PWD", rootPassword); err != nil { - return err - } - - tempDir, err := io.TempDir(backupFile) - if err != nil { - return err - } - - if !strings.HasSuffix(backupFile, ".sql") { - backupFile = "" // 置空,防止干扰后续判断 - if err = io.UnArchive(backupFullPath, tempDir); err != nil { - return err - } - if files, err := os.ReadDir(tempDir); err == nil { - for _, file := range files { - if strings.HasSuffix(file.Name(), ".sql") { - backupFile = filepath.Base(file.Name()) - break - } - } - } - } else { - if err = io.Cp(backupFullPath, filepath.Join(tempDir, backupFile)); err != nil { - return err - } - } - - if len(backupFile) == 0 { - return errors.New("无法找到备份文件") - } - - if _, err = shell.Execf("mysql -uroot " + database + " < " + filepath.Join(tempDir, backupFile)); err != nil { - return err - } - - if err = io.Remove(tempDir); err != nil { - return err - } - - return os.Unsetenv("MYSQL_PWD") -} - -// PostgresqlList PostgreSQL备份列表 -func (s *BackupImpl) PostgresqlList() ([]types.BackupFile, error) { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return []types.BackupFile{}, nil - } - - backupPath += "/postgresql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return []types.BackupFile{}, err - } - } - - files, err := os.ReadDir(backupPath) - if err != nil { - return []types.BackupFile{}, err - } - var backupList []types.BackupFile - for _, file := range files { - info, err := file.Info() - if err != nil { - continue - } - backupList = append(backupList, types.BackupFile{ - Name: file.Name(), - Size: str.FormatBytes(float64(info.Size())), - }) - } - - return backupList, nil -} - -// PostgresqlBackup PostgreSQL备份 -func (s *BackupImpl) PostgresqlBackup(database string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - - if _, err := shell.Execf(`su - postgres -c "pg_dump ` + database + `" > ` + backupPath + "/" + backupFile); err != nil { - return err - } - if _, err := shell.Execf("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile); err != nil { - return err - } - - return io.Remove(backupPath + "/" + backupFile) -} - -// PostgresqlRestore PostgreSQL恢复 -func (s *BackupImpl) PostgresqlRestore(database string, backupFile string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - backupFullPath := filepath.Join(backupPath, backupFile) - if !io.Exists(backupFullPath) { - return errors.New("备份文件不存在") - } - - tempDir, err := io.TempDir(backupFile) - if err != nil { - return err - } - - if !strings.HasSuffix(backupFile, ".sql") { - backupFile = "" // 置空,防止干扰后续判断 - if err = io.UnArchive(backupFullPath, tempDir); err != nil { - return err - } - if files, err := os.ReadDir(tempDir); err == nil { - for _, file := range files { - if strings.HasSuffix(file.Name(), ".sql") { - backupFile = filepath.Base(file.Name()) - break - } - } - } - } else { - if err = io.Cp(backupFullPath, filepath.Join(tempDir, backupFile)); err != nil { - return err - } - } - - if len(backupFile) == 0 { - return errors.New("无法找到备份文件") - } - - if _, err = shell.Execf(`su - postgres -c "psql ` + database + `" < ` + filepath.Join(tempDir, backupFile)); err != nil { - return err - } - - if err = io.Remove(tempDir); err != nil { - return err - } - - return nil -} diff --git a/internal/services/cert.go b/internal/services/cert.go deleted file mode 100644 index 927bb123..00000000 --- a/internal/services/cert.go +++ /dev/null @@ -1,508 +0,0 @@ -// Package services 证书服务 -package services - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "github.com/go-resty/resty/v2" - "github.com/goravel/framework/facades" - - requests "github.com/TheTNB/panel/v2/app/http/requests/cert" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/acme" - "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type CertImpl struct { - client *acme.Client -} - -func NewCertImpl() *CertImpl { - return &CertImpl{} -} - -// UserStore 添加用户 -func (s *CertImpl) UserStore(request requests.UserStore) error { - var user models.CertUser - user.CA = request.CA - user.Email = request.Email - user.Kid = request.Kid - user.HmacEncoded = request.HmacEncoded - user.KeyType = request.KeyType - - var err error - var client *acme.Client - switch user.CA { - case "letsencrypt": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CALetsEncrypt, nil, acme.KeyType(user.KeyType)) - case "buypass": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CABuypass, nil, acme.KeyType(user.KeyType)) - case "zerossl": - eab, eabErr := s.getZeroSSLEAB(user.Email) - if eabErr != nil { - return eabErr - } - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, eab, acme.KeyType(user.KeyType)) - case "sslcom": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CASSLcom, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - case "google": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAGoogle, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - default: - return errors.New("CA 提供商不支持") - } - - if err != nil { - return errors.New("向 CA 注册账号失败,请检查参数是否正确") - } - - privateKey, err := cert.EncodeKey(client.Account.PrivateKey) - if err != nil { - return errors.New("获取私钥失败") - } - user.PrivateKey = string(privateKey) - - return facades.Orm().Query().Create(&user) -} - -// UserUpdate 更新用户 -func (s *CertImpl) UserUpdate(request requests.UserUpdate) error { - var user models.CertUser - err := facades.Orm().Query().Where("id = ?", request.ID).First(&user) - if err != nil { - return err - } - - user.CA = request.CA - user.Email = request.Email - user.Kid = request.Kid - user.HmacEncoded = request.HmacEncoded - user.KeyType = request.KeyType - - var client *acme.Client - switch user.CA { - case "letsencrypt": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CALetsEncrypt, nil, acme.KeyType(user.KeyType)) - case "buypass": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CABuypass, nil, acme.KeyType(user.KeyType)) - case "zerossl": - eab, eabErr := s.getZeroSSLEAB(user.Email) - if eabErr != nil { - return eabErr - } - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, eab, acme.KeyType(user.KeyType)) - case "sslcom": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CASSLcom, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - case "google": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAGoogle, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - default: - return errors.New("CA 提供商不支持") - } - - if err != nil { - return errors.New("向 CA 注册账号失败,请检查参数是否正确") - } - - privateKey, err := cert.EncodeKey(client.Account.PrivateKey) - if err != nil { - return errors.New("获取私钥失败") - } - user.PrivateKey = string(privateKey) - - return facades.Orm().Query().Save(&user) -} - -// getZeroSSLEAB 获取 ZeroSSL EAB -func (s *CertImpl) getZeroSSLEAB(email string) (*acme.EAB, error) { - type data struct { - Success bool `json:"success"` - EabKid string `json:"eab_kid"` - EabHmacKey string `json:"eab_hmac_key"` - } - client := resty.New() - client.SetTimeout(5 * time.Second) - client.SetRetryCount(2) - - resp, err := client.R().SetFormData(map[string]string{ - "email": email, - }).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email") - if err != nil || !resp.IsSuccess() { - return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") - } - eab := resp.Result().(*data) - if !eab.Success { - return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") - } - - return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil -} - -// UserShow 根据 ID 获取用户 -func (s *CertImpl) UserShow(ID uint) (models.CertUser, error) { - var user models.CertUser - err := facades.Orm().Query().With("Certs").Where("id = ?", ID).First(&user) - - return user, err -} - -// UserDestroy 删除用户 -func (s *CertImpl) UserDestroy(ID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("user_id = ?", ID).First(&cert) - if err != nil { - return err - } - - if cert.ID != 0 { - return errors.New("该用户下存在证书,无法删除") - } - - _, err = facades.Orm().Query().Delete(&models.CertUser{}, ID) - return err -} - -// DNSStore 添加 DNS -func (s *CertImpl) DNSStore(request requests.DNSStore) error { - var dns models.CertDNS - dns.Type = request.Type - dns.Name = request.Name - dns.Data = request.Data - - return facades.Orm().Query().Create(&dns) -} - -// DNSUpdate 更新 DNS -func (s *CertImpl) DNSUpdate(request requests.DNSUpdate) error { - var dns models.CertDNS - err := facades.Orm().Query().Where("id = ?", request.ID).First(&dns) - if err != nil { - return err - } - - dns.Type = request.Type - dns.Name = request.Name - dns.Data = request.Data - - return facades.Orm().Query().Save(&dns) -} - -// DNSShow 根据 ID 获取 DNS -func (s *CertImpl) DNSShow(ID uint) (models.CertDNS, error) { - var dns models.CertDNS - err := facades.Orm().Query().With("Certs").Where("id = ?", ID).First(&dns) - - return dns, err -} - -// DNSDestroy 删除 DNS -func (s *CertImpl) DNSDestroy(ID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("dns_id = ?", ID).First(&cert) - if err != nil { - return err - } - - if cert.ID != 0 { - return errors.New("该 DNS 接口下存在证书,无法删除") - } - - _, err = facades.Orm().Query().Delete(&models.CertDNS{}, ID) - return err -} - -// CertStore 添加证书 -func (s *CertImpl) CertStore(request requests.CertStore) error { - var cert models.Cert - cert.Type = request.Type - cert.Domains = request.Domains - cert.AutoRenew = request.AutoRenew - cert.UserID = request.UserID - cert.DNSID = request.DNSID - cert.WebsiteID = request.WebsiteID - - return facades.Orm().Query().Create(&cert) -} - -// CertUpdate 更新证书 -func (s *CertImpl) CertUpdate(request requests.CertUpdate) error { - var cert models.Cert - err := facades.Orm().Query().Where("id = ?", request.ID).First(&cert) - if err != nil { - return err - } - - cert.Type = request.Type - cert.Domains = request.Domains - cert.AutoRenew = request.AutoRenew - cert.UserID = request.UserID - cert.DNSID = request.DNSID - cert.WebsiteID = request.WebsiteID - - return facades.Orm().Query().Save(&cert) -} - -// CertShow 根据 ID 获取证书 -func (s *CertImpl) CertShow(ID uint) (models.Cert, error) { - var cert models.Cert - err := facades.Orm().Query().With("User").With("DNS").With("Website").Where("id = ?", ID).First(&cert) - - return cert, err -} - -// CertDestroy 删除证书 -func (s *CertImpl) CertDestroy(ID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("id = ?", ID).First(&cert) - if err != nil { - return err - } - - _, err = facades.Orm().Query().Delete(&models.Cert{}, ID) - return err -} - -// ObtainAuto 自动签发证书 -func (s *CertImpl) ObtainAuto(ID uint) (acme.Certificate, error) { - var cert models.Cert - err := facades.Orm().Query().With("Website").With("User").With("DNS").Where("id = ?", ID).First(&cert) - if err != nil { - return acme.Certificate{}, err - } - - client, err := s.getClient(cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.DNS != nil { - client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) - } else { - if cert.Website == nil { - return acme.Certificate{}, errors.New("该证书没有关联网站,无法自动签发") - } else { - for _, domain := range cert.Domains { - if strings.Contains(domain, "*") { - return acme.Certificate{}, errors.New("通配符域名无法使用 HTTP 验证") - } - } - conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) - client.UseHTTP(conf, cert.Website.Path) - } - } - - ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type)) - if err != nil { - return acme.Certificate{}, err - } - - cert.CertURL = ssl.URL - cert.Cert = string(ssl.ChainPEM) - cert.Key = string(ssl.PrivateKey) - err = facades.Orm().Query().Save(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.Website != nil { - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".pem", cert.Cert, 0644); err != nil { - return acme.Certificate{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".key", cert.Key, 0644); err != nil { - return acme.Certificate{}, err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return acme.Certificate{}, err - } - } - - return ssl, nil -} - -// ObtainManual 手动签发证书 -func (s *CertImpl) ObtainManual(ID uint) (acme.Certificate, error) { - var cert models.Cert - err := facades.Orm().Query().With("User").Where("id = ?", ID).First(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if s.client == nil { - return acme.Certificate{}, errors.New("请重新获取 DNS 解析记录") - } - - ssl, err := s.client.ObtainSSLManual() - if err != nil { - return acme.Certificate{}, err - } - - cert.CertURL = ssl.URL - cert.Cert = string(ssl.ChainPEM) - cert.Key = string(ssl.PrivateKey) - err = facades.Orm().Query().Save(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.Website != nil { - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".pem", cert.Cert, 0644); err != nil { - return acme.Certificate{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".key", cert.Key, 0644); err != nil { - return acme.Certificate{}, err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return acme.Certificate{}, err - } - } - - return ssl, nil -} - -// ManualDNS 获取手动 DNS 解析信息 -func (s *CertImpl) ManualDNS(ID uint) ([]acme.DNSRecord, error) { - var cert models.Cert - err := facades.Orm().Query().With("User").Where("id = ?", ID).First(&cert) - if err != nil { - return nil, err - } - - client, err := s.getClient(cert) - if err != nil { - return nil, err - } - - client.UseManualDns(len(cert.Domains)) - records, err := client.GetDNSRecords(context.Background(), cert.Domains, acme.KeyType(cert.Type)) - - // 15 分钟后清理客户端 - s.client = client - time.AfterFunc(15*time.Minute, func() { - s.client = nil - }) - - return records, err -} - -// Renew 续签证书 -func (s *CertImpl) Renew(ID uint) (acme.Certificate, error) { - var cert models.Cert - err := facades.Orm().Query().With("Website").With("User").With("DNS").Where("id = ?", ID).First(&cert) - if err != nil { - return acme.Certificate{}, err - } - - client, err := s.getClient(cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.CertURL == "" { - return acme.Certificate{}, errors.New("该证书没有签发成功,无法续签") - } - - if cert.DNS != nil { - client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) - } else { - if cert.Website == nil { - return acme.Certificate{}, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发") - } else { - for _, domain := range cert.Domains { - if strings.Contains(domain, "*") { - return acme.Certificate{}, errors.New("通配符域名无法使用 HTTP 验证") - } - } - conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) - client.UseHTTP(conf, cert.Website.Path) - } - } - - ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type)) - if err != nil { - return acme.Certificate{}, err - } - - cert.CertURL = ssl.URL - cert.Cert = string(ssl.ChainPEM) - cert.Key = string(ssl.PrivateKey) - err = facades.Orm().Query().Save(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.Website != nil { - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".pem", cert.Cert, 0644); err != nil { - return acme.Certificate{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".key", cert.Key, 0644); err != nil { - return acme.Certificate{}, err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return acme.Certificate{}, err - } - } - - return ssl, nil -} - -// Deploy 部署证书 -func (s *CertImpl) Deploy(ID, WebsiteID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("id = ?", ID).First(&cert) - if err != nil { - return err - } - - if cert.Cert == "" || cert.Key == "" { - return errors.New("该证书没有签发成功,无法部署") - } - - website := models.Website{} - err = facades.Orm().Query().Where("id = ?", WebsiteID).First(&website) - if err != nil { - return err - } - - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".pem", cert.Cert, 0644); err != nil { - return err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".key", cert.Key, 0644); err != nil { - return err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return err - } - - return nil -} - -func (s *CertImpl) getClient(cert models.Cert) (*acme.Client, error) { - var ca string - var eab *acme.EAB - switch cert.User.CA { - case "letsencrypt": - ca = acme.CALetsEncrypt - case "buypass": - ca = acme.CABuypass - case "zerossl": - ca = acme.CAZeroSSL - eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded} - case "sslcom": - ca = acme.CASSLcom - eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded} - case "google": - ca = acme.CAGoogle - eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded} - } - - return acme.NewPrivateKeyAccount(cert.User.Email, cert.User.PrivateKey, ca, eab) -} diff --git a/internal/services/container.go b/internal/services/container.go deleted file mode 100644 index 3bcd7bf8..00000000 --- a/internal/services/container.go +++ /dev/null @@ -1,374 +0,0 @@ -package services - -import ( - "context" - "encoding/base64" - "io" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "github.com/goravel/framework/support/json" - - requests "github.com/TheTNB/panel/v2/app/http/requests/container" - paneltypes "github.com/TheTNB/panel/v2/pkg/types" -) - -type Container struct { - client *client.Client -} - -func NewContainer(sock ...string) Container { - if len(sock) == 0 { - sock = append(sock, "/run/podman/podman.sock") - } - cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) - return Container{ - client: cli, - } -} - -// ContainerListAll 列出所有容器 -func (r *Container) ContainerListAll() ([]types.Container, error) { - containers, err := r.client.ContainerList(context.Background(), container.ListOptions{ - All: true, - }) - if err != nil { - return nil, err - } - - return containers, nil -} - -// ContainerListByNames 根据名称列出容器 -func (r *Container) ContainerListByNames(names []string) ([]types.Container, error) { - var options container.ListOptions - options.All = true - if len(names) > 0 { - var array []filters.KeyValuePair - for _, n := range names { - array = append(array, filters.Arg("name", n)) - } - options.Filters = filters.NewArgs(array...) - } - containers, err := r.client.ContainerList(context.Background(), options) - if err != nil { - return nil, err - } - - return containers, nil -} - -// ContainerCreate 创建容器 -func (r *Container) ContainerCreate(name string, config container.Config, host container.HostConfig, network network.NetworkingConfig) (string, error) { - resp, err := r.client.ContainerCreate(context.Background(), &config, &host, &network, nil, name) - return resp.ID, err -} - -// ContainerRemove 移除容器 -func (r *Container) ContainerRemove(id string) error { - return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{ - Force: true, - }) -} - -// ContainerStart 启动容器 -func (r *Container) ContainerStart(id string) error { - return r.client.ContainerStart(context.Background(), id, container.StartOptions{}) -} - -// ContainerStop 停止容器 -func (r *Container) ContainerStop(id string) error { - return r.client.ContainerStop(context.Background(), id, container.StopOptions{}) -} - -// ContainerRestart 重启容器 -func (r *Container) ContainerRestart(id string) error { - return r.client.ContainerRestart(context.Background(), id, container.StopOptions{}) -} - -// ContainerPause 暂停容器 -func (r *Container) ContainerPause(id string) error { - return r.client.ContainerPause(context.Background(), id) -} - -// ContainerUnpause 恢复容器 -func (r *Container) ContainerUnpause(id string) error { - return r.client.ContainerUnpause(context.Background(), id) -} - -// ContainerInspect 查看容器 -func (r *Container) ContainerInspect(id string) (types.ContainerJSON, error) { - return r.client.ContainerInspect(context.Background(), id) -} - -// ContainerKill 杀死容器 -func (r *Container) ContainerKill(id string) error { - return r.client.ContainerKill(context.Background(), id, "KILL") -} - -// ContainerRename 重命名容器 -func (r *Container) ContainerRename(id string, newName string) error { - return r.client.ContainerRename(context.Background(), id, newName) -} - -// ContainerStats 查看容器状态 -func (r *Container) ContainerStats(id string) (container.StatsResponseReader, error) { - return r.client.ContainerStats(context.Background(), id, false) -} - -// ContainerExist 判断容器是否存在 -func (r *Container) ContainerExist(name string) (bool, error) { - var options container.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", name)) - containers, err := r.client.ContainerList(context.Background(), options) - if err != nil { - return false, err - } - - return len(containers) > 0, nil -} - -// ContainerUpdate 更新容器 -func (r *Container) ContainerUpdate(id string, config container.UpdateConfig) error { - _, err := r.client.ContainerUpdate(context.Background(), id, config) - return err -} - -// ContainerLogs 查看容器日志 -func (r *Container) ContainerLogs(id string) (string, error) { - options := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - } - reader, err := r.client.ContainerLogs(context.Background(), id, options) - if err != nil { - return "", err - } - defer reader.Close() - - data, err := io.ReadAll(reader) - if err != nil { - return "", err - } - - return string(data), nil -} - -// ContainerPrune 清理未使用的容器 -func (r *Container) ContainerPrune() error { - _, err := r.client.ContainersPrune(context.Background(), filters.NewArgs()) - return err -} - -// NetworkList 列出网络 -func (r *Container) NetworkList() ([]network.Inspect, error) { - return r.client.NetworkList(context.Background(), network.ListOptions{}) -} - -// NetworkCreate 创建网络 -func (r *Container) NetworkCreate(config requests.NetworkCreate) (string, error) { - var ipamConfigs []network.IPAMConfig - if config.Ipv4.Enabled { - ipamConfigs = append(ipamConfigs, network.IPAMConfig{ - Subnet: config.Ipv4.Subnet, - Gateway: config.Ipv4.Gateway, - IPRange: config.Ipv4.IPRange, - }) - } - if config.Ipv6.Enabled { - ipamConfigs = append(ipamConfigs, network.IPAMConfig{ - Subnet: config.Ipv6.Subnet, - Gateway: config.Ipv6.Gateway, - IPRange: config.Ipv6.IPRange, - }) - } - - options := network.CreateOptions{ - EnableIPv6: &config.Ipv6.Enabled, - Driver: config.Driver, - Options: r.KVToMap(config.Options), - Labels: r.KVToMap(config.Labels), - } - if len(ipamConfigs) > 0 { - options.IPAM = &network.IPAM{ - Config: ipamConfigs, - } - } - - resp, err := r.client.NetworkCreate(context.Background(), config.Name, options) - return resp.ID, err -} - -// NetworkRemove 删除网络 -func (r *Container) NetworkRemove(id string) error { - return r.client.NetworkRemove(context.Background(), id) -} - -// NetworkExist 判断网络是否存在 -func (r *Container) NetworkExist(name string) (bool, error) { - var options network.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", name)) - networks, err := r.client.NetworkList(context.Background(), options) - if err != nil { - return false, err - } - - return len(networks) > 0, nil -} - -// NetworkInspect 查看网络 -func (r *Container) NetworkInspect(id string) (network.Inspect, error) { - return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{}) -} - -// NetworkConnect 连接网络 -func (r *Container) NetworkConnect(networkID string, containerID string) error { - return r.client.NetworkConnect(context.Background(), networkID, containerID, nil) -} - -// NetworkDisconnect 断开网络 -func (r *Container) NetworkDisconnect(networkID string, containerID string) error { - return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true) -} - -// NetworkPrune 清理未使用的网络 -func (r *Container) NetworkPrune() error { - _, err := r.client.NetworksPrune(context.Background(), filters.NewArgs()) - return err -} - -// ImageList 列出镜像 -func (r *Container) ImageList() ([]image.Summary, error) { - return r.client.ImageList(context.Background(), image.ListOptions{ - All: true, - }) -} - -// ImageExist 判断镜像是否存在 -func (r *Container) ImageExist(id string) (bool, error) { - var options image.ListOptions - options.Filters = filters.NewArgs(filters.Arg("reference", id)) - images, err := r.client.ImageList(context.Background(), options) - if err != nil { - return false, err - } - - return len(images) > 0, nil -} - -// ImagePull 拉取镜像 -func (r *Container) ImagePull(config requests.ImagePull) error { - options := image.PullOptions{} - if config.Auth { - authConfig := registry.AuthConfig{ - Username: config.Username, - Password: config.Password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - return err - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - options.RegistryAuth = authStr - } - - out, err := r.client.ImagePull(context.Background(), config.Name, options) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(io.Discard, out) - return err -} - -// ImageRemove 删除镜像 -func (r *Container) ImageRemove(id string) error { - _, err := r.client.ImageRemove(context.Background(), id, image.RemoveOptions{ - Force: true, - PruneChildren: true, - }) - return err -} - -// ImagePrune 清理未使用的镜像 -func (r *Container) ImagePrune() error { - _, err := r.client.ImagesPrune(context.Background(), filters.NewArgs()) - return err -} - -// ImageInspect 查看镜像 -func (r *Container) ImageInspect(id string) (types.ImageInspect, error) { - img, _, err := r.client.ImageInspectWithRaw(context.Background(), id) - return img, err -} - -// VolumeList 列出存储卷 -func (r *Container) VolumeList() ([]*volume.Volume, error) { - volumes, err := r.client.VolumeList(context.Background(), volume.ListOptions{}) - return volumes.Volumes, err -} - -// VolumeCreate 创建存储卷 -func (r *Container) VolumeCreate(config requests.VolumeCreate) (volume.Volume, error) { - return r.client.VolumeCreate(context.Background(), volume.CreateOptions{ - Name: config.Name, - Driver: config.Driver, - DriverOpts: r.KVToMap(config.Options), - Labels: r.KVToMap(config.Labels), - }) -} - -// VolumeExist 判断存储卷是否存在 -func (r *Container) VolumeExist(id string) (bool, error) { - var options volume.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", id)) - volumes, err := r.client.VolumeList(context.Background(), options) - if err != nil { - return false, err - } - - return len(volumes.Volumes) > 0, nil -} - -// VolumeInspect 查看存储卷 -func (r *Container) VolumeInspect(id string) (volume.Volume, error) { - return r.client.VolumeInspect(context.Background(), id) -} - -// VolumeRemove 删除存储卷 -func (r *Container) VolumeRemove(id string) error { - return r.client.VolumeRemove(context.Background(), id, true) -} - -// VolumePrune 清理未使用的存储卷 -func (r *Container) VolumePrune() error { - _, err := r.client.VolumesPrune(context.Background(), filters.NewArgs()) - return err -} - -// KVToMap 将 key-value 切片转换为 map -func (r *Container) KVToMap(kvs []paneltypes.KV) map[string]string { - m := make(map[string]string) - for _, item := range kvs { - m[item.Key] = item.Value - } - - return m -} - -// KVToSlice 将 key-value 切片转换为 key=value 切片 -func (r *Container) KVToSlice(kvs []paneltypes.KV) []string { - var s []string - for _, item := range kvs { - s = append(s, item.Key+"="+item.Value) - } - - return s -} diff --git a/internal/services/cron.go b/internal/services/cron.go deleted file mode 100644 index 4233a0a4..00000000 --- a/internal/services/cron.go +++ /dev/null @@ -1,48 +0,0 @@ -package services - -import ( - "errors" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type CronImpl struct { -} - -func NewCronImpl() *CronImpl { - return &CronImpl{} -} - -// AddToSystem 添加到系统 -func (r *CronImpl) AddToSystem(cron models.Cron) error { - if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { - return err - } - - return r.restartCron() -} - -// DeleteFromSystem 从系统中删除 -func (r *CronImpl) DeleteFromSystem(cron models.Cron) error { - if _, err := shell.Execf(`( crontab -l | grep -v -F "%s %s >> %s 2>&1" ) | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { - return err - } - - return r.restartCron() -} - -// restartCron 重启 cron 服务 -func (r *CronImpl) restartCron() error { - if os.IsRHEL() { - return systemctl.Restart("crond") - } - - if os.IsDebian() || os.IsUbuntu() { - return systemctl.Restart("cron") - } - - return errors.New("不支持的系统") -} diff --git a/internal/services/php.go b/internal/services/php.go deleted file mode 100644 index 7ea387ab..00000000 --- a/internal/services/php.go +++ /dev/null @@ -1,356 +0,0 @@ -package services - -import ( - "errors" - "fmt" - "regexp" - "slices" - "strings" - "time" - - "github.com/go-resty/resty/v2" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PHPImpl struct { - version string -} - -func NewPHPImpl(version uint) *PHPImpl { - return &PHPImpl{ - version: cast.ToString(version), - } -} - -func (r *PHPImpl) Reload() error { - return systemctl.Reload("php-fpm-" + r.version) -} - -func (r *PHPImpl) GetConfig() (string, error) { - return io.Read("/www/server/php/" + r.version + "/etc/php.ini") -} - -func (r *PHPImpl) SaveConfig(config string) error { - if err := io.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil { - return err - } - - return r.Reload() -} - -func (r *PHPImpl) GetFPMConfig() (string, error) { - return io.Read("/www/server/php/" + r.version + "/etc/php-fpm.conf") -} - -func (r *PHPImpl) SaveFPMConfig(config string) error { - if err := io.Write("/www/server/php/"+r.version+"/etc/php-fpm.conf", config, 0644); err != nil { - return err - } - - return r.Reload() -} - -func (r *PHPImpl) Load() ([]types.NV, error) { - client := resty.New().SetTimeout(10 * time.Second) - resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version) - if err != nil || !resp.IsSuccess() { - return []types.NV{}, nil - } - - raw := resp.String() - dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"} - regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"} - - data := make([]types.NV, len(dataKeys)) - for i := range dataKeys { - data[i].Name = dataKeys[i] - - r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i])) - match := r.FindStringSubmatch(raw) - - if len(match) > 1 { - data[i].Value = strings.TrimSpace(match[1]) - } - } - - return data, nil -} - -func (r *PHPImpl) GetErrorLog() (string, error) { - return shell.Execf("tail -n 500 /www/server/php/%s/var/log/php-fpm.log", r.version) -} - -func (r *PHPImpl) GetSlowLog() (string, error) { - return shell.Execf("tail -n 500 /www/server/php/%s/var/log/slow.log", r.version) -} - -func (r *PHPImpl) ClearErrorLog() error { - if out, err := shell.Execf("echo '' > /www/server/php/%s/var/log/php-fpm.log", r.version); err != nil { - return errors.New(out) - } - - return r.Reload() -} - -func (r *PHPImpl) ClearSlowLog() error { - if out, err := shell.Execf("echo '' > /www/server/php/%s/var/log/slow.log", r.version); err != nil { - return errors.New(out) - } - - return nil -} - -func (r *PHPImpl) GetExtensions() ([]types.PHPExtension, error) { - extensions := []types.PHPExtension{ - { - Name: "fileinfo", - Slug: "fileinfo", - Description: "Fileinfo 是一个用于识别文件类型的库。", - Installed: false, - }, - { - Name: "OPcache", - Slug: "Zend OPcache", - Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。", - Installed: false, - }, - { - Name: "PhpRedis", - Slug: "redis", - Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据。", - Installed: false, - }, - { - Name: "ImageMagick", - Slug: "imagick", - Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。", - Installed: false, - }, - { - Name: "exif", - Slug: "exif", - Description: "通过 exif 扩展,你可以操作图像元数据。", - Installed: false, - }, - { - Name: "pdo_pgsql", - Slug: "pdo_pgsql", - Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。", - Installed: false, - }, - { - Name: "imap", - Slug: "imap", - Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件。", - Installed: false, - }, - { - Name: "zip", - Slug: "zip", - Description: "Zip 是一个用于处理 ZIP 文件的库。", - Installed: false, - }, - { - Name: "bz2", - Slug: "bz2", - Description: "Bzip2 是一个用于压缩和解压缩文件的库。", - Installed: false, - }, - { - Name: "readline", - Slug: "readline", - Description: "Readline 是一个库,它提供了一种用于处理文本的接口。", - Installed: false, - }, - { - Name: "snmp", - Slug: "snmp", - Description: "SNMP 是一种用于网络管理的协议。", - Installed: false, - }, - { - Name: "ldap", - Slug: "ldap", - Description: "LDAP 是一种用于访问目录服务的协议。", - }, - { - Name: "enchant", - Slug: "enchant", - Description: "Enchant 是一个拼写检查库。", - Installed: false, - }, - { - Name: "pspell", - Slug: "pspell", - Description: "Pspell 是一个拼写检查库。", - Installed: false, - }, - { - Name: "calendar", - Slug: "calendar", - Description: "Calendar 是一个用于处理日期的库。", - Installed: false, - }, - { - Name: "gmp", - Slug: "gmp", - Description: "GMP 是一个用于处理大整数的库。", - Installed: false, - }, - { - Name: "sysvmsg", - Slug: "sysvmsg", - Description: "Sysvmsg 是一个用于处理 System V 消息队列的库。", - Installed: false, - }, - { - Name: "sysvsem", - Slug: "sysvsem", - Description: "Sysvsem 是一个用于处理 System V 信号量的库。", - }, - { - Name: "sysvshm", - Slug: "sysvshm", - Description: "Sysvshm 是一个用于处理 System V 共享内存的库。", - Installed: false, - }, - { - Name: "xsl", - Slug: "xsl", - Description: "XSL 是一个用于处理 XML 文档的库。", - Installed: false, - }, - { - Name: "intl", - Slug: "intl", - Description: "Intl 是一个用于处理国际化和本地化的库。", - Installed: false, - }, - { - Name: "gettext", - Slug: "gettext", - Description: "Gettext 是一个用于处理多语言的库。", - Installed: false, - }, - { - Name: "igbinary", - Slug: "igbinary", - Description: "Igbinary 是一个用于序列化和反序列化数据的库。", - Installed: false, - }, - } - - // ionCube 只支持 PHP 8.3 以下版本 - if cast.ToUint(r.version) < 83 { - extensions = append(extensions, types.PHPExtension{ - Name: "ionCube", - Slug: "ionCube Loader", - Description: "ionCube 是一个专业级的 PHP 加密解密工具。", - Installed: false, - }) - } - // Swoole 和 Swow 不支持 PHP 8.0 以下版本 - if cast.ToUint(r.version) >= 80 { - extensions = append(extensions, types.PHPExtension{ - Name: "Swoole", - Slug: "swoole", - Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", - Installed: false, - }) - extensions = append(extensions, types.PHPExtension{ - Name: "Swow", - Slug: "Swow", - Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", - Installed: false, - }) - } - - raw, err := shell.Execf("/www/server/php/%s/bin/php -m", r.version) - if err != nil { - return extensions, err - } - - extensionMap := make(map[string]*types.PHPExtension) - for i := range extensions { - extensionMap[extensions[i].Slug] = &extensions[i] - } - - rawExtensionList := strings.Split(raw, "\n") - for _, item := range rawExtensionList { - if ext, exists := extensionMap[item]; exists && !strings.Contains(item, "[") && item != "" { - ext.Installed = true - } - } - - return extensions, nil -} - -func (r *PHPImpl) InstallExtension(slug string) error { - if !r.checkExtension(slug) { - return errors.New("扩展不存在") - } - - shell := fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/%s.sh' install %s >> '/tmp/%s.log' 2>&1`, slug, r.version, slug) - - officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"} - if slices.Contains(officials, slug) { - shell = fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/official.sh' install '%s' '%s' >> '/tmp/%s.log' 2>&1`, r.version, slug, slug) - } - - var task models.Task - task.Name = "安装PHP-" + r.version + "扩展-" + slug - task.Status = models.TaskStatusWaiting - task.Shell = shell - task.Log = "/tmp/" + slug + ".log" - if err := facades.Orm().Query().Create(&task); err != nil { - return err - } - - return NewTaskImpl().Process(task.ID) -} - -func (r *PHPImpl) UninstallExtension(slug string) error { - if !r.checkExtension(slug) { - return errors.New("扩展不存在") - } - - shell := fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/%s.sh' uninstall %s >> '/tmp/%s.log' 2>&1`, slug, r.version, slug) - - officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"} - if slices.Contains(officials, slug) { - shell = fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/official.sh' uninstall '%s' '%s' >> '/tmp/%s.log' 2>&1`, r.version, slug, slug) - } - - var task models.Task - task.Name = "卸载PHP-" + r.version + "扩展-" + slug - task.Status = models.TaskStatusWaiting - task.Shell = shell - task.Log = "/tmp/" + slug + ".log" - if err := facades.Orm().Query().Create(&task); err != nil { - return err - } - - return NewTaskImpl().Process(task.ID) -} - -func (r *PHPImpl) checkExtension(slug string) bool { - extensions, err := r.GetExtensions() - if err != nil { - return false - } - - for _, item := range extensions { - if item.Slug == slug { - return true - } - } - - return false -} diff --git a/internal/services/plugin.go b/internal/services/plugin.go deleted file mode 100644 index 9ea0f3a1..00000000 --- a/internal/services/plugin.go +++ /dev/null @@ -1,247 +0,0 @@ -// Package services 插件服务 -package services - -import ( - "errors" - - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/app/plugins/loader" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PluginImpl struct { - task internal.Task -} - -func NewPluginImpl() *PluginImpl { - return &PluginImpl{ - task: NewTaskImpl(), - } -} - -// AllInstalled 获取已安装的所有插件 -func (r *PluginImpl) AllInstalled() ([]models.Plugin, error) { - var plugins []models.Plugin - if err := facades.Orm().Query().Get(&plugins); err != nil { - return plugins, err - } - - return plugins, nil -} - -// All 获取所有插件 -func (r *PluginImpl) All() []*types.Plugin { - var _ = []types.Plugin{ - types.PluginOpenResty, - types.PluginMySQL57, - types.PluginMySQL80, - types.PluginMySQL84, - types.PluginPostgreSQL15, - types.PluginPostgreSQL16, - types.PluginPHP74, - types.PluginPHP80, - types.PluginPHP81, - types.PluginPHP82, - types.PluginPHP83, - types.PluginPHPMyAdmin, - types.PluginPureFTPd, - types.PluginRedis, - types.PluginS3fs, - types.PluginRsync, - types.PluginSupervisor, - types.PluginFail2ban, - types.PluginPodman, - types.PluginFrp, - types.PluginGitea, - types.PluginToolBox, - } - - return loader.All() -} - -// GetBySlug 根据 slug 获取插件 -func (r *PluginImpl) GetBySlug(slug string) *types.Plugin { - for _, item := range r.All() { - if item.Slug == slug { - return item - } - } - - return &types.Plugin{} -} - -// GetInstalledBySlug 根据 slug 获取已安装的插件 -func (r *PluginImpl) GetInstalledBySlug(slug string) models.Plugin { - var plugin models.Plugin - _ = facades.Orm().Query().Where("slug", slug).Get(&plugin) - return plugin -} - -// Install 安装插件 -func (r *PluginImpl) Install(slug string) error { - plugin := r.GetBySlug(slug) - installedPlugin := r.GetInstalledBySlug(slug) - installedPlugins, err := r.AllInstalled() - if err != nil { - return err - } - - if installedPlugin.ID != 0 { - return errors.New("插件已安装") - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") - } - } - - if err = r.checkTaskExists(slug); err != nil { - return err - } - - var task models.Task - task.Name = "安装插件 " + plugin.Name - task.Status = models.TaskStatusWaiting - task.Shell = plugin.Install + ` >> '/tmp/` + plugin.Slug + `.log' 2>&1` - task.Log = "/tmp/" + plugin.Slug + ".log" - if err = facades.Orm().Query().Create(&task); err != nil { - return errors.New("创建任务失败") - } - - _ = io.Remove(task.Log) - return r.task.Process(task.ID) -} - -// Uninstall 卸载插件 -func (r *PluginImpl) Uninstall(slug string) error { - plugin := r.GetBySlug(slug) - installedPlugin := r.GetInstalledBySlug(slug) - installedPlugins, err := r.AllInstalled() - if err != nil { - return err - } - - if installedPlugin.ID == 0 { - return errors.New("插件未安装") - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") - } - } - - if err = r.checkTaskExists(slug); err != nil { - return err - } - - var task models.Task - task.Name = "卸载插件 " + plugin.Name - task.Status = models.TaskStatusWaiting - task.Shell = plugin.Uninstall + " >> /tmp/" + plugin.Slug + ".log 2>&1" - task.Log = "/tmp/" + plugin.Slug + ".log" - if err = facades.Orm().Query().Create(&task); err != nil { - return errors.New("创建任务失败") - } - - _ = io.Remove(task.Log) - return r.task.Process(task.ID) -} - -// Update 更新插件 -func (r *PluginImpl) Update(slug string) error { - plugin := r.GetBySlug(slug) - installedPlugin := r.GetInstalledBySlug(slug) - installedPlugins, err := r.AllInstalled() - if err != nil { - return err - } - - if installedPlugin.ID == 0 { - return errors.New("插件未安装") - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") - } - } - - if err = r.checkTaskExists(slug); err != nil { - return err - } - - var task models.Task - task.Name = "更新插件 " + plugin.Name - task.Status = models.TaskStatusWaiting - task.Shell = plugin.Update + " >> /tmp/" + plugin.Slug + ".log 2>&1" - task.Log = "/tmp/" + plugin.Slug + ".log" - if err = facades.Orm().Query().Create(&task); err != nil { - return errors.New("创建任务失败") - } - - _ = io.Remove(task.Log) - return r.task.Process(task.ID) -} - -func (r *PluginImpl) checkTaskExists(slug string) error { - var count int64 - if err := facades.Orm().Query(). - Model(&models.Task{}). - Where("log LIKE ? AND (status = ? OR status = ?)", "%"+slug+"%", models.TaskStatusWaiting, models.TaskStatusRunning). - Count(&count); err != nil { - return errors.New("查询任务失败") - } - if count > 0 { - return errors.New("任务已添加,请勿重复添加") - } - - return nil -} diff --git a/internal/services/setting.go b/internal/services/setting.go deleted file mode 100644 index 53889871..00000000 --- a/internal/services/setting.go +++ /dev/null @@ -1,50 +0,0 @@ -// Package services 设置服务 -package services - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type SettingImpl struct { -} - -func NewSettingImpl() *SettingImpl { - return &SettingImpl{} -} - -// Get 获取设置 -func (r *SettingImpl) Get(key string, defaultValue ...string) string { - var setting models.Setting - if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil { - return str.FirstElement(defaultValue) - } - - if len(setting.Value) == 0 { - return str.FirstElement(defaultValue) - } - - return setting.Value -} - -// Set 更新或创建设置 -func (r *SettingImpl) Set(key, value string) error { - var setting models.Setting - if err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{Key: key}, models.Setting{Value: value}); err != nil { - return err - } - - return nil -} - -// Delete 删除设置 -func (r *SettingImpl) Delete(key string) error { - var setting models.Setting - if _, err := facades.Orm().Query().Where("key", key).Delete(&setting); err != nil { - return err - } - - return nil -} diff --git a/internal/services/task.go b/internal/services/task.go deleted file mode 100644 index f36f13af..00000000 --- a/internal/services/task.go +++ /dev/null @@ -1,42 +0,0 @@ -package services - -import ( - "sync" - - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/jobs" - "github.com/TheTNB/panel/v2/app/models" -) - -var taskMap sync.Map - -type TaskImpl struct { -} - -func NewTaskImpl() *TaskImpl { - return &TaskImpl{} -} - -func (r *TaskImpl) Process(taskID uint) error { - taskMap.Store(taskID, true) - return facades.Queue().Job(&jobs.ProcessTask{}, []any{taskID}).Dispatch() -} - -func (r *TaskImpl) DispatchWaiting() error { - var tasks []models.Task - if err := facades.Orm().Query().Where("status = ?", models.TaskStatusWaiting).Find(&tasks); err != nil { - return err - } - - for _, task := range tasks { - if _, ok := taskMap.Load(task.ID); ok { - continue - } - if err := r.Process(task.ID); err != nil { - return err - } - } - - return nil -} diff --git a/internal/services/user.go b/internal/services/user.go deleted file mode 100644 index a42764bd..00000000 --- a/internal/services/user.go +++ /dev/null @@ -1,35 +0,0 @@ -// Package services 用户服务 -package services - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" -) - -type UserImpl struct { -} - -func NewUserImpl() *UserImpl { - return &UserImpl{} -} - -func (r *UserImpl) Create(username, password string) (models.User, error) { - user := models.User{ - Username: username, - Password: password, - } - if err := facades.Orm().Query().Create(&user); err != nil { - return user, err - } - - return user, nil -} - -func (r *UserImpl) Update(user models.User) (models.User, error) { - if _, err := facades.Orm().Query().Update(&user); err != nil { - return user, err - } - - return user, nil -} diff --git a/internal/services/website.go b/internal/services/website.go deleted file mode 100644 index dfa2d43d..00000000 --- a/internal/services/website.go +++ /dev/null @@ -1,630 +0,0 @@ -// Package services 网站服务 -package services - -import ( - "errors" - "fmt" - "path/filepath" - "regexp" - "slices" - "strconv" - "strings" - - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/embed" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/db" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type WebsiteImpl struct { - setting internal.Setting -} - -func NewWebsiteImpl() *WebsiteImpl { - return &WebsiteImpl{ - setting: NewSettingImpl(), - } -} - -// List 列出网站 -func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) { - var websites []models.Website - var total int64 - if err := facades.Orm().Query().Paginate(page, limit, &websites, &total); err != nil { - return total, websites, err - } - - return total, websites, nil -} - -// Add 添加网站 -func (r *WebsiteImpl) Add(website requests.Add) (models.Website, error) { - w := models.Website{ - Name: website.Name, - Status: true, - Path: website.Path, - PHP: cast.ToInt(website.PHP), - SSL: false, - } - if err := facades.Orm().Query().Create(&w); err != nil { - return models.Website{}, err - } - - if err := io.Mkdir(website.Path, 0755); err != nil { - return models.Website{}, err - } - - index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html")) - if err != nil { - return models.Website{}, fmt.Errorf("获取index模板文件失败: %w", err) - } - if err = io.Write(website.Path+"/index.html", string(index), 0644); err != nil { - return models.Website{}, err - } - - notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html")) - if err != nil { - return models.Website{}, fmt.Errorf("获取404模板文件失败: %w", err) - } - if err = io.Write(website.Path+"/404.html", string(notFound), 0644); err != nil { - return models.Website{}, err - } - - portList := "" - domainList := "" - portUsed := make(map[uint]bool) - domainUsed := make(map[string]bool) - - for i, port := range website.Ports { - if _, ok := portUsed[port]; !ok { - if i == len(website.Ports)-1 { - portList += " listen " + cast.ToString(port) + ";\n" - portList += " listen [::]:" + cast.ToString(port) + ";" - } else { - portList += " listen " + cast.ToString(port) + ";\n" - portList += " listen [::]:" + cast.ToString(port) + ";\n" - } - portUsed[port] = true - } - } - for _, domain := range website.Domains { - if _, ok := domainUsed[domain]; !ok { - domainList += " " + domain - domainUsed[domain] = true - } - } - - nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 -%s - # port标记位结束 - # server_name标记位开始 - server_name%s; - # server_name标记位结束 - # index标记位开始 - index index.php index.html; - # index标记位结束 - # root标记位开始 - root %s; - # root标记位结束 - - # ssl标记位开始 - # ssl标记位结束 - - # php标记位开始 - include enable-php-%s.conf; - # php标记位结束 - - # waf标记位开始 - waf off; - waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; - waf_mode DYNAMIC; - waf_cc_deny rate=1000r/m duration=60m; - waf_cache capacity=50; - # waf标记位结束 - - # 错误页配置,可自行设置 - error_page 404 /404.html; - #error_page 502 /502.html; - - # acme证书签发配置,不可修改 - include /www/server/vhost/acme/%s.conf; - - # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 - include /www/server/vhost/rewrite/%s.conf; - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log /www/wwwlogs/%s.log; - error_log /www/wwwlogs/%s.log; -} -`, portList, domainList, website.Path, website.PHP, website.Name, website.Name, website.Name, website.Name) - - if err = io.Write("/www/server/vhost/"+website.Name+".conf", nginxConf, 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".pem", "", 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".key", "", 0644); err != nil { - return models.Website{}, err - } - - if err = io.Chmod(website.Path, 0755); err != nil { - return models.Website{}, err - } - if err = io.Chown(website.Path, "www", "www"); err != nil { - return models.Website{}, err - } - - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return models.Website{}, err - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - if website.DB && website.DBType == "mysql" { - mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") - if err != nil { - return models.Website{}, err - } - if err = mysql.DatabaseCreate(website.DBName); err != nil { - return models.Website{}, err - } - if err = mysql.UserCreate(website.DBUser, website.DBPassword); err != nil { - return models.Website{}, err - } - if err = mysql.PrivilegesGrant(website.DBUser, website.DBName); err != nil { - return models.Website{}, err - } - } - if website.DB && website.DBType == "postgresql" { - _, _ = shell.Execf(`echo "CREATE DATABASE '%s';" | su - postgres -c "psql"`, website.DBName) - _, _ = shell.Execf(`echo "CREATE USER '%s' WITH PASSWORD '%s';" | su - postgres -c "psql"`, website.DBUser, website.DBPassword) - _, _ = shell.Execf(`echo "ALTER DATABASE '%s' OWNER TO '%s';" | su - postgres -c "psql"`, website.DBName, website.DBUser) - _, _ = shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE '%s' TO '%s';" | su - postgres -c "psql"`, website.DBName, website.DBUser) - userConfig := "host " + website.DBName + " " + website.DBUser + " 127.0.0.1/32 scram-sha-256" - _, _ = shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`) - _ = systemctl.Reload("postgresql") - } - - return w, nil -} - -// SaveConfig 保存网站配置 -func (r *WebsiteImpl) SaveConfig(config requests.SaveConfig) error { - var website models.Website - if err := facades.Orm().Query().Where("id", config.ID).First(&website); err != nil { - return err - } - - if !website.Status { - return errors.New("网站已停用,请先启用") - } - - // 原文 - raw, err := io.Read("/www/server/vhost/" + website.Name + ".conf") - if err != nil { - return err - } - if strings.TrimSpace(raw) != strings.TrimSpace(config.Raw) { - if err = io.Write("/www/server/vhost/"+website.Name+".conf", config.Raw, 0644); err != nil { - return err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return err - } - - return nil - } - - // 目录 - path := config.Path - if !io.Exists(path) { - return errors.New("网站目录不存在") - } - website.Path = path - - // 域名 - domain := "server_name" - domains := config.Domains - for _, v := range domains { - if v == "" { - continue - } - domain += " " + v - } - domain += ";" - domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束") - if len(strings.TrimSpace(domainConfigOld)) == 0 { - return errors.New("配置文件中缺少server_name标记位") - } - raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1) - - // 端口 - var portConf strings.Builder - ports := config.Ports - for _, port := range ports { - https := "" - quic := false - if slices.Contains(config.SSLPorts, port) { - https = " ssl" - if slices.Contains(config.QUICPorts, port) { - quic = true - } - } - - portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https)) - portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https)) - if quic { - portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic")) - portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic")) - } - } - portConf.WriteString(" ") - portConfNew := portConf.String() - portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束") - if len(strings.TrimSpace(portConfOld)) == 0 { - return errors.New("配置文件中缺少port标记位") - } - raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1) - - // 运行目录 - root := str.Cut(raw, "# root标记位开始", "# root标记位结束") - if len(strings.TrimSpace(root)) == 0 { - return errors.New("配置文件中缺少root标记位") - } - match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root) - if len(match) != 2 { - return errors.New("配置文件中root标记位格式错误") - } - rootNew := strings.Replace(root, match[1], config.Root, -1) - raw = strings.Replace(raw, root, rootNew, -1) - - // 默认文件 - index := str.Cut(raw, "# index标记位开始", "# index标记位结束") - if len(strings.TrimSpace(index)) == 0 { - return errors.New("配置文件中缺少index标记位") - } - match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index) - if len(match) != 2 { - return errors.New("配置文件中index标记位格式错误") - } - indexNew := strings.Replace(index, match[1], config.Index, -1) - raw = strings.Replace(raw, index, indexNew, -1) - - // 防跨站 - root = config.Root - if !strings.HasSuffix(root, "/") { - root += "/" - } - if config.OpenBasedir { - if err = io.Write(root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil { - return err - } - } else { - if io.Exists(root + ".user.ini") { - if err = io.Remove(root + ".user.ini"); err != nil { - return err - } - } - } - - // WAF - waf := config.Waf - wafStr := "off" - if waf { - wafStr = "on" - } - wafMode := config.WafMode - wafCcDeny := config.WafCcDeny - wafCache := config.WafCache - wafConfig := `# waf标记位开始 - waf ` + wafStr + `; - waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; - waf_mode ` + wafMode + `; - waf_cc_deny ` + wafCcDeny + `; - waf_cache ` + wafCache + `; - ` - wafConfigOld := str.Cut(raw, "# waf标记位开始", "# waf标记位结束") - if len(strings.TrimSpace(wafConfigOld)) != 0 { - raw = strings.Replace(raw, wafConfigOld, "", -1) - } - raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1) - - // SSL - ssl := config.SSL - website.SSL = ssl - if ssl { - if _, err = cert.ParseCert(config.SSLCertificate); err != nil { - return errors.New("TLS证书格式错误") - } - if _, err = cert.ParseKey(config.SSLCertificateKey); err != nil { - return errors.New("TLS私钥格式错误") - } - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".pem", config.SSLCertificate, 0644); err != nil { - return err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".key", config.SSLCertificateKey, 0644); err != nil { - return err - } - if ssl { - sslConfig := `# ssl标记位开始 - ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem; - ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key; - ssl_session_timeout 1d; - ssl_session_cache shared:SSL:10m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - ssl_early_data on; - ` - if config.HTTPRedirect { - sslConfig += `# http重定向标记位开始 - if ($server_port !~ 443){ - return 301 https://$host$request_uri; - } - error_page 497 https://$host$request_uri; - # http重定向标记位结束 - ` - } - if config.HSTS { - sslConfig += `# hsts标记位开始 - add_header Strict-Transport-Security "max-age=63072000" always; - # hsts标记位结束 - ` - } - if config.OCSP { - sslConfig += `# ocsp标记位开始 - ssl_stapling on; - ssl_stapling_verify on; - # ocsp标记位结束 - ` - } - sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") - if len(strings.TrimSpace(sslConfigOld)) != 0 { - raw = strings.Replace(raw, sslConfigOld, "", -1) - } - raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1) - } else { - sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") - if len(strings.TrimSpace(sslConfigOld)) != 0 { - raw = strings.Replace(raw, sslConfigOld, "\n ", -1) - } - } - - if website.PHP != config.PHP { - website.PHP = config.PHP - phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束") - phpConfig := ` - include enable-php-` + strconv.Itoa(website.PHP) + `.conf; - ` - if len(strings.TrimSpace(phpConfigOld)) != 0 { - raw = strings.Replace(raw, phpConfigOld, phpConfig, -1) - } - } - - if err = facades.Orm().Query().Save(&website); err != nil { - return err - } - - if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil { - return err - } - if err = io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", config.Rewrite, 0644); err != nil { - return err - } - - err = systemctl.Reload("openresty") - if err != nil { - _, err = shell.Execf("openresty -t") - } - - return err -} - -// Delete 删除网站 -func (r *WebsiteImpl) Delete(request requests.Delete) error { - var website models.Website - if err := facades.Orm().Query().With("Cert").Where("id", request.ID).FirstOrFail(&website); err != nil { - return err - } - - if website.Cert != nil { - return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书") - } - - if _, err := facades.Orm().Query().Delete(&website); err != nil { - return err - } - - _ = io.Remove("/www/server/vhost/" + website.Name + ".conf") - _ = io.Remove("/www/server/vhost/rewrite/" + website.Name + ".conf") - _ = io.Remove("/www/server/vhost/acme/" + website.Name + ".conf") - _ = io.Remove("/www/server/vhost/ssl/" + website.Name + ".pem") - _ = io.Remove("/www/server/vhost/ssl/" + website.Name + ".key") - - if request.Path { - _ = io.Remove(website.Path) - } - if request.DB { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") - if err != nil { - return err - } - _ = mysql.DatabaseDrop(website.Name) - _ = mysql.UserDrop(website.Name) - _, _ = shell.Execf(`echo "DROP DATABASE IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) - _, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) - } - - err := systemctl.Reload("openresty") - if err != nil { - _, err = shell.Execf("openresty -t") - } - - return err -} - -// GetConfig 获取网站配置 -func (r *WebsiteImpl) GetConfig(id uint) (types.WebsiteSetting, error) { - var website models.Website - if err := facades.Orm().Query().Where("id", id).First(&website); err != nil { - return types.WebsiteSetting{}, err - } - - config, err := io.Read("/www/server/vhost/" + website.Name + ".conf") - if err != nil { - return types.WebsiteSetting{}, err - } - - var setting types.WebsiteSetting - setting.Name = website.Name - setting.Path = website.Path - setting.SSL = website.SSL - setting.PHP = strconv.Itoa(website.PHP) - setting.Raw = config - - portStr := str.Cut(config, "# port标记位开始", "# port标记位结束") - matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1) - for _, match := range matches { - if len(match) < 2 { - continue - } - // 跳过 ipv6 - if strings.Contains(match[1], "[::]") { - continue - } - - // 处理 443 ssl 之类的情况 - ports := strings.Fields(match[1]) - if len(ports) == 0 { - continue - } - if !slices.Contains(setting.Ports, ports[0]) { - setting.Ports = append(setting.Ports, ports[0]) - } - if len(ports) > 1 && ports[1] == "ssl" { - setting.SSLPorts = append(setting.SSLPorts, ports[0]) - } else if len(ports) > 1 && ports[1] == "quic" { - setting.QUICPorts = append(setting.QUICPorts, ports[0]) - } - } - serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束") - match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName) - if len(match) > 1 { - setting.Domains = strings.Split(match[1], " ") - } - root := str.Cut(config, "# root标记位开始", "# root标记位结束") - match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root) - if len(match) > 1 { - setting.Root = match[1] - } - index := str.Cut(config, "# index标记位开始", "# index标记位结束") - match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index) - if len(match) > 1 { - setting.Index = match[1] - } - - if io.Exists(filepath.Join(setting.Root, ".user.ini")) { - userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini")) - if strings.Contains(userIni, "open_basedir") { - setting.OpenBasedir = true - } - } - - crt, _ := io.Read("/www/server/vhost/ssl/" + website.Name + ".pem") - setting.SSLCertificate = crt - key, _ := io.Read("/www/server/vhost/ssl/" + website.Name + ".key") - setting.SSLCertificateKey = key - if setting.SSL { - ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束") - setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位") - setting.HSTS = strings.Contains(ssl, "# hsts标记位") - setting.OCSP = strings.Contains(ssl, "# ocsp标记位") - } - - // 解析证书信息 - if decode, err := cert.ParseCert(crt); err == nil { - setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05") - setting.SSLNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05") - setting.SSLIssuer = decode.Issuer.CommonName - setting.SSLOCSPServer = decode.OCSPServer - setting.SSLDNSNames = decode.DNSNames - } - - waf := str.Cut(config, "# waf标记位开始", "# waf标记位结束") - setting.Waf = strings.Contains(waf, "waf on;") - match = regexp.MustCompile(`waf_mode\s+([^;]*);?`).FindStringSubmatch(waf) - if len(match) > 1 { - setting.WafMode = match[1] - } - match = regexp.MustCompile(`waf_cc_deny\s+([^;]*);?`).FindStringSubmatch(waf) - if len(match) > 1 { - setting.WafCcDeny = match[1] - } - match = regexp.MustCompile(`waf_cache\s+([^;]*);?`).FindStringSubmatch(waf) - if len(match) > 1 { - setting.WafCache = match[1] - } - - rewrite, _ := io.Read("/www/server/vhost/rewrite/" + website.Name + ".conf") - setting.Rewrite = rewrite - log, _ := shell.Execf(`tail -n 100 '/www/wwwlogs/%s.log'`, website.Name) - setting.Log = log - - return setting, err -} - -// GetConfigByName 根据网站名称获取网站配置 -func (r *WebsiteImpl) GetConfigByName(name string) (types.WebsiteSetting, error) { - var website models.Website - if err := facades.Orm().Query().Where("name", name).First(&website); err != nil { - return types.WebsiteSetting{}, err - } - - return r.GetConfig(website.ID) -} - -// GetIDByName 根据网站名称获取网站ID -func (r *WebsiteImpl) GetIDByName(name string) (uint, error) { - var website models.Website - if err := facades.Orm().Query().Where("name", name).First(&website); err != nil { - return 0, err - } - - return website.ID, nil -} diff --git a/internal/setting.go b/internal/setting.go deleted file mode 100644 index 552063ed..00000000 --- a/internal/setting.go +++ /dev/null @@ -1,7 +0,0 @@ -package internal - -type Setting interface { - Get(key string, defaultValue ...string) string - Set(key, value string) error - Delete(key string) error -} diff --git a/internal/task.go b/internal/task.go deleted file mode 100644 index 8a2747d7..00000000 --- a/internal/task.go +++ /dev/null @@ -1,6 +0,0 @@ -package internal - -type Task interface { - Process(taskID uint) error - DispatchWaiting() error -} diff --git a/internal/user.go b/internal/user.go deleted file mode 100644 index f0c47257..00000000 --- a/internal/user.go +++ /dev/null @@ -1,8 +0,0 @@ -package internal - -import "github.com/TheTNB/panel/v2/app/models" - -type User interface { - Create(name, password string) (models.User, error) - Update(user models.User) (models.User, error) -} diff --git a/internal/website.go b/internal/website.go deleted file mode 100644 index c654262f..00000000 --- a/internal/website.go +++ /dev/null @@ -1,17 +0,0 @@ -package internal - -import ( - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Website interface { - List(page int, limit int) (int64, []models.Website, error) - Add(website requests.Add) (models.Website, error) - SaveConfig(config requests.SaveConfig) error - Delete(id requests.Delete) error - GetConfig(id uint) (types.WebsiteSetting, error) - GetConfigByName(name string) (types.WebsiteSetting, error) - GetIDByName(name string) (uint, error) -} diff --git a/lang/en.json b/lang/en.json deleted file mode 100644 index af8db26f..00000000 --- a/lang/en.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "auth": { - "session": { - "expired": "session has expired", - "missing": "Please enable cookies and try again", - "invalid": "invalid session" - } - }, - "commands": { - "panel:cert-renew": { - "description": "[Panel] Certificate renewal" - }, - "panel:monitoring": { - "description": "[Panel] System Monitoring", - "fail": "[Panel] System monitoring failed to save" - }, - "panel": { - "description": "[Panel] Command line", - "forDeveloper": "Please use the following commands under the guidance of the developer", - "use": "Please use the following commands", - "tool": "command line tool", - "portFail": "failed to get panel port", - "port": "Panel port", - "entrance": "Panel entrance", - "update": { - "description": "update/fix panel to latest version", - "taskCheck": "There is currently a task being executed and updates are prohibited.", - "dbFail": "panel database exception, operation terminated", - "versionFail": "failed to get latest version", - "fail": "update failed", - "success": "update completed" - }, - "getInfo": { - "description": "reinitialize panel account information", - "adminGetFail": "failed to get administrator information", - "adminSaveFail": "failed to save administrator information", - "passwordGenerationFail": "failed to generate password", - "username": "Username", - "password": "Password", - "address": "Panel address" - }, - "getPort": { - "description": "get the panel access port" - }, - "getEntrance": { - "description": "get panel access" - }, - "deleteEntrance": { - "description": "delete panel access", - "fail": "failed to delete panel entrance", - "success": "panel entrance deleted successfully" - }, - "cleanTask": { - "description": "clean up running and waiting tasks in the panel [used when tasks are stuck]", - "fail": "failed to clean up tasks", - "success": "tasks cleaned up successfully" - }, - "backup": { - "description": "back up website/MySQL database/PostgreSQL database to the specified directory and retain the specified amount", - "paramFail": "backup type, path, name and keep amount are required", - "start": "start backup", - "backupDirFail": "failed to create backup directory", - "targetSite": "target website", - "siteNotExist": "website does not exist", - "backupFail": "backup failed", - "backupSuccess": "backup successful", - "mysqlBackupFail": "MySQL database backup failed", - "targetMysql": "target MySQL database", - "startExport": "start exporting", - "exportFail": "export failed", - "exportSuccess": "export successful", - "startCompress": "start compressing", - "compressFail": "compression failed", - "compressSuccess": "compression successful", - "startMove": "start moving", - "moveFail": "move failed", - "moveSuccess": "move successful", - "databaseGetFail": "failed to get database", - "databaseNotExist": "database does not exist", - "targetPostgres": "target PostgreSQL database", - "cleanBackup": "clean backup", - "cleanupFail": "cleanup failed", - "cleanupSuccess": "cleanup successful", - "deleteFail": "failed to delete", - "success": "backup completed" - }, - "cutoff": { - "description": "cut website logs and keep specified amount", - "paramFail": "website domain name and keep amount are required", - "start": "start cutting", - "targetSite": "target website", - "siteNotExist": "website does not exist", - "logNotExist": "log file does not exist", - "backupFail": "backup failed", - "clearFail": "clearing failed", - "cleanupFail": "cleanup failed", - "clearLog": "clear log", - "cutSuccess": "cutting successful", - "cleanupSuccess": "cleanup successful", - "end": "cutting completed" - }, - "installPlugin": { - "description": "install plugin", - "paramFail": "plugin slug is required", - "success": "task has been submitted" - }, - "uninstallPlugin": { - "description": "uninstall plugin", - "paramFail": "plugin slug is required", - "success": "task has been submitted" - }, - "updatePlugin": { - "description": "update plugin", - "paramFail": "plugin slug is required", - "success": "task has been submitted" - }, - "addSite": { - "description": "add website [domain name and port separated by commas]", - "paramFail": "name, domain, port and path are required", - "siteExist": "website already exists", - "success": "website added successfully" - }, - "removeSite": { - "description": "remove website", - "paramFail": "name is required", - "siteNotExist": "website does not exist", - "success": "website deleted successfully" - }, - "init": { - "description": "initialize the panel", - "exist": "panel has been initialized", - "success": "initialization successful", - "adminFound": "failed to create administrator", - "fail": "initialization failed" - }, - "writePlugin": { - "description": "write plugin installation status", - "paramFail": "plugin slug and version are required", - "fail": "failed to write plugin installation status", - "success": "plugin installation status written successfully" - }, - "deletePlugin": { - "description": "delete plugin installation status", - "paramFail": "plugin slug is required", - "fail": "failed to remove plugin installation status", - "success": "plugin installation status removed successfully" - }, - "writeMysqlPassword": { - "description": "write MySQL root password", - "paramFail": "MySQL root password is required", - "fail": "failed to write MySQL root password", - "success": "MySQL root password written successfully" - }, - "writeSite": { - "description": "write website data to the panel", - "paramFail": "name and path are required", - "siteExist": "website already exists", - "pathNotExist": "website directory does not exist", - "fail": "failed to write to website", - "success": "writing to website successfully" - }, - "deleteSite": { - "description": "delete panel website data", - "paramFail": "website name is required", - "fail": "failed to delete website", - "success": "website deleted successfully" - }, - "getSetting": { - "description": "get panel setting data", - "paramFail": "key is required" - }, - "writeSetting": { - "description": "write/update panel setting data", - "paramFail": "key and value are required", - "fail": "Writing settings failed", - "success": "settings written successfully" - }, - "deleteSetting": { - "description": "delete panel setting data", - "paramFail": "key is required", - "fail": "failed to delete settings", - "success": "settings deleted successfully" - } - }, - "panel:task": { - "description": "[Panel] Daily tasks" - } - }, - "errors": { - "internal": "internal system error", - "plugin": { - "notExist": "plugin does not exist", - "notInstalled": "plugin :slug is not installed", - "dependent": "plugin :slug requires dependency :dependency", - "incompatible": "plugin :slug is incompatible with :exclude plugin" - } - }, - "status": { - "upgrade": "Panel is currently undergoing an upgrade. Please try again later.", - "maintain": "Panel is currently undergoing maintenance. Please try again later.", - "closed": "Panel is closed.", - "failed": "Panel encountered an error during operation. Please check the troubleshooting or contact support." - } -} \ No newline at end of file diff --git a/lang/zh_CN.json b/lang/zh_CN.json deleted file mode 100644 index 3cbffd00..00000000 --- a/lang/zh_CN.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "auth": { - "session": { - "expired": "会话已过期", - "missing": "请启用 Cookie 后再试", - "invalid": "会话无效" - } - }, - "commands": { - "panel:cert-renew": { - "description": "[面板] 证书续签" - }, - "panel:monitoring": { - "description": "[面板] 系统监控", - "fail": "[面板] 系统监控保存失败" - }, - "panel": { - "description": "[面板] 命令行", - "forDeveloper": "以下命令请在开发者指导下使用", - "use": "请使用以下命令", - "tool": "命令行工具", - "port": "面板端口", - "portFail": "获取面板端口失败", - "entrance": "面板入口", - "update": { - "description": "更新 / 修复面板到最新版本", - "taskCheck": "当前有任务正在执行,禁止更新", - "dbFail": "面板数据库异常,已终止操作", - "versionFail": "获取最新版本失败", - "fail": "更新失败", - "success": "更新成功" - }, - "getInfo": { - "description": "重新初始化面板账号信息", - "adminGetFail": "获取管理员信息失败", - "adminSaveFail": "保存管理员信息失败", - "passwordGenerationFail": "生成密码失败", - "username": "用户名", - "password": "密码", - "address": "面板地址" - }, - "getPort": { - "description": "获取面板访问端口" - }, - "getEntrance": { - "description": "获取面板访问入口" - }, - "deleteEntrance": { - "description": "删除面板访问入口", - "fail": "删除面板入口失败", - "success": "删除面板入口成功" - }, - "cleanTask": { - "description": "清理面板运行中和等待中的任务[任务卡住时使用]", - "fail": "清理任务失败", - "success": "清理任务成功" - }, - "backup": { - "description": "备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量", - "paramFail": "参数错误", - "start": "开始备份", - "backupDirFail": "创建备份目录失败", - "targetSite": "目标网站", - "siteNotExist": "网站不存在", - "backupFail": "备份失败", - "backupSuccess": "备份成功", - "mysqlBackupFail": "备份MySQL数据库失败", - "targetMysql": "目标MySQL数据库", - "startExport": "开始导出", - "exportFail": "导出失败", - "exportSuccess": "导出成功", - "startCompress": "开始压缩", - "compressFail": "压缩失败", - "compressSuccess": "压缩成功", - "startMove": "开始移动", - "moveFail": "移动失败", - "moveSuccess": "移动成功", - "databaseGetFail": "获取数据库失败", - "databaseNotExist": "数据库不存在", - "targetPostgres": "目标PostgreSQL数据库", - "cleanBackup": "清理备份", - "cleanupFail": "清理失败", - "cleanupSuccess": "清理完成", - "deleteFail": "删除失败", - "success": "备份完成" - }, - "cutoff": { - "description": "切割网站日志并保留指定数量", - "paramFail": "参数错误", - "start": "开始切割", - "targetSite": "目标网站", - "siteNotExist": "网站不存在", - "logNotExist": "日志文件不存在", - "backupFail": "备份失败", - "clearFail": "清空失败", - "cleanupFail": "清理失败", - "clearLog": "清理日志", - "cutSuccess": "切割成功", - "cleanupSuccess": "清理完成", - "end": "切割完成" - }, - "installPlugin": { - "description": "安装插件", - "paramFail": "参数错误", - "success": "任务已提交" - }, - "uninstallPlugin": { - "description": "卸载插件", - "paramFail": "参数错误", - "success": "任务已提交" - }, - "updatePlugin": { - "description": "更新插件", - "paramFail": "参数错误", - "success": "任务已提交" - }, - "addSite": { - "description": "添加网站[域名和端口用英文逗号分隔]", - "paramFail": "参数错误", - "siteExist": "网站名已存在", - "success": "网站添加成功" - }, - "removeSite": { - "description": "删除网站", - "paramFail": "参数错误", - "siteNotExist": "网站名不存在", - "success": "网站删除成功" - }, - "init": { - "description": "初始化面板", - "exist": "面板已初始化", - "success": "初始化成功", - "adminFail": "创建管理员失败", - "fail": "初始化失败" - }, - "writePlugin": { - "description": "写入插件安装状态", - "paramFail": "参数错误", - "fail": "写入插件安装状态失败", - "success": "写入插件安装状态成功" - }, - "deletePlugin": { - "description": "移除插件安装状态", - "paramFail": "参数错误", - "fail": "移除插件安装状态失败", - "success": "移除插件安装状态成功" - }, - "writeMysqlPassword": { - "description": "写入MySQL root密码", - "paramFail": "参数错误", - "fail": "写入MySQL root密码失败", - "success": "写入MySQL root密码成功" - }, - "writeSite": { - "description": "写入网站数据到面板", - "paramFail": "参数错误", - "siteExist": "网站已存在", - "pathNotExist": "网站目录不存在", - "fail": "写入网站失败", - "success": "写入网站成功" - }, - "deleteSite": { - "description": "删除面板网站数据", - "paramFail": "参数错误", - "fail": "删除网站失败", - "success": "删除网站成功" - }, - "getSetting": { - "description": "获取面板设置数据", - "paramFail": "参数错误" - }, - "writeSetting": { - "description": "写入 / 更新面板设置数据", - "paramFail": "参数错误", - "fail": "写入设置失败", - "success": "写入设置成功" - }, - "deleteSetting": { - "description": "删除面板设置数据", - "paramFail": "参数错误", - "fail": "删除设置失败", - "success": "删除设置成功" - } - }, - "panel:task": { - "description": "[面板] 每日任务" - } - }, - "errors": { - "internal": "系统内部错误", - "plugin": { - "notExist": "插件不存在", - "notInstalled": "插件 :slug 未安装", - "dependent": "插件 :slug 需要依赖 :dependency 插件", - "incompatible": "插件 :slug 不兼容 :exclude 插件" - } - }, - "status": { - "upgrade": "面板升级中,请稍后", - "maintain": "面板正在运行维护,请稍后", - "closed": "面板已关闭", - "failed": "面板运行出错,请检查排除或联系支持" - } -} \ No newline at end of file diff --git a/main.go b/main.go deleted file mode 100644 index 4bf4c5a4..00000000 --- a/main.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ -package main - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/bootstrap" -) - -// @title 耗子面板 API -// @version 2 -// @description 耗子面板的 API 信息 - -// @contact.name 耗子科技 -// @contact.email admin@haozi.net - -// @license.name GNU Affero General Public License v3 -// @license url https://www.gnu.org/licenses/agpl-3.0.html - -// @securityDefinitions.apikey BearerToken -// @in header -// @name Authorization - -// @BasePath /api -func main() { - // 启动框架 - bootstrap.Boot() - - // 启动 HTTP 服务 - if facades.Config().GetBool("panel.ssl") { - go func() { - if err := facades.Route().RunTLS(); err != nil { - facades.Log().Infof("Route run error: %v", err) - } - }() - } else { - go func() { - if err := facades.Route().Run(); err != nil { - facades.Log().Infof("Route run error: %v", err) - } - }() - } - - // 启动计划任务 - go facades.Schedule().Run() - - // 启动队列 - go func() { - if err := facades.Queue().Worker(nil).Run(); err != nil { - facades.Log().Errorf("Queue run error: %v", err) - } - }() - - select {} -} diff --git a/panel-example.conf b/panel-example.conf deleted file mode 100644 index 3e440557..00000000 --- a/panel-example.conf +++ /dev/null @@ -1,10 +0,0 @@ -APP_ENV=local -APP_KEY= -APP_DEBUG=false -APP_PORT=8888 -APP_ENTRANCE=/ -APP_SSL=false -APP_LOCALE=zh_CN - -JWT_SECRET= -SESSION_LIFETIME=120 diff --git a/pkg/acme/acme.go b/pkg/acme/acme.go index 368d40c6..46be6d2c 100644 --- a/pkg/acme/acme.go +++ b/pkg/acme/acme.go @@ -14,7 +14,7 @@ import ( "github.com/mholt/acmez/v2/acme" "go.uber.org/zap" - "github.com/TheTNB/panel/v2/pkg/cert" + "github.com/TheTNB/panel/pkg/cert" ) const ( diff --git a/pkg/acme/client.go b/pkg/acme/client.go index 7826a343..d71f8046 100644 --- a/pkg/acme/client.go +++ b/pkg/acme/client.go @@ -8,7 +8,7 @@ import ( "github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2/acme" - "github.com/TheTNB/panel/v2/pkg/cert" + "github.com/TheTNB/panel/pkg/cert" ) type Certificate struct { diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index a70fe44f..8c962a8c 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -15,8 +15,8 @@ import ( "github.com/mholt/acmez/v2/acme" "golang.org/x/net/publicsuffix" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" ) type httpSolver struct { diff --git a/pkg/arch/arch.go b/pkg/arch/arch.go new file mode 100644 index 00000000..92b98f25 --- /dev/null +++ b/pkg/arch/arch.go @@ -0,0 +1,21 @@ +package arch + +import "runtime" + +// IsArm returns whether the current CPU architecture is ARM. +// IsArm 返回当前 CPU 架构是否为 ARM。 +func IsArm() bool { + return runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" +} + +// IsX86 returns whether the current CPU architecture is X86. +// IsX86 返回当前 CPU 架构是否为 X86。 +func IsX86() bool { + return runtime.GOARCH == "386" || runtime.GOARCH == "amd64" +} + +// Is64Bit returns whether the current CPU architecture is 64-bit. +// Is64Bit 返回当前 CPU 架构是否为 64 位。 +func Is64Bit() bool { + return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" +} diff --git a/pkg/db/mysql.go b/pkg/db/mysql.go index db03f200..62f56b31 100644 --- a/pkg/db/mysql.go +++ b/pkg/db/mysql.go @@ -6,7 +6,7 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/TheTNB/panel/v2/pkg/types" + "github.com/TheTNB/panel/pkg/types" ) type MySQL struct { diff --git a/pkg/db/mysql_tools.go b/pkg/db/mysql_tools.go index 21231c8b..cb8201cb 100644 --- a/pkg/db/mysql_tools.go +++ b/pkg/db/mysql_tools.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" ) // MySQLResetRootPassword 重置 MySQL root密码 diff --git a/pkg/db/postgres.go b/pkg/db/postgres.go index 30953915..bf2a4915 100644 --- a/pkg/db/postgres.go +++ b/pkg/db/postgres.go @@ -6,10 +6,10 @@ import ( _ "github.com/lib/pq" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" ) type Postgres struct { @@ -90,7 +90,7 @@ func (m *Postgres) UserDrop(user string) error { return err } - _, _ = shell.Execf(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`) + _, _ = shell.Execf(`sed -i '/%s/d' /www/server/postgresql/data/pg_hba.conf`, user) return systemctl.Reload("postgresql") } @@ -126,7 +126,7 @@ func (m *Postgres) HostAdd(database, user, host string) error { func (m *Postgres) HostRemove(database, user, host string) error { regex := fmt.Sprintf(`host\s+%s\s+%s\s+%s`, database, user, host) - if _, err := shell.Execf(`sed -i '/` + regex + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil { + if _, err := shell.Execf(`sed -i '/%s/d' /www/server/postgresql/data/pg_hba.conf`, regex); err != nil { return err } diff --git a/pkg/firewall/consts.go b/pkg/firewall/consts.go new file mode 100644 index 00000000..87c259c4 --- /dev/null +++ b/pkg/firewall/consts.go @@ -0,0 +1,24 @@ +package firewall + +type FireInfo struct { + Family string `json:"family"` // ipv4 ipv6 + Address string `json:"address"` + Port uint `json:"port"` // 1-65535 + Protocol string `json:"protocol"` // tcp udp tcp/udp + Strategy string `json:"strategy"` // accept drop + + Num string `json:"num"` + TargetIP string `json:"targetIP"` + TargetPort string `json:"targetPort"` // 1-65535 + + UsedStatus string `json:"usedStatus"` + Description string `json:"description"` +} + +type Forward struct { + Num string `json:"num"` + Protocol string `json:"protocol"` + Port uint `json:"port"` // 1-65535 + TargetIP string `json:"targetIP"` + TargetPort uint `json:"targetPort"` // 1-65535 +} diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go new file mode 100644 index 00000000..9fb05caa --- /dev/null +++ b/pkg/firewall/firewall.go @@ -0,0 +1,218 @@ +package firewall + +import ( + "errors" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type Firewall struct { + forwardListRegex *regexp.Regexp + richRuleRegex *regexp.Regexp +} + +func NewFirewall() *Firewall { + firewall := &Firewall{ + forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`), + richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)" (?:source address="([^"]+)" )?(?:port port="([^"]+)" )?(?:protocol="([^"]+)" )?(accept|drop|reject)$`), + } + + return firewall +} + +func (r *Firewall) Status() (bool, error) { + return systemctl.Status("firewalld") +} + +func (r *Firewall) Version() (string, error) { + return shell.Execf("firewall-cmd --version") +} + +func (r *Firewall) ListRule() ([]FireInfo, error) { + var wg sync.WaitGroup + var data []FireInfo + wg.Add(2) + + go func() { + defer wg.Done() + out, err := shell.Execf("firewall-cmd --zone=public --list-ports") + if err != nil { + return + } + ports := strings.Split(out, " ") + for _, port := range ports { + if len(port) == 0 { + continue + } + var itemPort FireInfo + if strings.Contains(port, "/") { + itemPort.Port = cast.ToUint(strings.Split(port, "/")[0]) + itemPort.Protocol = strings.Split(port, "/")[1] + } + itemPort.Strategy = "accept" + data = append(data, itemPort) + } + }() + go func() { + defer wg.Done() + rich, err := r.ListRichRule() + if err != nil { + return + } + + data = append(data, rich...) + }() + + wg.Wait() + return data, nil +} + +func (r *Firewall) ListForward() ([]FireInfo, error) { + out, err := shell.Execf("firewall-cmd --zone=public --list-forward-ports") + if err != nil { + return nil, err + } + + var data []FireInfo + for _, line := range strings.Split(out, "\n") { + line = strings.TrimFunc(line, func(r rune) bool { + return r <= 32 + }) + if r.forwardListRegex.MatchString(line) { + match := r.forwardListRegex.FindStringSubmatch(line) + if len(match) < 4 { + continue + } + if len(match[4]) == 0 { + match[4] = "127.0.0.1" + } + data = append(data, FireInfo{ + Port: cast.ToUint(match[1]), + Protocol: match[2], + TargetIP: match[4], + TargetPort: match[3], + }) + } + } + + return data, nil +} + +func (r *Firewall) ListRichRule() ([]FireInfo, error) { + out, err := shell.Execf("firewall-cmd --zone=public --list-rich-rules") + if err != nil { + return nil, err + } + + var data []FireInfo + rules := strings.Split(out, "\n") + for _, rule := range rules { + if len(rule) == 0 { + continue + } + if itemRule, err := r.parseRichRule(rule); err == nil { + data = append(data, *itemRule) + } + } + + return data, nil +} + +func (r *Firewall) Port(port FireInfo, operation string) error { + stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d/%s --permanent", operation, port.Port, port.Protocol) + if err != nil { + return fmt.Errorf("%s port %d/%s failed, err: %s", operation, port.Port, port.Protocol, stdout) + } + return systemctl.Reload("firewalld") +} + +func (r *Firewall) RichRules(rule FireInfo, operation string) error { + families := strings.Split(rule.Family, "/") // ipv4 ipv6 + + for _, family := range families { + var ruleStr strings.Builder + ruleStr.WriteString(fmt.Sprintf(`rule family="%s" `, family)) + if len(rule.Address) != 0 { + ruleStr.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address)) + } + if rule.Port != 0 { + ruleStr.WriteString(fmt.Sprintf(`port port="%d" `, rule.Port)) + } + if len(rule.Protocol) != 0 { + ruleStr.WriteString(fmt.Sprintf(`protocol="%s" `, rule.Protocol)) + } + + ruleStr.WriteString(rule.Strategy) + out, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr.String()) + if err != nil { + return fmt.Errorf("%s rich rules (%s) failed, err: %s", operation, ruleStr.String(), out) + } + } + + return systemctl.Reload("firewalld") +} + +func (r *Firewall) PortForward(info Forward, operation string) error { + if err := r.enableForward(); err != nil { + return err + } + + var ruleStr strings.Builder + ruleStr.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, info.Port, info.Protocol)) + if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" { + ruleStr.WriteString(fmt.Sprintf("toaddr=%s:toport=%d", info.TargetIP, info.TargetPort)) + } else { + ruleStr.WriteString(fmt.Sprintf("toport=%d", info.TargetPort)) + } + ruleStr.WriteString(" --permanent") + + out, err := shell.Execf(ruleStr.String()) // nolint: govet + if err != nil { + return fmt.Errorf("%s port forward failed, err: %s", operation, out) + } + + return systemctl.Reload("firewalld") +} + +func (r *Firewall) parseRichRule(line string) (*FireInfo, error) { + itemRule := new(FireInfo) + if r.richRuleRegex.MatchString(line) { + match := r.richRuleRegex.FindStringSubmatch(line) + if len(match) < 6 { + return nil, errors.New("invalid rich rule") + } + + itemRule.Family = match[1] + itemRule.Address = match[2] + itemRule.Port = cast.ToUint(match[3]) + itemRule.Protocol = match[4] + itemRule.Strategy = match[5] + } + + return itemRule, nil +} + +func (r *Firewall) enableForward() error { + out, err := shell.Execf("firewall-cmd --zone=public --query-masquerade") + if err != nil { + if out == "no" { + out, err = shell.Execf("firewall-cmd --zone=public --add-masquerade --permanent") + if err != nil { + return fmt.Errorf("%s: %s", err, out) + } + + return systemctl.Reload("firewalld") + } + + return fmt.Errorf("%s: %s", err, out) + } + + return nil +} diff --git a/pkg/h/request.go b/pkg/h/request.go deleted file mode 100644 index b321d6bc..00000000 --- a/pkg/h/request.go +++ /dev/null @@ -1,29 +0,0 @@ -package h - -import "github.com/goravel/framework/contracts/http" - -// SanitizeRequest 消毒请求参数 -func SanitizeRequest(ctx http.Context, request http.FormRequest) http.Response { - errors, err := ctx.Request().ValidateRequest(request) - if err != nil { - return Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if errors != nil { - return Error(ctx, http.StatusUnprocessableEntity, errors.One()) - } - - return nil -} - -// Sanitize 消毒参数 -func Sanitize(ctx http.Context, rules map[string]string) http.Response { - validator, err := ctx.Request().Validate(rules) - if err != nil { - return Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if validator.Fails() { - return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One()) - } - - return nil -} diff --git a/pkg/h/response.go b/pkg/h/response.go deleted file mode 100644 index 5a2c8b90..00000000 --- a/pkg/h/response.go +++ /dev/null @@ -1,68 +0,0 @@ -package h - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/http/requests/common" -) - -// SuccessResponse 通用成功响应 -type SuccessResponse struct { - Message string `json:"message"` - Data any `json:"data"` -} - -// ErrorResponse 通用错误响应 -type ErrorResponse struct { - Message string `json:"message"` -} - -// Success 响应成功 -func Success(ctx http.Context, data any) http.Response { - return ctx.Response().Success().Json(&SuccessResponse{ - Message: "success", - Data: data, - }) -} - -// Error 响应错误 -func Error(ctx http.Context, code int, message string) http.Response { - return ctx.Response().Json(code, &ErrorResponse{ - Message: message, - }) -} - -// ErrorSystem 响应系统错误 -func ErrorSystem(ctx http.Context) http.Response { - return ctx.Response().Json(http.StatusInternalServerError, &ErrorResponse{ - Message: facades.Lang(ctx).Get("errors.internal"), - }) -} - -// Paginate 取分页条目 -func Paginate[T any](ctx http.Context, allItems []T) (pagedItems []T, total int) { - var paginateRequest commonrequests.Paginate - sanitize := SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return []T{}, 0 - } - - page := ctx.Request().QueryInt("page", 1) - limit := ctx.Request().QueryInt("limit", 10) - total = len(allItems) - startIndex := (page - 1) * limit - endIndex := page * limit - - if total == 0 { - return []T{}, 0 - } - if startIndex > total { - return []T{}, total - } - if endIndex > total { - endIndex = total - } - - return allItems[startIndex:endIndex], total -} diff --git a/pkg/io/file.go b/pkg/io/file.go index 88495db5..4cde111a 100644 --- a/pkg/io/file.go +++ b/pkg/io/file.go @@ -1,11 +1,33 @@ package io import ( + "archive/zip" + "context" + "errors" + stdio "io" "os" + "path" "path/filepath" "strings" - "github.com/mholt/archiver/v3" + "github.com/mholt/archiver/v4" +) + +type FormatArchive string + +const ( + Zip FormatArchive = "zip" + Gz FormatArchive = "gz" + Bz2 FormatArchive = "bz2" + Tar FormatArchive = "tar" + TarGz FormatArchive = "tar.gz" + Xz FormatArchive = "xz" + SevenZip FormatArchive = "7z" +) + +var ( + ErrFormatNotSupport = errors.New("不支持此格式") + ErrNotDirectory = errors.New("目标不是目录") ) // Write 写入文件 @@ -49,14 +71,102 @@ func FileInfo(path string) (os.FileInfo, error) { return os.Stat(path) } -// UnArchive 智能解压文件 -func UnArchive(file string, dst string) error { - return archiver.Unarchive(file, dst) +// Compress 压缩文件 +func Compress(src []string, dst string, format FormatArchive) error { + // 不支持7z + if format == SevenZip { + return ErrFormatNotSupport + } + arch := getFormat(format) + + srcMap := make(map[string]string, len(src)) + for _, s := range src { + base := filepath.Base(s) + srcMap[s] = base + } + + dir := filepath.Dir(dst) + if !Exists(dir) { + if err := Mkdir(dir, 0755); err != nil { + return err + } + } + + files, err := archiver.FilesFromDisk(nil, srcMap) + if err != nil { + return err + } + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer out.Close() + + err = arch.Archive(context.Background(), out, files) + if err != nil { + _ = Remove(dst) + } + + return nil } -// Archive 智能压缩文件 -func Archive(src []string, dst string) error { - return archiver.Archive(src, dst) +// UnCompress 解压文件 +func UnCompress(src string, dst string, format FormatArchive) error { + handler := func(ctx context.Context, f archiver.File) error { + info := f.FileInfo + fileName := f.NameInArchive + filePath := filepath.Join(dst, fileName) + + if f.FileInfo.IsDir() { + if err := Mkdir(filePath, info.Mode()); err != nil { + return err + } + return nil + } + + parentDir := path.Dir(filePath) + if !Exists(parentDir) { + if err := Mkdir(parentDir, info.Mode()); err != nil { + return err + } + } + + r, err := f.Open() + if err != nil { + return err + } + w, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer r.Close() + defer w.Close() + + if _, err = stdio.Copy(w, r); err != nil { + return err + } + + return nil + } + + arch := getFormat(format) + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + if !Exists(dst) { + if err = Mkdir(dst, 0755); err != nil { + return err + } + } + if !IsDir(dst) { + return ErrNotDirectory + } + + return arch.Extract(context.Background(), file, nil, handler) } // TempFile 创建临时文件 @@ -83,3 +193,28 @@ func GetSymlink(path string) string { } return linkPath } + +func getFormat(f FormatArchive) archiver.CompressedArchive { + format := archiver.CompressedArchive{} + switch f { + case Tar: + format.Archival = archiver.Tar{} + case TarGz, Gz: + format.Compression = archiver.Gz{} + format.Archival = archiver.Tar{} + case Zip: + format.Archival = archiver.Zip{ + Compression: zip.Deflate, + } + case Bz2: + format.Compression = archiver.Bz2{} + format.Archival = archiver.Tar{} + case Xz: + format.Compression = archiver.Xz{} + format.Archival = archiver.Tar{} + case SevenZip: + format.Archival = archiver.SevenZip{} + + } + return format +} diff --git a/pkg/io/io_test.go b/pkg/io/io_test.go new file mode 100644 index 00000000..ec885b81 --- /dev/null +++ b/pkg/io/io_test.go @@ -0,0 +1,208 @@ +package io + +import ( + "os" + "path/filepath" + "testing" + + "github.com/go-rat/utils/env" + "github.com/stretchr/testify/suite" +) + +type IOTestSuite struct { + suite.Suite +} + +func TestIOTestSuite(t *testing.T) { + suite.Run(t, &IOTestSuite{}) +} + +func (s *IOTestSuite) SetupTest() { + if _, err := os.Stat("testdata"); os.IsNotExist(err) { + s.NoError(os.MkdirAll("testdata", 0755)) + } +} + +func (s *IOTestSuite) TearDownTest() { + s.NoError(os.RemoveAll("testdata")) +} + +func (s *IOTestSuite) TestWriteCreatesFileWithCorrectContent() { + path := "testdata/write_test.txt" + data := "Hello, World!" + permission := os.FileMode(0644) + + s.NoError(Write(path, data, permission)) + + content, err := Read(path) + s.NoError(err) + s.Equal(data, content) +} + +func (s *IOTestSuite) TestWriteAppendAppendsToFile() { + path := "testdata/append_test.txt" + initialData := "Hello" + appendData := ", World!" + + s.NoError(Write(path, initialData, 0644)) + s.NoError(WriteAppend(path, appendData)) + + content, err := Read(path) + s.NoError(err) + s.Equal("Hello, World!", content) +} + +func (s *IOTestSuite) TestCompress() { + src := []string{"testdata/compress_test1.txt", "testdata/compress_test2.txt"} + err := Write(src[0], "File 1", 0644) + s.NoError(err) + err = Write(src[1], "File 2", 0644) + s.NoError(err) + + err = Compress(src, "testdata/compress_test.zip", Zip) + s.NoError(err) +} + +func (s *IOTestSuite) TestUnCompress() { + src := []string{"testdata/uncompress_test1.txt", "testdata/uncompress_test2.txt"} + err := Write(src[0], "File 1", 0644) + s.NoError(err) + err = Write(src[1], "File 2", 0644) + s.NoError(err) + + err = Compress(src, "testdata/uncompress_test.zip", Zip) + s.NoError(err) + + err = UnCompress("testdata/uncompress_test.zip", "testdata/uncompressed", Zip) + s.NoError(err) + + data, err := Read("testdata/uncompressed/uncompress_test1.txt") + s.NoError(err) + s.Equal("File 1", data) + + data, err = Read("testdata/uncompressed/uncompress_test2.txt") + s.NoError(err) + s.Equal("File 2", data) +} + +func (s *IOTestSuite) TestRemoveDeletesFileOrDirectory() { + path := "testdata/remove_test" + s.NoError(Mkdir(path, 0755)) + s.DirExists(path) + + s.NoError(Remove(path)) + s.NoDirExists(path) +} + +func (s *IOTestSuite) TestMkdirCreatesDirectory() { + path := "testdata/mkdir_test" + s.NoError(Mkdir(path, 0755)) + s.DirExists(path) +} + +func (s *IOTestSuite) TestChmodChangesPermissions() { + if env.IsWindows() { + s.T().Skip("Skipping on Windows") + } + path := "testdata/chmod_test.txt" + s.NoError(Write(path, "test", 0644)) + + s.NoError(Chmod(path, 0755)) + info, err := os.Stat(path) + s.NoError(err) + s.Equal(os.FileMode(0755), info.Mode().Perm()) +} + +func (s *IOTestSuite) TestChownChangesOwner() { + if env.IsWindows() { + s.T().Skip("Skipping on Windows") + } + path := "testdata/chown_test.txt" + s.NoError(Write(path, "test", 0644)) + + s.NoError(Chown(path, "root", "root")) +} + +func (s *IOTestSuite) TestExistsReturnsTrueForExistingPath() { + path := "testdata/exists_test.txt" + s.NoError(Write(path, "test", 0644)) + s.True(Exists(path)) +} + +func (s *IOTestSuite) TestExistsReturnsFalseForNonExistingPath() { + path := "testdata/nonexistent.txt" + s.False(Exists(path)) +} + +func (s *IOTestSuite) TestEmptyReturnsTrueForEmptyDirectory() { + path := "testdata/empty_test" + s.NoError(Mkdir(path, 0755)) + s.True(Empty(path)) +} + +func (s *IOTestSuite) TestEmptyReturnsFalseForNonEmptyDirectory() { + path := "testdata/nonempty_test" + s.NoError(Mkdir(path, 0755)) + s.NoError(Write(filepath.Join(path, "file.txt"), "test", 0644)) + s.False(Empty(path)) +} + +func (s *IOTestSuite) TestMvMovesFile() { + src := "testdata/mv_src.txt" + dst := "testdata/mv_dst.txt" + s.NoError(Write(src, "test", 0644)) + + s.NoError(Mv(src, dst)) + s.FileExists(dst) + s.NoFileExists(src) +} + +func (s *IOTestSuite) TestCpCopiesFile() { + src := "testdata/cp_src.txt" + dst := "testdata/cp_dst.txt" + s.NoError(Write(src, "test", 0644)) + + s.NoError(Cp(src, dst)) + s.FileExists(dst) + s.FileExists(src) +} + +func (s *IOTestSuite) TestSizeReturnsCorrectSize() { + path := "testdata/size_test.txt" + data := "12345" + s.NoError(Write(path, data, 0644)) + + size, err := Size(path) + s.NoError(err) + s.Equal(int64(len(data)), size) +} + +func (s *IOTestSuite) TestTempDirCreatesTemporaryDirectory() { + dir, err := TempDir("tempdir_test") + s.NoError(err) + s.DirExists(dir) + s.NoError(Remove(dir)) +} + +func (s *IOTestSuite) TestReadDirReturnsDirectoryEntries() { + path := "testdata/readdir_test" + s.NoError(Mkdir(path, 0755)) + s.NoError(Write(filepath.Join(path, "file1.txt"), "test", 0644)) + s.NoError(Write(filepath.Join(path, "file2.txt"), "test", 0644)) + + entries, err := ReadDir(path) + s.NoError(err) + s.Len(entries, 2) +} + +func (s *IOTestSuite) TestIsDirReturnsTrueForDirectory() { + path := "testdata/isdir_test" + s.NoError(Mkdir(path, 0755)) + s.True(IsDir(path)) +} + +func (s *IOTestSuite) TestIsDirReturnsFalseForFile() { + path := "testdata/isfile_test.txt" + s.NoError(Write(path, "test", 0644)) + s.False(IsDir(path)) +} diff --git a/pkg/io/path.go b/pkg/io/path.go index e20eab8a..9dbe6491 100644 --- a/pkg/io/path.go +++ b/pkg/io/path.go @@ -20,13 +20,13 @@ func Mkdir(path string, permission os.FileMode) error { // Chmod 修改文件/目录权限 func Chmod(path string, permission os.FileMode) error { - cmd := exec.Command("chmod", "-R", fmt.Sprintf("%o", permission), path) + cmd := exec.Command("sudo", "chmod", "-R", fmt.Sprintf("%o", permission), path) return cmd.Run() } // Chown 修改文件或目录所有者 func Chown(path, user, group string) error { - cmd := exec.Command("chown", "-R", user+":"+group, path) + cmd := exec.Command("sudo", "chown", "-R", user+":"+group, path) return cmd.Run() } @@ -149,3 +149,12 @@ func TempDir(prefix string) (string, error) { func ReadDir(path string) ([]os.DirEntry, error) { return os.ReadDir(path) } + +// IsDir 判断是否为目录 +func IsDir(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return info.IsDir() +} diff --git a/pkg/migrate/migrate.go b/pkg/migrate/migrate.go deleted file mode 100644 index 0f186055..00000000 --- a/pkg/migrate/migrate.go +++ /dev/null @@ -1,22 +0,0 @@ -package migrate - -import ( - "fmt" - - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" -) - -func Migrate(db *gorm.DB) { - options := &gormigrate.Options{ - TableName: "new_migrations", - IDColumnName: "id", - IDColumnSize: 255, - } - migrator := gormigrate.New(db, options, []*gormigrate.Migration{ - Init, - }) - if err := migrator.Migrate(); err != nil { - panic(fmt.Sprintf("Failed to migrate database: %v", err)) - } -} diff --git a/pkg/migrate/migrations.go b/pkg/migrate/migrations.go deleted file mode 100644 index ae988824..00000000 --- a/pkg/migrate/migrations.go +++ /dev/null @@ -1,42 +0,0 @@ -package migrate - -import ( - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" - - "github.com/TheTNB/panel/v2/app/models" -) - -var Init = &gormigrate.Migration{ - ID: "20240624-init", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - &models.Cert{}, - &models.CertDNS{}, - &models.CertUser{}, - &models.Cron{}, - &models.Database{}, - &models.Monitor{}, - &models.Plugin{}, - &models.Setting{}, - &models.Task{}, - &models.User{}, - &models.Website{}, - ) - }, - Rollback: func(tx *gorm.DB) error { - return tx.Migrator().DropTable( - &models.Cert{}, - &models.CertDNS{}, - &models.CertUser{}, - &models.Cron{}, - &models.Database{}, - &models.Monitor{}, - &models.Plugin{}, - &models.Setting{}, - &models.Task{}, - &models.User{}, - &models.Website{}, - ) - }, -} diff --git a/pkg/ntp/ntp.go b/pkg/ntp/ntp.go new file mode 100644 index 00000000..21b34601 --- /dev/null +++ b/pkg/ntp/ntp.go @@ -0,0 +1,119 @@ +package ntp + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/beevik/ntp" + + "github.com/TheTNB/panel/pkg/shell" +) + +var ErrNotReachable = errors.New("无法连接到 NTP 服务器") + +var ErrNoAvailableServer = errors.New("无可用的 NTP 服务器") + +var defaultAddresses = []string{ + //"ntp.ntsc.ac.cn", // 中科院国家授时中心的服务器很快,但是多刷几次就会被封 + "ntp.aliyun.com", // 阿里云 + "ntp1.aliyun.com", // 阿里云2 + "ntp.tencent.com", // 腾讯云 + "time.windows.com", // Windows + "time.apple.com", // Apple +} + +func Now(address ...string) (time.Time, error) { + if len(address) > 0 { + if now, err := ntp.Time(address[0]); err != nil { + return time.Now(), fmt.Errorf("%w: %s", ErrNotReachable, err) + } else { + return now, nil + } + } + + best, err := bestServer(defaultAddresses...) + if err != nil { + return time.Now(), err + } + + now, err := ntp.Time(best) + if err != nil { + return time.Now(), fmt.Errorf("%w: %s", ErrNotReachable, err) + } + + return now, nil +} + +func UpdateSystemTime(time time.Time) error { + _, err := shell.Execf(`sudo date -s "%s"`, time.Format("2006-01-02 15:04:05")) + return err +} + +func UpdateSystemTimeZone(timezone string) error { + _, err := shell.Execf(`sudo timedatectl set-timezone %s`, timezone) + return err +} + +// pingServer 计算NTP服务器的延迟 +func pingServer(addr string) (time.Duration, error) { + options := ntp.QueryOptions{Timeout: 1 * time.Second} + response, err := ntp.QueryWithOptions(addr, options) + if err != nil { + return 0, err + } + + return response.RTT, nil +} + +// bestServer 返回延迟最低的NTP服务器 +func bestServer(addresses ...string) (string, error) { + if len(addresses) == 0 { + addresses = defaultAddresses + } + + type ntpResult struct { + address string + delay time.Duration + err error + } + + results := make(chan ntpResult, len(addresses)) + var wg sync.WaitGroup + + for _, addr := range addresses { + wg.Add(1) + go func(addr string) { + defer wg.Done() + + delay, err := pingServer(addr) + results <- ntpResult{address: addr, delay: delay, err: err} + }(addr) + } + + wg.Wait() + close(results) + + var bestAddr string + var bestDelay time.Duration + found := false + + for result := range results { + if result.err != nil { + continue + } + + if !found || result.delay < bestDelay { + bestAddr = result.address + bestDelay = result.delay + found = true + } + } + + if !found { + return "", ErrNoAvailableServer + } + + return bestAddr, nil +} diff --git a/pkg/ntp/ntp_test.go b/pkg/ntp/ntp_test.go new file mode 100644 index 00000000..6cee6a51 --- /dev/null +++ b/pkg/ntp/ntp_test.go @@ -0,0 +1,49 @@ +package ntp + +import ( + "testing" + "time" + + "github.com/go-rat/utils/env" + "github.com/stretchr/testify/suite" +) + +type NTPTestSuite struct { + suite.Suite +} + +func TestNTPTestSuite(t *testing.T) { + suite.Run(t, &NTPTestSuite{}) +} + +func (suite *NTPTestSuite) TestNowWithDefaultAddresses() { + now, _ := Now() + suite.WithinDuration(time.Now(), now, time.Minute) +} + +func (suite *NTPTestSuite) TestNowWithCustomAddress() { + now, err := Now("time.windows.com") + suite.NoError(err) + suite.WithinDuration(time.Now(), now, time.Minute) +} + +func (suite *NTPTestSuite) TestNowWithInvalidAddress() { + _, err := Now("invalid.address") + suite.Error(err) +} + +func (suite *NTPTestSuite) TestUpdateSystemTime() { + if env.IsWindows() { + suite.T().Skip("Skipping on Windows") + } + err := UpdateSystemTime(time.Now()) + suite.NoError(err) +} + +func (suite *NTPTestSuite) TestUpdateSystemTimeZone() { + if env.IsWindows() { + suite.T().Skip("Skipping on Windows") + } + err := UpdateSystemTimeZone("UTC") + suite.NoError(err) +} diff --git a/pkg/pluginloader/plugin.go b/pkg/pluginloader/plugin.go new file mode 100644 index 00000000..fa92b64a --- /dev/null +++ b/pkg/pluginloader/plugin.go @@ -0,0 +1,44 @@ +// Package pluginloader 面板插件加载器 +package pluginloader + +import ( + "fmt" + "sync" + + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/types" +) + +var plugins sync.Map + +func Register(plugin *types.Plugin) { + plugins.Store(plugin.Slug, plugin) +} + +func Get(slug string) (*types.Plugin, error) { + if plugin, ok := plugins.Load(slug); ok { + return plugin.(*types.Plugin), nil + } + return nil, fmt.Errorf("plugin %s not found", slug) +} + +func All() []*types.Plugin { + var list []*types.Plugin + plugins.Range(func(_, plugin any) bool { + if p, ok := plugin.(*types.Plugin); ok { + list = append(list, p) + } + return true + }) + return list +} + +func Boot(r chi.Router) { + plugins.Range(func(_, plugin any) bool { + if p, ok := plugin.(*types.Plugin); ok { + r.Route(fmt.Sprintf("/api/plugins/%s", p.Slug), p.Route) + } + return true + }) +} diff --git a/pkg/queue/job.go b/pkg/queue/job.go new file mode 100644 index 00000000..20432a31 --- /dev/null +++ b/pkg/queue/job.go @@ -0,0 +1,16 @@ +package queue + +type Job interface { + Handle(args ...any) error +} + +type JobWithErrHandle interface { + Job + ErrHandle(err error) +} + +type Jobs struct { + Job Job + Args []any + Delay uint +} diff --git a/pkg/queue/queue.go b/pkg/queue/queue.go new file mode 100644 index 00000000..f64cd049 --- /dev/null +++ b/pkg/queue/queue.go @@ -0,0 +1,92 @@ +package queue + +import ( + "errors" + "time" +) + +type Queue struct { + jobs chan Jobs + isShutdown chan struct{} + done chan struct{} +} + +func New() *Queue { + return &Queue{ + jobs: make(chan Jobs, 10), + isShutdown: make(chan struct{}), + done: make(chan struct{}), + } +} + +func (r *Queue) Push(job Job, args []any) error { + select { + case <-r.isShutdown: + return errors.New("queue is shutdown, cannot add new jobs") + default: + r.jobs <- Jobs{Job: job, Args: args} + return nil + } +} + +func (r *Queue) Bulk(jobs []Jobs) error { + for _, job := range jobs { + if job.Delay > 0 { + time.AfterFunc(time.Duration(job.Delay)*time.Second, func() { + select { + case <-r.isShutdown: + return + default: + r.jobs <- Jobs{Job: job.Job, Args: job.Args} + } + }) + continue + } + + select { + case <-r.isShutdown: + return errors.New("queue is shutdown, cannot add new jobs") + default: + r.jobs <- job + } + } + + return nil +} + +func (r *Queue) Later(delay uint, job Job, args []any) error { + time.AfterFunc(time.Duration(delay)*time.Second, func() { + select { + case <-r.isShutdown: + return + default: + r.jobs <- Jobs{Job: job, Args: args} + } + }) + + return nil +} + +func (r *Queue) Run() { + go func() { + for { + select { + case job := <-r.jobs: + if err := job.Job.Handle(job.Args...); err != nil { + if errJob, ok := job.Job.(JobWithErrHandle); ok { + errJob.ErrHandle(err) + } + } + case <-r.isShutdown: + close(r.done) + return + } + } + }() +} + +func (r *Queue) Shutdown() error { + close(r.isShutdown) + <-r.done + return nil +} diff --git a/pkg/queue/queue_test.go b/pkg/queue/queue_test.go new file mode 100644 index 00000000..db86a0fe --- /dev/null +++ b/pkg/queue/queue_test.go @@ -0,0 +1,140 @@ +package queue + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type QueueTestSuite struct { + suite.Suite +} + +func TestQueueTestSuite(t *testing.T) { + suite.Run(t, &QueueTestSuite{}) +} + +func (suite *QueueTestSuite) TestQueueInitialization() { + queue := New() + suite.NotNil(queue) + suite.NotNil(queue.jobs) + suite.NotNil(queue.isShutdown) + suite.NotNil(queue.done) +} + +func (suite *QueueTestSuite) TestPushJobToQueue() { + queue := New() + job := &MockJob{} + err := queue.Push(job, []any{"arg1", "arg2"}) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestPushJobToShutdownQueue() { + queue := New() + queue.Run() + suite.NoError(queue.Shutdown()) + job := &MockJob{} + err := queue.Push(job, []any{"arg1", "arg2"}) + suite.Error(err) + suite.EqualError(err, "queue is shutdown, cannot add new jobs") +} + +func (suite *QueueTestSuite) TestBulkJobsToQueue() { + queue := New() + jobs := []Jobs{ + {Job: &MockJob{}, Args: []any{"arg1"}}, + {Job: &MockJob{}, Args: []any{"arg2"}}, + } + err := queue.Bulk(jobs) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestBulkJobsToShutdownQueue() { + queue := New() + queue.Run() + suite.NoError(queue.Shutdown()) + jobs := []Jobs{ + {Job: &MockJob{}, Args: []any{"arg1"}}, + {Job: &MockJob{}, Args: []any{"arg2"}}, + } + err := queue.Bulk(jobs) + suite.Error(err) + suite.EqualError(err, "queue is shutdown, cannot add new jobs") +} + +func (suite *QueueTestSuite) TestLaterJobExecution() { + queue := New() + job := &MockJob{} + err := queue.Later(1, job, []any{"arg1"}) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestLaterJobExecutionOnShutdownQueue() { + queue := New() + queue.Run() + suite.NoError(queue.Shutdown()) + job := &MockJob{} + err := queue.Later(1, job, []any{"arg1"}) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestRunQueue() { + queue := New() + job := &MockJob{} + suite.NoError(queue.Push(job, []any{"arg1"})) + queue.Run() + time.Sleep(1 * time.Second) + suite.True(job.Executed) +} + +func (suite *QueueTestSuite) TestRunQueueWithLaterJob() { + queue := New() + job := &MockJob{} + suite.NoError(queue.Later(1, job, []any{"arg1"})) + queue.Run() + time.Sleep(2 * time.Second) + suite.True(job.Executed) +} + +func (suite *QueueTestSuite) TestRunQueueWithBulkJobs() { + queue := New() + jobs := []Jobs{ + {Job: &MockJob{}, Args: []any{"arg1"}}, + {Job: &MockJob{}, Args: []any{"arg2"}}, + } + suite.NoError(queue.Bulk(jobs)) + queue.Run() + time.Sleep(1 * time.Second) +} + +func (suite *QueueTestSuite) TestRunQueueWithErrHandle() { + queue := New() + job := &MockJob{} + suite.NoError(queue.Push(job, []any{"arg1"})) + queue.Run() + time.Sleep(1 * time.Second) + suite.Error(job.Err) +} + +func (suite *QueueTestSuite) TestShutdownQueue() { + queue := New() + queue.Run() + err := queue.Shutdown() + suite.NoError(err) +} + +type MockJob struct { + Executed bool + Err error +} + +func (job *MockJob) Handle(args ...any) error { + job.Executed = true + return errors.New("error") +} + +func (job *MockJob) ErrHandle(err error) { + job.Err = err +} diff --git a/pkg/shell/exec.go b/pkg/shell/exec.go index 63e4b641..8c3cf7de 100644 --- a/pkg/shell/exec.go +++ b/pkg/shell/exec.go @@ -9,9 +9,7 @@ import ( "strings" "time" - "github.com/goravel/framework/support" - - "github.com/TheTNB/panel/v2/pkg/slice" + "github.com/TheTNB/panel/pkg/slice" ) // Execf 执行 shell 命令 @@ -52,12 +50,8 @@ func ExecfAsync(shell string, args ...any) error { } go func() { - err := cmd.Wait() - if err != nil { - if support.Env == support.EnvTest { - fmt.Println(err.Error()) - panic(err) - } + if err = cmd.Wait(); err != nil { + fmt.Println(err.Error()) } }() diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 687ce960..be7a35fe 100644 --- a/pkg/ssh/ssh.go +++ b/pkg/ssh/ssh.go @@ -5,7 +5,7 @@ import ( "golang.org/x/crypto/ssh" - "github.com/TheTNB/panel/v2/pkg/io" + "github.com/TheTNB/panel/pkg/io" ) type AuthMethod int8 diff --git a/pkg/systemctl/service.go b/pkg/systemctl/service.go index 401d8143..1f854d53 100644 --- a/pkg/systemctl/service.go +++ b/pkg/systemctl/service.go @@ -5,7 +5,7 @@ import ( "os/exec" "strings" - "github.com/TheTNB/panel/v2/pkg/shell" + "github.com/TheTNB/panel/pkg/shell" ) // Status 获取服务状态 diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 74cbd3b9..6216feb6 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -9,9 +9,8 @@ import ( "time" "github.com/go-resty/resty/v2" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/color" - "github.com/goravel/framework/support/env" + "github.com/golang-module/carbon/v2" + "github.com/gookit/color" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/host" @@ -20,8 +19,9 @@ import ( "github.com/shirou/gopsutil/net" "github.com/spf13/cast" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" + "github.com/TheTNB/panel/pkg/arch" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" ) // MonitoringInfo 监控信息 @@ -159,70 +159,70 @@ func GetLatestPanelVersion() (PanelInfo, error) { var name, version, body, date, downloadName, downloadUrl, checksums, checksumsUrl string if isChina { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if version, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if body, err = shell.Execf("jq -r '.description' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.description' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if date, err = shell.Execf("jq -r '.created_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.created_at' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } } else { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if version, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if body, err = shell.Execf("jq -r '.body' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.body' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if date, err = shell.Execf("jq -r '.published_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.published_at' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } @@ -252,9 +252,9 @@ func GetPanelVersion(version string) (PanelInfo, error) { } if isChina { - output, err = shell.Execf(`curl -sSL "https://git.haozi.net/api/v4/projects/opensource%%2Fpanel/releases/` + version + `"`) + output, err = shell.Execf(`curl -sSL "https://git.haozi.net/api/v4/projects/opensource%%2Fpanel/releases/%s"`, version) } else { - output, err = shell.Execf(`curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/tags/` + version + `"`) + output, err = shell.Execf(`curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/tags/%s"`, version) } if len(output) == 0 || err != nil { @@ -278,70 +278,70 @@ func GetPanelVersion(version string) (PanelInfo, error) { var name, version2, body, date, downloadName, downloadUrl, checksums, checksumsUrl string if isChina { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if version2, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version2, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if body, err = shell.Execf("jq -r '.description' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.description' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if date, err = shell.Execf("jq -r '.created_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.created_at' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } } else { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if version2, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version2, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if body, err = shell.Execf("jq -r '.body' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.body' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if date, err = shell.Execf("jq -r '.published_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.published_at' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } @@ -361,113 +361,113 @@ func GetPanelVersion(version string) (PanelInfo, error) { // UpdatePanel 更新面板 func UpdatePanel(panelInfo PanelInfo) error { - color.Green().Printfln("目标版本: " + panelInfo.Version) - color.Green().Printfln("下载链接: " + panelInfo.DownloadUrl) + color.Greenln("目标版本: %s", panelInfo.Version) + color.Greenln("下载链接: %s", panelInfo.DownloadUrl) - color.Green().Printfln("前置检查...") + color.Greenln("前置检查...") if io.Exists("/tmp/panel-storage.zip") || io.Exists("/tmp/panel.conf.bak") { return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败导致的,请谨慎排除后重试") } - color.Green().Printfln("备份面板数据...") + color.Greenln("备份面板数据...") // 备份面板 - if err := io.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil { - color.Red().Printfln("备份面板失败") + if err := io.Compress([]string{"/www/panel"}, fmt.Sprintf("/www/backup/panel/panel-%s.zip", carbon.Now().ToShortDateTimeString()), io.Zip); err != nil { + color.Redln("备份面板失败") return err } if _, err := shell.Execf("cd /www/panel/storage && zip -r /tmp/panel-storage.zip *"); err != nil { - color.Red().Printfln("备份面板数据失败") + color.Redln("备份面板数据失败") return err } if _, err := shell.Execf("cp -f /www/panel/panel.conf /tmp/panel.conf.bak"); err != nil { - color.Red().Printfln("备份面板配置失败") + color.Redln("备份面板配置失败") return err } if !io.Exists("/tmp/panel-storage.zip") || !io.Exists("/tmp/panel.conf.bak") { return errors.New("备份面板数据失败") } - color.Green().Printfln("备份完成") + color.Greenln("备份完成") - color.Green().Printfln("清理旧版本...") + color.Greenln("清理旧版本...") if _, err := shell.Execf("rm -rf /www/panel/*"); err != nil { - color.Red().Printfln("清理旧版本失败") + color.Redln("清理旧版本失败") return err } - color.Green().Printfln("清理完成") + color.Greenln("清理完成") - color.Green().Printfln("正在下载...") - if _, err := shell.Execf("wget -T 120 -t 3 -O /www/panel/" + panelInfo.DownloadName + " " + panelInfo.DownloadUrl); err != nil { - color.Red().Printfln("下载失败") + color.Greenln("正在下载...") + if _, err := shell.Execf("wget -T 120 -t 3 -O /www/panel/%s %s", panelInfo.DownloadName, panelInfo.DownloadUrl); err != nil { + color.Redln("下载失败") return err } - if _, err := shell.Execf("wget -T 20 -t 3 -O /www/panel/" + panelInfo.Checksums + " " + panelInfo.ChecksumsUrl); err != nil { - color.Red().Printfln("下载失败") + if _, err := shell.Execf("wget -T 20 -t 3 -O /www/panel/%s %s", panelInfo.Checksums, panelInfo.ChecksumsUrl); err != nil { + color.Redln("下载失败") return err } if !io.Exists("/www/panel/"+panelInfo.DownloadName) || !io.Exists("/www/panel/"+panelInfo.Checksums) { return errors.New("下载失败") } - color.Green().Printfln("下载完成") + color.Greenln("下载完成") - color.Green().Printfln("校验下载文件...") - check, err := shell.Execf("cd /www/panel && sha256sum -c " + panelInfo.Checksums + " --ignore-missing") + color.Greenln("校验下载文件...") + check, err := shell.Execf("cd /www/panel && sha256sum -c %s", panelInfo.Checksums+" --ignore-missing") if check != panelInfo.DownloadName+": OK" || err != nil { return errors.New("下载文件校验失败") } if err = io.Remove("/www/panel/" + panelInfo.Checksums); err != nil { - color.Red().Printfln("清理临时文件失败") + color.Redln("清理临时文件失败") return err } - color.Green().Printfln("文件校验完成") + color.Greenln("文件校验完成") - color.Green().Printfln("更新新版本...") - if _, err = shell.Execf("cd /www/panel && unzip -o " + panelInfo.DownloadName + " && rm -rf " + panelInfo.DownloadName); err != nil { - color.Red().Printfln("更新失败") + color.Greenln("更新新版本...") + if _, err = shell.Execf("cd /www/panel && unzip -o %s && rm -rf %s", panelInfo.DownloadName, panelInfo.DownloadName); err != nil { + color.Redln("更新失败") return err } if !io.Exists("/www/panel/panel") { return errors.New("更新失败,可能是下载过程中出现了问题") } - color.Green().Printfln("更新完成") + color.Greenln("更新完成") - color.Green().Printfln("恢复面板数据...") + color.Greenln("恢复面板数据...") if _, err = shell.Execf("cp -f /tmp/panel-storage.zip /www/panel/storage/panel-storage.zip && cd /www/panel/storage && unzip -o panel-storage.zip && rm -rf panel-storage.zip"); err != nil { - color.Red().Printfln("恢复面板数据失败") + color.Redln("恢复面板数据失败") return err } if _, err = shell.Execf("cp -f /tmp/panel.conf.bak /www/panel/panel.conf"); err != nil { - color.Red().Printfln("恢复面板配置失败") + color.Redln("恢复面板配置失败") return err } if _, err = shell.Execf("cp -f /www/panel/scripts/panel.sh /usr/bin/panel"); err != nil { - color.Red().Printfln("恢复面板脚本失败") + color.Redln("恢复面板脚本失败") return err } if !io.Exists("/www/panel/storage/panel.db") || !io.Exists("/www/panel/panel.conf") { return errors.New("恢复面板数据失败") } - color.Green().Printfln("恢复完成") + color.Greenln("恢复完成") - color.Green().Printfln("设置面板文件权限...") + color.Greenln("设置面板文件权限...") _, _ = shell.Execf("chmod -R 700 /www/panel") _, _ = shell.Execf("chmod -R 700 /usr/bin/panel") - color.Green().Printfln("设置完成") + color.Greenln("设置完成") - color.Green().Printfln("运行升级后脚本...") + color.Greenln("运行升级后脚本...") if _, err = shell.Execf("bash /www/panel/scripts/update_panel.sh"); err != nil { - color.Red().Printfln("运行面板升级后脚本失败") + color.Redln("运行面板升级后脚本失败") return err } if _, err = shell.Execf("cp -f /www/panel/scripts/panel.service /etc/systemd/system/panel.service"); err != nil { - color.Red().Printfln("写入面板服务文件失败") + color.Redln("写入面板服务文件失败") return err } _, _ = shell.Execf("systemctl daemon-reload") - if _, err = shell.Execf("panel writeSetting version " + panelInfo.Version); err != nil { - color.Red().Printfln("写入面板版本号失败") + if _, err = shell.Execf("panel writeSetting version %s", panelInfo.Version); err != nil { + color.Redln("写入面板版本号失败") return err } - color.Green().Printfln("升级完成") + color.Greenln("升级完成") _, _ = shell.Execf("rm -rf /tmp/panel-storage.zip") _, _ = shell.Execf("rm -rf /tmp/panel.conf.bak") @@ -476,14 +476,14 @@ func UpdatePanel(panelInfo PanelInfo) error { } func RestartPanel() { - color.Green().Printfln("重启面板...") + color.Greenln("重启面板...") err := shell.ExecfAsync("sleep 2 && systemctl restart panel") if err != nil { - color.Red().Printfln("重启失败") + color.Redln("重启失败") return } - color.Green().Printfln("重启完成") + color.Greenln("重启完成") } // IsChina 是否中国大陆 diff --git a/pkg/tools/tools_test.go b/pkg/tools/tools_test.go index 3a89e0bf..42bc32c5 100644 --- a/pkg/tools/tools_test.go +++ b/pkg/tools/tools_test.go @@ -48,11 +48,11 @@ func (s *HelperTestSuite) TestGenerateVersions() { }, versions) } -func (s *HelperTestSuite) TestGetLatestPanelVersion() { +/*func (s *HelperTestSuite) TestGetLatestPanelVersion() { version, err := GetLatestPanelVersion() s.NotEmpty(version) s.Nil(err) -} +}*/ func (s *HelperTestSuite) TestGetPanelVersion() { version, err := GetPanelVersion("v2.1.29") diff --git a/pkg/types/common.go b/pkg/types/common.go index 7939130f..33c98941 100644 --- a/pkg/types/common.go +++ b/pkg/types/common.go @@ -9,3 +9,28 @@ type KV struct { Key string `json:"key"` Value string `json:"value"` } + +type LV struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// KVToMap 将 key-value 切片转换为 map +func KVToMap(kvs []KV) map[string]string { + m := make(map[string]string) + for _, item := range kvs { + m[item.Key] = item.Value + } + + return m +} + +// KVToSlice 将 key-value 切片转换为 key=value 切片 +func KVToSlice(kvs []KV) []string { + var s []string + for _, item := range kvs { + s = append(s, item.Key+"="+item.Value) + } + + return s +} diff --git a/pkg/types/plugin.go b/pkg/types/plugin.go index f664012e..beb0c285 100644 --- a/pkg/types/plugin.go +++ b/pkg/types/plugin.go @@ -1,17 +1,17 @@ package types -import "github.com/goravel/framework/contracts/foundation" +import "github.com/go-chi/chi/v5" // Plugin 插件元数据结构 type Plugin struct { - Name string // 插件名称 - Description string // 插件描述 - Slug string // 插件标识 - Version string // 插件版本 - Requires []string // 依赖插件 - Excludes []string // 排除插件 - Install string // 安装命令 - Uninstall string // 卸载命令 - Update string // 更新命令 - Boot func(app foundation.Application) // 启动时执行的命令 + Slug string `json:"slug"` // 插件标识 + Name string `json:"name"` // 插件名称 + Description string `json:"description"` // 插件描述 + Version string `json:"version"` // 插件版本 + Requires []string `json:"requires"` // 依赖插件 + Excludes []string `json:"excludes"` // 排除插件 + Install string `json:"-"` // 安装命令 + Uninstall string `json:"-"` // 卸载命令 + Update string `json:"-"` // 更新命令 + Route func(r chi.Router) `json:"-"` // 路由 } diff --git a/renovate.json b/renovate.json index 9b1b5d7e..2a877874 100644 --- a/renovate.json +++ b/renovate.json @@ -7,21 +7,26 @@ "🤖 Dependencies" ], "commitMessagePrefix": "chore(deps): ", + "lockFileMaintenance": { + "enabled": true, + "automerge": true + }, "postUpdateOptions": [ - "gomodTidy" + "gomodTidy", + "pnpmDedupe" ], "packageRules": [ { - "description": "Automerge non-major updates", + "description": "Automerge updates", "matchUpdateTypes": [ + "major", "minor", "patch" ], - "matchCurrentVersion": "!/^0/", "automerge": true } ], "ignoreDeps": [ - "github.com/goravel/framework" + "eslint" ] } diff --git a/routes/api.go b/routes/api.go deleted file mode 100644 index b7a66f8a..00000000 --- a/routes/api.go +++ /dev/null @@ -1,234 +0,0 @@ -package routes - -import ( - "path/filepath" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/route" - "github.com/goravel/framework/facades" - frameworkmiddleware "github.com/goravel/framework/http/middleware" - - "github.com/TheTNB/panel/v2/app/http/controllers" - "github.com/TheTNB/panel/v2/app/http/middleware" - "github.com/TheTNB/panel/v2/embed" -) - -func Api() { - facades.Route().Prefix("api/panel").Group(func(r route.Router) { - r.Prefix("info").Group(func(r route.Router) { - infoController := controllers.NewInfoController() - r.Get("panel", infoController.Panel) - r.Middleware(middleware.Session()).Get("homePlugins", infoController.HomePlugins) - r.Middleware(middleware.Session()).Get("nowMonitor", infoController.NowMonitor) - r.Middleware(middleware.Session()).Get("systemInfo", infoController.SystemInfo) - r.Middleware(middleware.Session()).Get("countInfo", infoController.CountInfo) - r.Middleware(middleware.Session()).Get("installedDbAndPhp", infoController.InstalledDbAndPhp) - r.Middleware(middleware.Session()).Get("checkUpdate", infoController.CheckUpdate) - r.Middleware(middleware.Session()).Get("updateInfo", infoController.UpdateInfo) - r.Middleware(middleware.Session()).Post("update", infoController.Update) - r.Middleware(middleware.Session()).Post("restart", infoController.Restart) - }) - r.Prefix("user").Group(func(r route.Router) { - userController := controllers.NewUserController() - r.Middleware(frameworkmiddleware.Throttle("login")).Post("login", userController.Login) - r.Post("logout", userController.Logout) - r.Get("isLogin", userController.IsLogin) - r.Middleware(middleware.Session()).Get("info", userController.Info) - }) - r.Prefix("task").Middleware(middleware.Session()).Group(func(r route.Router) { - taskController := controllers.NewTaskController() - r.Get("status", taskController.Status) - r.Get("list", taskController.List) - r.Get("log", taskController.Log) - r.Post("delete", taskController.Delete) - }) - r.Prefix("website").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - websiteController := controllers.NewWebsiteController() - r.Get("defaultConfig", websiteController.GetDefaultConfig) - r.Post("defaultConfig", websiteController.SaveDefaultConfig) - r.Get("backupList", websiteController.BackupList) - r.Put("uploadBackup", websiteController.UploadBackup) - r.Delete("deleteBackup", websiteController.DeleteBackup) - }) - r.Prefix("websites").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - websiteController := controllers.NewWebsiteController() - r.Get("/", websiteController.List) - r.Post("/", websiteController.Add) - r.Post("delete", websiteController.Delete) - r.Get("{id}/config", websiteController.GetConfig) - r.Post("{id}/config", websiteController.SaveConfig) - r.Delete("{id}/log", websiteController.ClearLog) - r.Post("{id}/updateRemark", websiteController.UpdateRemark) - r.Post("{id}/createBackup", websiteController.CreateBackup) - r.Post("{id}/restoreBackup", websiteController.RestoreBackup) - r.Post("{id}/resetConfig", websiteController.ResetConfig) - r.Post("{id}/status", websiteController.Status) - }) - r.Prefix("cert").Middleware(middleware.Session()).Group(func(r route.Router) { - certController := controllers.NewCertController() - r.Get("caProviders", certController.CAProviders) - r.Get("dnsProviders", certController.DNSProviders) - r.Get("algorithms", certController.Algorithms) - r.Get("users", certController.UserList) - r.Post("users", certController.UserStore) - r.Put("users/{id}", certController.UserUpdate) - r.Get("users/{id}", certController.UserShow) - r.Delete("users/{id}", certController.UserDestroy) - r.Get("dns", certController.DNSList) - r.Post("dns", certController.DNSStore) - r.Put("dns/{id}", certController.DNSUpdate) - r.Get("dns/{id}", certController.DNSShow) - r.Delete("dns/{id}", certController.DNSDestroy) - r.Get("certs", certController.CertList) - r.Post("certs", certController.CertStore) - r.Put("certs/{id}", certController.CertUpdate) - r.Get("certs/{id}", certController.CertShow) - r.Delete("certs/{id}", certController.CertDestroy) - r.Post("obtain", certController.Obtain) - r.Post("renew", certController.Renew) - r.Post("manualDNS", certController.ManualDNS) - r.Post("deploy", certController.Deploy) - }) - r.Prefix("plugin").Middleware(middleware.Session()).Group(func(r route.Router) { - pluginController := controllers.NewPluginController() - r.Get("list", pluginController.List) - r.Post("install", pluginController.Install) - r.Post("uninstall", pluginController.Uninstall) - r.Post("update", pluginController.Update) - r.Post("updateShow", pluginController.UpdateShow) - r.Get("isInstalled", pluginController.IsInstalled) - }) - r.Prefix("cron").Middleware(middleware.Session()).Group(func(r route.Router) { - cronController := controllers.NewCronController() - r.Get("list", cronController.List) - r.Get("{id}", cronController.Script) - r.Post("add", cronController.Add) - r.Put("{id}", cronController.Update) - r.Delete("{id}", cronController.Delete) - r.Post("status", cronController.Status) - r.Get("log/{id}", cronController.Log) - }) - r.Prefix("safe").Middleware(middleware.Session()).Group(func(r route.Router) { - safeController := controllers.NewSafeController() - r.Get("firewallStatus", safeController.GetFirewallStatus) - r.Post("firewallStatus", safeController.SetFirewallStatus) - r.Get("firewallRules", safeController.GetFirewallRules) - r.Post("firewallRules", safeController.AddFirewallRule) - r.Delete("firewallRules", safeController.DeleteFirewallRule) - r.Get("sshStatus", safeController.GetSshStatus) - r.Post("sshStatus", safeController.SetSshStatus) - r.Get("sshPort", safeController.GetSshPort) - r.Post("sshPort", safeController.SetSshPort) - r.Get("pingStatus", safeController.GetPingStatus) - r.Post("pingStatus", safeController.SetPingStatus) - }) - r.Prefix("container").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - containerController := controllers.NewContainerController() - r.Get("list", containerController.ContainerList) - r.Get("search", containerController.ContainerSearch) - r.Post("create", containerController.ContainerCreate) - r.Post("remove", containerController.ContainerRemove) - r.Post("start", containerController.ContainerStart) - r.Post("stop", containerController.ContainerStop) - r.Post("restart", containerController.ContainerRestart) - r.Post("pause", containerController.ContainerPause) - r.Post("unpause", containerController.ContainerUnpause) - r.Get("inspect", containerController.ContainerInspect) - r.Post("kill", containerController.ContainerKill) - r.Post("rename", containerController.ContainerRename) - r.Get("stats", containerController.ContainerStats) - r.Get("exist", containerController.ContainerExist) - r.Get("logs", containerController.ContainerLogs) - r.Post("prune", containerController.ContainerPrune) - - r.Prefix("network").Group(func(r route.Router) { - r.Get("list", containerController.NetworkList) - r.Post("create", containerController.NetworkCreate) - r.Post("remove", containerController.NetworkRemove) - r.Get("exist", containerController.NetworkExist) - r.Get("inspect", containerController.NetworkInspect) - r.Post("connect", containerController.NetworkConnect) - r.Post("disconnect", containerController.NetworkDisconnect) - r.Post("prune", containerController.NetworkPrune) - }) - - r.Prefix("image").Group(func(r route.Router) { - r.Get("list", containerController.ImageList) - r.Get("exist", containerController.ImageExist) - r.Post("pull", containerController.ImagePull) - r.Post("remove", containerController.ImageRemove) - r.Get("inspect", containerController.ImageInspect) - r.Post("prune", containerController.ImagePrune) - }) - - r.Prefix("volume").Group(func(r route.Router) { - r.Get("list", containerController.VolumeList) - r.Post("create", containerController.VolumeCreate) - r.Get("exist", containerController.VolumeExist) - r.Get("inspect", containerController.VolumeInspect) - r.Post("remove", containerController.VolumeRemove) - r.Post("prune", containerController.VolumePrune) - }) - }) - r.Prefix("file").Middleware(middleware.Session()).Group(func(r route.Router) { - fileController := controllers.NewFileController() - r.Post("create", fileController.Create) - r.Get("content", fileController.Content) - r.Post("save", fileController.Save) - r.Post("delete", fileController.Delete) - r.Post("upload", fileController.Upload) - r.Post("move", fileController.Move) - r.Post("copy", fileController.Copy) - r.Get("download", fileController.Download) - r.Post("remoteDownload", fileController.RemoteDownload) - r.Get("info", fileController.Info) - r.Post("permission", fileController.Permission) - r.Post("archive", fileController.Archive) - r.Post("unArchive", fileController.UnArchive) - r.Post("search", fileController.Search) - r.Get("list", fileController.List) - }) - r.Prefix("monitor").Middleware(middleware.Session()).Group(func(r route.Router) { - monitorController := controllers.NewMonitorController() - r.Post("switch", monitorController.Switch) - r.Post("saveDays", monitorController.SaveDays) - r.Post("clear", monitorController.Clear) - r.Get("list", monitorController.List) - r.Get("switchAndDays", monitorController.SwitchAndDays) - }) - r.Prefix("ssh").Middleware(middleware.Session()).Group(func(r route.Router) { - sshController := controllers.NewSshController() - r.Get("info", sshController.GetInfo) - r.Post("info", sshController.UpdateInfo) - r.Get("session", sshController.Session) - }) - r.Prefix("setting").Middleware(middleware.Session()).Group(func(r route.Router) { - settingController := controllers.NewSettingController() - r.Get("list", settingController.List) - r.Post("update", settingController.Update) - r.Get("https", settingController.GetHttps) - r.Post("https", settingController.UpdateHttps) - }) - r.Prefix("system").Middleware(middleware.Session()).Group(func(r route.Router) { - controller := controllers.NewSystemController() - r.Get("service/status", controller.ServiceStatus) - r.Get("service/isEnabled", controller.ServiceIsEnabled) - r.Post("service/enable", controller.ServiceEnable) - r.Post("service/disable", controller.ServiceDisable) - r.Post("service/restart", controller.ServiceRestart) - r.Post("service/reload", controller.ServiceReload) - r.Post("service/start", controller.ServiceStart) - r.Post("service/stop", controller.ServiceStop) - }) - }) - - // 文档 - swaggerController := controllers.NewSwaggerController() - facades.Route().Middleware(middleware.Session()).Get("swagger/*any", swaggerController.Index) - - // 404 - facades.Route().Fallback(func(ctx http.Context) http.Response { - index, _ := embed.PublicFS.ReadFile(filepath.Join("frontend", "index.html")) - return ctx.Response().Data(http.StatusOK, "text/html; charset=utf-8", index) - }) -} diff --git a/routes/plugin.go b/routes/plugin.go deleted file mode 100644 index 2ef1a6f3..00000000 --- a/routes/plugin.go +++ /dev/null @@ -1,172 +0,0 @@ -package routes - -import ( - "github.com/goravel/framework/contracts/route" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/http/controllers/plugins" - "github.com/TheTNB/panel/v2/app/http/middleware" -) - -// Plugin 加载插件路由 -func Plugin() { - facades.Route().Prefix("api/plugins").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - r.Prefix("mysql").Group(func(route route.Router) { - mySQLController := plugins.NewMySQLController() - route.Get("load", mySQLController.Load) - route.Get("config", mySQLController.GetConfig) - route.Post("config", mySQLController.SaveConfig) - route.Get("errorLog", mySQLController.ErrorLog) - route.Post("clearErrorLog", mySQLController.ClearErrorLog) - route.Get("slowLog", mySQLController.SlowLog) - route.Post("clearSlowLog", mySQLController.ClearSlowLog) - route.Get("rootPassword", mySQLController.GetRootPassword) - route.Post("rootPassword", mySQLController.SetRootPassword) - route.Get("databases", mySQLController.DatabaseList) - route.Post("databases", mySQLController.AddDatabase) - route.Delete("databases", mySQLController.DeleteDatabase) - route.Get("backups", mySQLController.BackupList) - route.Post("backups", mySQLController.CreateBackup) - route.Put("backups", mySQLController.UploadBackup) - route.Delete("backups", mySQLController.DeleteBackup) - route.Post("backups/restore", mySQLController.RestoreBackup) - route.Get("users", mySQLController.UserList) - route.Post("users", mySQLController.AddUser) - route.Delete("users", mySQLController.DeleteUser) - route.Post("users/password", mySQLController.SetUserPassword) - route.Post("users/privileges", mySQLController.SetUserPrivileges) - }) - r.Prefix("postgresql").Group(func(route route.Router) { - controller := plugins.NewPostgreSQLController() - route.Get("load", controller.Load) - route.Get("config", controller.GetConfig) - route.Post("config", controller.SaveConfig) - route.Get("userConfig", controller.GetUserConfig) - route.Post("userConfig", controller.SaveUserConfig) - route.Get("log", controller.Log) - route.Post("clearLog", controller.ClearLog) - route.Get("databases", controller.DatabaseList) - route.Post("databases", controller.AddDatabase) - route.Delete("databases", controller.DeleteDatabase) - route.Get("backups", controller.BackupList) - route.Post("backups", controller.CreateBackup) - route.Put("backups", controller.UploadBackup) - route.Delete("backups", controller.DeleteBackup) - route.Post("backups/restore", controller.RestoreBackup) - route.Get("roles", controller.RoleList) - route.Post("roles", controller.AddRole) - route.Delete("roles", controller.DeleteRole) - route.Post("roles/password", controller.SetRolePassword) - }) - r.Prefix("php").Group(func(route route.Router) { - phpController := plugins.NewPHPController() - route.Get("{version}/load", phpController.Load) - route.Get("{version}/config", phpController.GetConfig) - route.Post("{version}/config", phpController.SaveConfig) - route.Get("{version}/fpmConfig", phpController.GetFPMConfig) - route.Post("{version}/fpmConfig", phpController.SaveFPMConfig) - route.Get("{version}/errorLog", phpController.ErrorLog) - route.Get("{version}/slowLog", phpController.SlowLog) - route.Post("{version}/clearErrorLog", phpController.ClearErrorLog) - route.Post("{version}/clearSlowLog", phpController.ClearSlowLog) - route.Get("{version}/extensions", phpController.ExtensionList) - route.Post("{version}/extensions", phpController.InstallExtension) - route.Delete("{version}/extensions", phpController.UninstallExtension) - }) - r.Prefix("phpmyadmin").Group(func(route route.Router) { - phpMyAdminController := plugins.NewPhpMyAdminController() - route.Get("info", phpMyAdminController.Info) - route.Post("port", phpMyAdminController.SetPort) - route.Get("config", phpMyAdminController.GetConfig) - route.Post("config", phpMyAdminController.SaveConfig) - }) - r.Prefix("pureftpd").Group(func(route route.Router) { - pureFtpdController := plugins.NewPureFtpdController() - route.Get("list", pureFtpdController.List) - route.Post("add", pureFtpdController.Add) - route.Delete("delete", pureFtpdController.Delete) - route.Post("changePassword", pureFtpdController.ChangePassword) - route.Get("port", pureFtpdController.GetPort) - route.Post("port", pureFtpdController.SetPort) - }) - r.Prefix("redis").Group(func(route route.Router) { - redisController := plugins.NewRedisController() - route.Get("load", redisController.Load) - route.Get("config", redisController.GetConfig) - route.Post("config", redisController.SaveConfig) - }) - r.Prefix("s3fs").Group(func(route route.Router) { - s3fsController := plugins.NewS3fsController() - route.Get("list", s3fsController.List) - route.Post("add", s3fsController.Add) - route.Post("delete", s3fsController.Delete) - }) - r.Prefix("supervisor").Group(func(route route.Router) { - supervisorController := plugins.NewSupervisorController() - route.Get("service", supervisorController.Service) - route.Get("log", supervisorController.Log) - route.Post("clearLog", supervisorController.ClearLog) - route.Get("config", supervisorController.Config) - route.Post("config", supervisorController.SaveConfig) - route.Get("processes", supervisorController.Processes) - route.Post("startProcess", supervisorController.StartProcess) - route.Post("stopProcess", supervisorController.StopProcess) - route.Post("restartProcess", supervisorController.RestartProcess) - route.Get("processLog", supervisorController.ProcessLog) - route.Post("clearProcessLog", supervisorController.ClearProcessLog) - route.Get("processConfig", supervisorController.ProcessConfig) - route.Post("processConfig", supervisorController.SaveProcessConfig) - route.Post("deleteProcess", supervisorController.DeleteProcess) - route.Post("addProcess", supervisorController.AddProcess) - - }) - r.Prefix("fail2ban").Group(func(route route.Router) { - fail2banController := plugins.NewFail2banController() - route.Get("jails", fail2banController.List) - route.Post("jails", fail2banController.Add) - route.Delete("jails", fail2banController.Delete) - route.Get("jails/{name}", fail2banController.BanList) - route.Post("unban", fail2banController.Unban) - route.Post("whiteList", fail2banController.SetWhiteList) - route.Get("whiteList", fail2banController.GetWhiteList) - }) - r.Prefix("podman").Group(func(route route.Router) { - controller := plugins.NewPodmanController() - route.Get("registryConfig", controller.GetRegistryConfig) - route.Post("registryConfig", controller.UpdateRegistryConfig) - route.Get("storageConfig", controller.GetStorageConfig) - route.Post("storageConfig", controller.UpdateStorageConfig) - }) - r.Prefix("rsync").Group(func(route route.Router) { - rsyncController := plugins.NewRsyncController() - route.Get("modules", rsyncController.List) - route.Post("modules", rsyncController.Create) - route.Post("modules/{name}", rsyncController.Update) - route.Delete("modules/{name}", rsyncController.Destroy) - route.Get("config", rsyncController.GetConfig) - route.Post("config", rsyncController.UpdateConfig) - }) - r.Prefix("frp").Group(func(route route.Router) { - frpController := plugins.NewFrpController() - route.Get("config", frpController.GetConfig) - route.Post("config", frpController.UpdateConfig) - }) - r.Prefix("gitea").Group(func(route route.Router) { - giteaController := plugins.NewGiteaController() - route.Get("config", giteaController.GetConfig) - route.Post("config", giteaController.UpdateConfig) - }) - r.Prefix("toolbox").Group(func(route route.Router) { - toolboxController := plugins.NewToolBoxController() - route.Get("dns", toolboxController.GetDNS) - route.Post("dns", toolboxController.SetDNS) - route.Get("swap", toolboxController.GetSWAP) - route.Post("swap", toolboxController.SetSWAP) - route.Get("timezone", toolboxController.GetTimezone) - route.Post("timezone", toolboxController.SetTimezone) - route.Get("hosts", toolboxController.GetHosts) - route.Post("hosts", toolboxController.SetHosts) - route.Post("rootPassword", toolboxController.SetRootPassword) - }) - }) -} diff --git a/scripts/calculate_j.sh b/scripts/calculate_j.sh deleted file mode 100644 index e165f0d0..00000000 --- a/scripts/calculate_j.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -# 计算 j 值(通用) -calculate_j() { - export LC_ALL=C - mem=$(free -m | awk '/^Mem:/{print $2}') - swap=$(free -m | awk '/^Swap:/{print $2}') - total=$((mem + swap)) - j_value=$((total / 1024)) - cpu_cores=$(nproc) - - if [ $j_value -eq 0 ]; then - j_value=1 - fi - - if [ $j_value -gt "$cpu_cores" ]; then - j_value=$cpu_cores - fi - - echo "$j_value" -} - -# 计算 j 值(2倍内存) -calculate_j2() { - export LC_ALL=C - mem=$(free -m | awk '/^Mem:/{print $2}') - swap=$(free -m | awk '/^Swap:/{print $2}') - total=$((mem + swap)) - j_value=$((total / 2024)) - cpu_cores=$(nproc) - - if [ $j_value -eq 0 ]; then - j_value=1 - fi - - if [ $j_value -gt "$cpu_cores" ]; then - j_value=$cpu_cores - fi - - echo "$j_value" -} diff --git a/scripts/fail2ban/install.sh b/scripts/fail2ban/install.sh deleted file mode 100644 index 0aacfbc0..00000000 --- a/scripts/fail2ban/install.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y fail2ban -elif [ "${OS}" == "debian" ]; then - apt-get install -y fail2ban -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:fail2ban安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 修改 fail2ban 配置文件 -sed -i 's!# logtarget.*!logtarget = /var/log/fail2ban.log!' /etc/fail2ban/fail2ban.conf -sed -i 's!logtarget\s*=.*!logtarget = /var/log/fail2ban.log!' /etc/fail2ban/jail.conf -cat > /etc/fail2ban/jail.local << EOF -[DEFAULT] -ignoreip = 127.0.0.1/8 -bantime = 600 -findtime = 300 -maxretry = 5 -banaction = firewallcmd-ipset -action = %(action_mwl)s - -# ssh-START -[ssh] -enabled = true -filter = sshd -port = 22 -maxretry = 5 -findtime = 300 -bantime = 86400 -action = %(action_mwl)s -logpath = /var/log/secure -# ssh-END -EOF -# 替换端口 -sshPort=$(cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}') -if [ "${sshPort}" == "" ]; then - sshPort="22" -fi -sed -i "s/port = 22/port = ${sshPort}/g" /etc/fail2ban/jail.local - -# Debian 的特殊处理 -if [ "${OS}" == "debian" ]; then - sed -i "s/\/var\/log\/secure/\/var\/log\/auth.log/g" /etc/fail2ban/jail.local - sed -i "s/banaction = firewallcmd-ipset/banaction = ufw/g" /etc/fail2ban/jail.local -fi - -# 启动 fail2ban -systemctl daemon-reload -systemctl unmask fail2ban -systemctl enable fail2ban -systemctl start fail2ban - -panel writePlugin fail2ban 1.0.2 diff --git a/scripts/fail2ban/uninstall.sh b/scripts/fail2ban/uninstall.sh deleted file mode 100644 index 2ea75013..00000000 --- a/scripts/fail2ban/uninstall.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -fail2ban-client unban --all -fail2ban-client stop -systemctl stop fail2ban -systemctl disable fail2ban - -if [ "${OS}" == "centos" ]; then - dnf remove -y fail2ban -elif [ "${OS}" == "debian" ]; then - apt-get purge -y fail2ban -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -rm -rf /etc/fail2ban - -panel deletePlugin fail2ban diff --git a/scripts/fail2ban/update.sh b/scripts/fail2ban/update.sh deleted file mode 100644 index ec9b57fe..00000000 --- a/scripts/fail2ban/update.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y fail2ban -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y fail2ban -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:fail2ban安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -systemctl restart fail2ban - -panel writePlugin fail2ban 1.0.2 diff --git a/scripts/frp/install.sh b/scripts/frp/install.sh deleted file mode 100644 index 7a391336..00000000 --- a/scripts/frp/install.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -downloadUrl="https://dl.cdn.haozi.net/panel/frp" -frpPath="/www/server/frp" -frpVersion="0.58.0" - -if [ ! -d "${frpPath}" ]; then - mkdir -p ${frpPath} -fi - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - frpFile="frp_${frpVersion}_linux_amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - frpFile="frp_${frpVersion}_linux_arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 下载frp -cd ${frpPath} -wget -T 120 -t 3 -O ${frpPath}/${frpFile} ${downloadUrl}/${frpFile} -wget -T 20 -t 3 -O ${frpPath}/${frpFile}.checksum.txt ${downloadUrl}/${frpFile}.checksum.txt -if ! sha256sum --status -c ${frpPath}/${frpFile}.checksum.txt; then - echo -e $HR - echo "错误:frp checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${frpPath} - exit 1 -fi - -# 解压frp -cd ${frpPath} -7z x ${frpFile} -chmod -R 700 ${frpPath} -rm -f ${frpFile} ${frpFile}.checksum.txt - -# 配置systemd -cat >/etc/systemd/system/frps.service </etc/systemd/system/frpc.service <. -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -downloadUrl="https://dl.cdn.haozi.net/panel/frp" -frpPath="/www/server/frp" -frpVersion="0.58.0" - -if [ ! -d "${frpPath}" ]; then - mkdir -p ${frpPath} -fi - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - frpFile="frp_${frpVersion}_linux_amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - frpFile="frp_${frpVersion}_linux_arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 备份配置 -if [ -f "${frpPath}/frps.toml" ]; then - cp -f ${frpPath}/frps.toml ${frpPath}/frps.toml.bak -fi -if [ -f "${frpPath}/frpc.toml" ]; then - cp -f ${frpPath}/frpc.toml ${frpPath}/frpc.toml.bak -fi - -# 下载frp -cd ${frpPath} -wget -T 120 -t 3 -O ${frpPath}/${frpFile} ${downloadUrl}/${frpFile} -wget -T 20 -t 3 -O ${frpPath}/${frpFile}.checksum.txt ${downloadUrl}/${frpFile}.checksum.txt -if ! sha256sum --status -c ${frpPath}/${frpFile}.checksum.txt; then - echo -e $HR - echo "错误:frp checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${frpPath} - exit 1 -fi - -# 解压frp -cd ${frpPath} -7z x ${frpFile} -chmod -R 700 ${frpPath} -rm -f ${frpFile} ${frpFile}.checksum.txt - -# 还原配置 -if [ -f "${frpPath}/frps.toml.bak" ]; then - cp -f ${frpPath}/frps.toml.bak ${frpPath}/frps.toml -fi -if [ -f "${frpPath}/frpc.toml.bak" ]; then - cp -f ${frpPath}/frpc.toml.bak ${frpPath}/frpc.toml -fi - -systemctl restart frps -systemctl restart frpc - -panel writePlugin frp ${frpVersion} -echo -e ${HR} -echo "frp 安装完成" -echo -e ${HR} - diff --git a/scripts/gitea/install.sh b/scripts/gitea/install.sh deleted file mode 100644 index dc83a02c..00000000 --- a/scripts/gitea/install.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/gitea" -giteaPath="/www/server/gitea" -giteaVersion="1.22.0" - -if [ ! -d "${giteaPath}" ]; then - mkdir -p ${giteaPath} -fi - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - giteaFile="gitea-${giteaVersion}-linux-amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - giteaFile="gitea-${giteaVersion}-linux-arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf install git git-lfs -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install git git-lfs -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -git lfs install -git lfs version - -# 下载 -cd ${giteaPath} -wget -T 120 -t 3 -O ${giteaPath}/${giteaFile} ${downloadUrl}/${giteaFile} -wget -T 20 -t 3 -O ${giteaPath}/${giteaFile}.checksum.txt ${downloadUrl}/${giteaFile}.checksum.txt -if ! sha256sum --status -c ${giteaPath}/${giteaFile}.checksum.txt; then - echo -e $HR - echo "错误:gitea checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${giteaPath} - exit 1 -fi - -# 解压 -cd ${giteaPath} -7z x ${giteaFile} -rm -f ${giteaFile} ${giteaFile}.checksum.txt -mv gitea-${giteaVersion}-linux-* gitea -if [ ! -f "${giteaPath}/gitea" ]; then - echo -e $HR - echo "错误:gitea 解压失败" - rm -rf ${giteaPath} - exit 1 -fi - -# 初始化目录 -mkdir -p ${giteaPath}/{custom,data,log} -chown -R www:www ${giteaPath} -chmod -R 750 ${giteaPath} -ln -sf ${giteaPath}/gitea /usr/local/bin/gitea - -# 配置systemd -cat >/etc/systemd/system/gitea.service <. -' - -HR="+----------------------------------------------------" -giteaPath="/www/server/gitea" - -systemctl stop gitea -systemctl disable gitea - -rm -f /usr/local/bin/gitea -rm -rf ${giteaPath} -rm -f /etc/systemd/system/gitea.service -systemctl daemon-reload - -panel deletePlugin gitea -echo -e $HR -echo "gitea 卸载完成,数据库可能需自行删除" -echo -e $HR diff --git a/scripts/gitea/update.sh b/scripts/gitea/update.sh deleted file mode 100644 index 6aa748d1..00000000 --- a/scripts/gitea/update.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -downloadUrl="https://dl.cdn.haozi.net/panel/gitea" -giteaPath="/www/server/gitea" -giteaVersion="1.22.0" - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - giteaFile="gitea-${giteaVersion}-linux-amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - giteaFile="gitea-${giteaVersion}-linux-arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 下载 -cd ${giteaPath} -wget -T 120 -t 3 -O ${giteaPath}/${giteaFile} ${downloadUrl}/${giteaFile} -wget -T 20 -t 3 -O ${giteaPath}/${giteaFile}.checksum.txt ${downloadUrl}/${giteaFile}.checksum.txt -if ! sha256sum --status -c ${giteaPath}/${giteaFile}.checksum.txt; then - echo -e $HR - echo "错误:gitea checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${giteaPath} - exit 1 -fi - -# 解压 -cd ${giteaPath} -7z x ${giteaFile} -rm -f ${giteaFile} ${giteaFile}.checksum.txt - -# 替换文件 -systemctl stop gitea -rm -f gitea -mv gitea-${giteaVersion}-linux-* gitea -if [ ! -f "${giteaPath}/gitea" ]; then - echo -e $HR - echo "错误:gitea 解压失败" - rm -rf ${giteaPath} - exit 1 -fi - -chown -R www:www ${giteaPath} -chmod -R 750 ${giteaPath} -systemctl start gitea - -panel writePlugin gitea ${giteaVersion} -echo -e $HR -echo "gitea 升级完成" -echo -e $HR diff --git a/scripts/install_panel.sh b/scripts/install_panel.sh deleted file mode 100644 index 55c114c2..00000000 --- a/scripts/install_panel.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -LOGO="+----------------------------------------------------\n| 耗子面板安装脚本\n+----------------------------------------------------\n| Copyright © 2022-"$(date +%Y)" 耗子科技 All rights reserved.\n+----------------------------------------------------" -HR="+----------------------------------------------------" -setup_Path="/www" -current_Path=$(pwd) -sshPort=$(cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}') -inChina=$(curl --retry 2 -m 10 -L https://www.cloudflare-cn.com/cdn-cgi/trace 2> /dev/null | grep -qx 'loc=CN' && echo "true" || echo "false") - -Prepare_System() { - if [ $(whoami) != "root" ]; then - echo -e $HR - echo "错误:请使用 root 用户运行安装命令。" - exit 1 - fi - - ARCH=$(uname -m) - OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - if [ "${OS}" == "unknown" ]; then - echo -e $HR - echo "错误:该系统不支持安装耗子面板,请更换 Debian 12.x / RHEL 9.x 安装。" - exit 1 - fi - if [ "${ARCH}" != "x86_64" ] && [ "${ARCH}" != "aarch64" ]; then - echo -e $HR - echo "错误:该系统架构不支持安装耗子面板,请更换 x86_64 / aarch64 架构安装。" - exit 1 - fi - - if [ "${ARCH}" == "x86_64" ]; then - if [ "$(cat /proc/cpuinfo | grep -c ssse3)" -lt "1" ]; then - echo -e $HR - echo "错误:至少需运行在支持 x86-64-v2 的 CPU 上,请开启对应 CPU 指令集后重试。" - exit 1 - fi - fi - - kernelVersion=$(uname -r | awk -F '.' '{print $1}') - if [ "${kernelVersion}" != "5" ] && [ "${kernelVersion}" != "6" ]; then - echo -e $HR - echo "错误:该系统内核版本太低,不支持安装耗子面板,请更换 Debian 12 / RHEL 9.x 安装。" - exit 1 - fi - - is64bit=$(getconf LONG_BIT) - if [ "${is64bit}" != '64' ]; then - echo -e $HR - echo "错误:32 位系统不支持安装耗子面板,请更换 64 位系统安装。" - exit 1 - fi - - isInstalled=$(systemctl status panel 2>&1 | grep "Active") - if [ "${isInstalled}" != "" ]; then - echo -e $HR - echo "错误:耗子面板已安装,请勿重复安装。" - exit 1 - fi - - if ! id -u "www" > /dev/null 2>&1; then - groupadd www - useradd -s /sbin/nologin -g www www - fi - - if [ ! -d ${setup_Path} ]; then - mkdir ${setup_Path} - fi - - timedatectl set-timezone Asia/Shanghai - - [ -s /etc/selinux/config ] && sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config - setenforce 0 > /dev/null 2>&1 - - ulimit -n 1048576 - echo 2147483584 > /proc/sys/fs/file-max - checkSoftNofile=$(cat /etc/security/limits.conf | grep '^* soft nofile .*$') - checkHardNofile=$(cat /etc/security/limits.conf | grep '^* hard nofile .*$') - checkSoftNproc=$(cat /etc/security/limits.conf | grep '^* soft nproc .*$') - checkHardNproc=$(cat /etc/security/limits.conf | grep '^* hard nproc .*$') - checkFsFileMax=$(cat /etc/sysctl.conf | grep '^fs.file-max.*$') - if [ "${checkSoftNofile}" == "" ]; then - echo "* soft nofile 1048576" >> /etc/security/limits.conf - fi - if [ "${checkHardNofile}" == "" ]; then - echo "* hard nofile 1048576" >> /etc/security/limits.conf - fi - if [ "${checkSoftNproc}" == "" ]; then - echo "* soft nproc 1048576" >> /etc/security/limits.conf - fi - if [ "${checkHardNproc}" == "" ]; then - echo "* hard nproc 1048576" >> /etc/security/limits.conf - fi - if [ "${checkFsFileMax}" == "" ]; then - echo fs.file-max = 2147483584 >> /etc/sysctl.conf - fi - - # 自动开启 BBR - bbrSupported=$(ls -l /lib/modules/*/kernel/net/ipv4 | grep -c tcp_bbr) - bbrEnabled=$(sysctl net.ipv4.tcp_congestion_control | grep -c bbr) - if [ "${bbrSupported}" != "0" ] && [ "${bbrEnabled}" == "0" ]; then - qdisc=$(sysctl net.core.default_qdisc | awk '{print $3}') - echo "net.core.default_qdisc=${qdisc}" >> /etc/sysctl.conf - echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf - sysctl -p - fi - - if [ "${OS}" == "centos" ]; then - if ${inChina}; then - sed -e 's|^mirrorlist=|#mirrorlist=|g' \ - -e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.tencent.com/rocky|g' \ - -e 's|^# baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.tencent.com/rocky|g' \ - -i.bak \ - /etc/yum.repos.d/[Rr]ocky*.repo - sed -e 's|^mirrorlist=|#mirrorlist=|g' \ - -e 's|^#baseurl=https://repo.almalinux.org|baseurl=https://mirrors.tencent.com|g' \ - -e 's|^# baseurl=https://repo.almalinux.org|baseurl=https://mirrors.tencent.com|g' \ - -i.bak \ - /etc/yum.repos.d/[Aa]lmalinux*.repo - - dnf makecache -y - fi - dnf install dnf-plugins-core -y - dnf install epel-release -y - dnf config-manager --set-enabled epel - if ${inChina}; then - sed -i 's|^#baseurl=https://download.example/pub|baseurl=https://mirrors.tencent.com|' /etc/yum.repos.d/epel* - sed -i 's|^# baseurl=https://download.example/pub|baseurl=https://mirrors.tencent.com|' /etc/yum.repos.d/epel* - sed -i 's|^metalink|#metalink|' /etc/yum.repos.d/epel* - dnf makecache -y - fi - # EL 8 - dnf config-manager --set-enabled powertools - # EL 9 - dnf config-manager --set-enabled crb - # Rocky Linux - /usr/bin/crb enable - dnf makecache -y - dnf install -y bash curl wget zip unzip tar p7zip p7zip-plugins git jq git-core dos2unix rsyslog make - elif [ "${OS}" == "debian" ]; then - if ${inChina}; then - sed -i 's/deb.debian.org/mirrors.tencent.com/g' /etc/apt/sources.list - sed -i 's/security.debian.org/mirrors.tencent.com/g' /etc/apt/sources.list - fi - apt-get update -y - apt-get install -y bash curl wget zip unzip tar p7zip p7zip-full git jq git dos2unix rsyslog make - fi - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装面板依赖软件失败,请截图错误信息寻求帮助。" - exit 1 - fi - - systemctl enable rsyslog - systemctl start rsyslog - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装面板依赖软件失败,请截图错误信息寻求帮助。" - exit 1 - fi -} - -Auto_Swap() { - # 判断是否有swap - swap=$(LC_ALL=C free | grep Swap | awk '{print $2}') - if [ "${swap}" -gt 1 ]; then - return - fi - - # 设置swap - swapFile="${setup_Path}/swap" - btrfsCheck=$(df -T /www | awk '{print $2}' | tail -n 1) - if [ "${btrfsCheck}" == "btrfs" ]; then - btrfs filesystem mkswapfile --size 4G --uuid clear ${swapFile} - else - dd if=/dev/zero of=$swapFile bs=1M count=4096 - fi - chmod 600 $swapFile - mkswap -f $swapFile - swapon $swapFile - echo "$swapFile swap swap defaults 0 0" >> /etc/fstab - - mount -a - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:检测到系统的 /etc/fstab 文件配置有误,请检查排除后重试,问题解决前勿重启系统。" - exit 1 - fi -} - -Init_Panel() { - systemctl stop panel - systemctl disable panel - rm -f /etc/systemd/system/panel.service - rm -rf ${setup_Path}/panel - mkdir ${setup_Path}/server - mkdir ${setup_Path}/server/cron - mkdir ${setup_Path}/server/cron/logs - chmod -R 755 ${setup_Path}/server - mkdir ${setup_Path}/panel - # 下载面板zip包并解压 - if [ "${ARCH}" == "x86_64" ]; then - if ${inChina}; then - panelZip=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("amd64v2")) | .direct_asset_url') - panelZipName=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("amd64v2")) | .name') - else - panelZip=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("amd64v2")) | .browser_download_url') - panelZipName=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("amd64v2")) | .name') - fi - elif [ "${ARCH}" == "aarch64" ]; then - if ${inChina}; then - panelZip=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("arm64")) | .direct_asset_url') - panelZipName=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("arm64")) | .name') - else - panelZip=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("arm64")) | .browser_download_url') - panelZipName=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("arm64")) | .name') - fi - else - echo -e $HR - echo "错误:该系统架构不支持安装耗子面板,请更换 x86_64 / aarch64 架构安装。" - exit 1 - fi - if [ "$?" != "0" ] || [ "${panelZip}" == "" ] || [ "${panelZipName}" == "" ]; then - echo -e $HR - echo "错误:获取面板下载链接失败,请截图错误信息寻求帮助。" - exit 1 - fi - wget -T 120 -t 3 -O ${setup_Path}/panel/${panelZipName} "${panelZip}" - - # 下载 checksums 文件 - if ${inChina}; then - checksumsFile=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("checksums")) | .direct_asset_url') - checksumsFileName=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("checksums")) | .name') - else - checksumsFile=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("checksums")) | .browser_download_url') - checksumsFileName=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("checksums")) | .name') - fi - wget -T 20 -t 3 -O ${setup_Path}/panel/${checksumsFileName} "${checksumsFile}" - - cd ${setup_Path}/panel - if ! sha256sum --status -c ${checksumsFileName} --ignore-missing; then - echo -e $HR - echo "错误:面板压缩包 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - unzip -o ${panelZipName} - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:解压面板失败,请截图错误信息寻求帮助。" - exit 1 - fi - rm -rf ${panelZipName} - rm -rf ${checksumsFileName} - cp panel-example.conf panel.conf - - # 设置面板 - entrance=$(cat /dev/urandom | head -n 16 | md5sum | head -c 6) - sed -i "s!APP_ENTRANCE=.*!APP_ENTRANCE=/${entrance}!g" panel.conf - ${setup_Path}/panel/panel --env="panel.conf" artisan key:generate - ${setup_Path}/panel/panel --env="panel.conf" artisan jwt:secret - openssl req -x509 -nodes -days 36500 -newkey ec:<(openssl ecparam -name secp384r1) -keyout ${setup_Path}/panel/storage/ssl.key -out ${setup_Path}/panel/storage/ssl.crt -subj "/C=CN/ST=Tianjin/L=Tianjin/O=HaoZi Technology Co., Ltd./OU=HaoZi Panel/CN=Panel" - chmod -R 700 ${setup_Path}/panel - cp -f scripts/panel.sh /usr/bin/panel - chmod -R 700 /usr/bin/panel - # 防火墙放行 - if [ "${OS}" == "centos" ]; then - dnf install firewalld -y - systemctl enable firewalld - systemctl start firewalld - firewall-cmd --set-default-zone=public > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=22/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=80/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=443/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=443/udp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=8888/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=${sshPort}/tcp > /dev/null 2>&1 - firewall-cmd --reload - elif [ "${OS}" == "debian" ]; then - apt-get install ufw -y - echo y | ufw enable - systemctl enable ufw - systemctl start ufw - ufw allow 22/tcp - ufw allow 80/tcp - ufw allow 443/tcp - ufw allow 443/udp - ufw allow 8888/tcp - ufw allow ${sshPort}/tcp - ufw reload - fi - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:防火墙放行失败,请截图错误信息寻求帮助。" - exit 1 - fi - # 写入服务文件 - cp -f ${setup_Path}/panel/scripts/panel.service /etc/systemd/system/panel.service - systemctl daemon-reload - systemctl enable panel.service - systemctl start panel.service - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:面板启动失败,请截图错误信息寻求帮助。" - exit 1 - fi - - clear - echo -e $LOGO - echo '面板安装成功!' - echo -e $HR - panel init - panel getInfo - - cd ${current_Path} - rm -f install_panel.sh - rm -f install_panel.sh.checksum.txt -} - -clear -echo -e $LOGO - -# 安装确认 -read -p "面板将安装至 ${setup_Path} 目录,请输入 y 并回车以开始安装:" install -if [ "$install" != 'y' ]; then - echo "输入不正确,已退出安装。" - exit -fi - -clear -echo -e $LOGO -echo '安装面板依赖软件(如报错请检查 APT/Yum 源是否正常)' -echo -e $HR -sleep 2s -Prepare_System -Auto_Swap - -echo -e $LOGO -echo '安装面板运行环境(视网络情况可能需要较长时间)' -echo -e $HR -sleep 2s -Init_Panel diff --git a/scripts/mysql/install.sh b/scripts/mysql/install.sh deleted file mode 100644 index 94eac14a..00000000 --- a/scripts/mysql/install.sh +++ /dev/null @@ -1,352 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/mysql" -setupPath="/www" -mysqlPath="${setupPath}/server/mysql" -mysqlVersion="" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "84" ]]; then - mysqlVersion="8.4.0" - j=$(calculate_j2) -elif [[ "${1}" == "80" ]]; then - mysqlVersion="8.0.37" - j=$(calculate_j2) -elif [[ "${1}" == "57" ]]; then - mysqlVersion="5.7.44" -else - echo -e $HR - echo "错误:不支持的 MySQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install cmake bison ncurses-devel libtirpc-devel openssl-devel pkg-config openldap-devel libudev-devel cyrus-sasl-devel patchelf rpcgen rpcsvc-proto-devel -y - dnf install gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils gcc-toolset-12-annobin-annocheck gcc-toolset-12-annobin-plugin-gcc -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config libldap2-dev libudev-dev libsasl2-dev patchelf -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -mysqlUserCheck=$(cat /etc/passwd | grep mysql) -if [ "${mysqlUserCheck}" == "" ]; then - groupadd mysql - useradd -s /sbin/nologin -g mysql mysql -fi - -# 准备目录 -rm -rf ${mysqlPath} -mkdir -p ${mysqlPath} -cd ${mysqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z ${downloadUrl}/mysql-${mysqlVersion}.7z -wget -T 20 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z.checksum.txt ${downloadUrl}/mysql-${mysqlVersion}.7z.checksum.txt -if ! sha256sum --status -c mysql-${mysqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:MySQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${mysqlPath} - exit 1 -fi - -7z x mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z.checksum.txt - -# 编译 -mv mysql-${mysqlVersion} src -chmod -R 755 src -cd src -rm mysql-test/CMakeLists.txt -sed -i 's/ADD_SUBDIRECTORY(mysql-test)//g' CMakeLists.txt -mkdir build -cd build - -# 5.7 和 8.0 需要 boost -if [[ "${1}" == "57" ]] || [[ "${1}" == "80" ]]; then - MAYBE_WITH_BOOST="-DWITH_BOOST=../boost" -fi - -cmake .. -DCMAKE_INSTALL_PREFIX=${mysqlPath} -DMYSQL_DATADIR=${mysqlPath}/data -DSYSCONFDIR=${mysqlPath}/conf -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STORAGE_ENGINE=1 -DWITH_FEDERATED_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci -DENABLED_LOCAL_INFILE=1 -DWITH_DEBUG=0 -DWITH_UNIT_TESTS=OFF -DINSTALL_MYSQLTESTDIR= -DCMAKE_BUILD_TYPE=Release -DWITH_SYSTEMD=1 -DSYSTEMD_PID_DIR=${mysqlPath} ${MAYBE_WITH_BOOST} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译初始化失败,请截图错误信息寻求帮助。" - rm -rf ${mysqlPath} - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译失败,请截图错误信息寻求帮助。" - rm -rf ${mysqlPath} - exit 1 -fi - -# 安装 -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 安装失败,请截图错误信息寻求帮助。" - rm -rf ${mysqlPath} - exit 1 -fi - -# 配置 -mkdir ${mysqlPath}/conf -cat > ${mysqlPath}/conf/my.cnf << EOF -[client] -port = 3306 -socket = /tmp/mysql.sock - -[mysqld] -port = 3306 -socket = /tmp/mysql.sock -datadir = ${mysqlPath}/data -default_storage_engine = InnoDB -skip-external-locking -table_definition_cache = 400 -performance_schema_max_table_instances = 400 -key_buffer_size = 8M -max_allowed_packet = 1G -table_open_cache = 32 -sort_buffer_size = 256K -net_buffer_length = 4K -read_buffer_size = 128K -read_rnd_buffer_size = 256K -myisam_sort_buffer_size = 4M -thread_cache_size = 4 -query_cache_size = 4M -tmp_table_size = 8M -explicit_defaults_for_timestamp = 1 -#skip-name-resolve -max_connections = 500 -max_connect_errors = 100 -open_files_limit = 65535 -early-plugin-load = "" - -log-bin = mysql-bin -server-id = 1 -slow_query_log = 1 -slow-query-log-file = ${mysqlPath}/mysql-slow.log -long_query_time = 3 -log-error = ${mysqlPath}/mysql-error.log - -innodb_data_home_dir = ${mysqlPath}/data -innodb_data_file_path = ibdata1:10M:autoextend -innodb_log_group_home_dir = ${mysqlPath}/data -innodb_buffer_pool_size = 16M -innodb_redo_log_capacity = 5M -innodb_log_buffer_size = 8M -innodb_flush_log_at_trx_commit = 1 -innodb_lock_wait_timeout = 50 -innodb_max_dirty_pages_pct = 90 -innodb_read_io_threads = 4 -innodb_write_io_threads = 4 - -[mysqldump] -quick -max_allowed_packet = 500M - -[myisamchk] -key_buffer_size = 20M -sort_buffer_size = 20M -read_buffer = 2M -write_buffer = 2M - -[mysqlhotcopy] -interactive-timeout -EOF - -# 根据CPU核心数确定写入线程数 -sed -i 's/innodb_write_io_threads = 4/innodb_write_io_threads = '${cpuCore}'/g' ${mysqlPath}/conf/my.cnf -sed -i 's/innodb_read_io_threads = 4/innodb_read_io_threads = '${cpuCore}'/g' ${mysqlPath}/conf/my.cnf - -if [[ "${1}" == "84" ]] || [[ "${1}" == "80" ]]; then - sed -i '/query_cache_size/d' ${mysqlPath}/conf/my.cnf -fi -if [[ "${1}" == "57" ]]; then - sed -i '/innodb_redo_log_capacity/d' ${mysqlPath}/conf/my.cnf -fi - -# 根据内存大小调参 -if [[ ${memTotal} -gt 1024 && ${memTotal} -lt 2048 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 128#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 768K#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 768K#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 16#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 2048 && ${memTotal} -lt 4096 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 256#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 1M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 1M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 32#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 4096 && ${memTotal} -lt 8192 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 512#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 2M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 2M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 64#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 8192 && ${memTotal} -lt 16384 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 1024#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 4M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 4M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 128#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 1024M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 16384 && ${memTotal} -lt 32768 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 2048#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 256#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 2048M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 1G#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 32768 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 1024M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 4096#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 512#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 4096M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 2G#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf -fi - -# 初始化 -cd ${mysqlPath} -rm -rf ${mysqlPath}/src -rm -rf ${mysqlPath}/data -mkdir -p ${mysqlPath}/data -chown -R mysql:mysql ${mysqlPath} -chmod -R 755 ${mysqlPath} -chmod 644 ${mysqlPath}/conf/my.cnf - -${mysqlPath}/bin/mysqld --initialize-insecure --user=mysql --basedir=${mysqlPath} --datadir=${mysqlPath}/data - -# 软链接 -ln -sf ${mysqlPath}/bin/* /usr/bin/ - -# 写入 systemd 配置 -cat > /etc/systemd/system/mysqld.service << EOF -[Unit] -Description=MySQL Server -Documentation=http://dev.mysql.com/doc/refman/en/using-systemd.html -After=network.target -After=syslog.target - -[Install] -WantedBy=multi-user.target - -[Service] -User=mysql -Group=mysql - -Type=forking - -PIDFile=/www/server/mysql/mysqld.pid - -# Disable service start and stop timeout logic of systemd for mysqld service. -TimeoutSec=0 - -# Execute pre and post scripts as root -PermissionsStartOnly=true - -# Start main service -ExecStart=/www/server/mysql/bin/mysqld --daemonize --pid-file=/www/server/mysql/mysqld.pid \$MYSQLD_OPTS - -# Use this to switch malloc implementation -EnvironmentFile=-/etc/sysconfig/mysql - -# Sets open_files_limit -LimitNOFILE = 500000 - -Restart=on-failure - -RestartPreventExitStatus=1 - -PrivateTmp=false -EOF - -systemctl daemon-reload -systemctl enable mysqld -systemctl start mysqld - -mysqlPassword=$(cat /dev/urandom | head -n 16 | md5sum | head -c 16) -${mysqlPath}/bin/mysqladmin -u root password ${mysqlPassword} -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DROP DATABASE test;" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DELETE FROM mysql.user WHERE user='';" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "FLUSH PRIVILEGES;" - -panel writePlugin mysql${1} ${mysqlVersion} -panel writeMysqlPassword ${mysqlPassword} - -echo -e "${HR}\nMySQL-${1} 安装完成\n${HR}" diff --git a/scripts/mysql/uninstall.sh b/scripts/mysql/uninstall.sh deleted file mode 100644 index 67845204..00000000 --- a/scripts/mysql/uninstall.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop mysqld -systemctl disable mysqld -rm -rf /etc/systemd/system/mysqld.service -systemctl daemon-reload -pkill -9 mysqld -rm -rf /www/server/mysql - -rm -f /usr/bin/mysql* -rm -f /usr/lib/libmysql* -rm -f /usr/lib64/libmysql* - -userdel -r mysql -groupdel mysql - -panel deletePlugin mysql${1} - -echo -e "${HR}\nMySQL-${1} 卸载完成\n${HR}" diff --git a/scripts/mysql/update.sh b/scripts/mysql/update.sh deleted file mode 100644 index aa1d127f..00000000 --- a/scripts/mysql/update.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/mysql" -setupPath="/www" -mysqlPath="${setupPath}/server/mysql" -mysqlVersion="" -mysqlPassword=$(panel getSetting mysql_root_password) -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "84" ]]; then - mysqlVersion="8.4.0" - j=$(calculate_j2) -elif [[ "${1}" == "80" ]]; then - mysqlVersion="8.0.37" - j=$(calculate_j2) -elif [[ "${1}" == "57" ]]; then - mysqlVersion="5.7.44" -else - echo -e $HR - echo "错误:不支持的 MySQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install cmake bison ncurses-devel libtirpc-devel openssl-devel pkg-config openldap-devel libudev-devel cyrus-sasl-devel patchelf rpcgen rpcsvc-proto-devel p7zip p7zip-plugins -y - dnf install gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils gcc-toolset-12-annobin-annocheck gcc-toolset-12-annobin-plugin-gcc -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config libldap2-dev libudev-dev libsasl2-dev patchelf p7zip p7zip-full -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -# 停止已有服务 -systemctl stop mysqld - -# 准备目录 -cd ${mysqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z ${downloadUrl}/mysql-${mysqlVersion}.7z -wget -T 20 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z.checksum.txt ${downloadUrl}/mysql-${mysqlVersion}.7z.checksum.txt -if ! sha256sum --status -c mysql-${mysqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:MySQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${mysqlPath} - exit 1 -fi - -7z x mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z.checksum.txt - -# 编译 -mv mysql-${mysqlVersion} src -chmod -R 755 src -cd src -mkdir build -cd build - -# 5.7 需要 boost -if [[ "${1}" == "57" ]]; then - MAYBE_WITH_BOOST="-DWITH_BOOST=../boost" -fi - -cmake .. -DCMAKE_INSTALL_PREFIX=${mysqlPath} -DMYSQL_DATADIR=${mysqlPath}/data -DSYSCONFDIR=${mysqlPath}/conf -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STORAGE_ENGINE=1 -DWITH_FEDERATED_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci -DENABLED_LOCAL_INFILE=1 -DWITH_DEBUG=0 -DWITH_UNIT_TESTS=OFF -DINSTALL_MYSQLTESTDIR= -DCMAKE_BUILD_TYPE=Release -DWITH_SYSTEMD=1 -DSYSTEMD_PID_DIR=${mysqlPath} ${MAYBE_WITH_BOOST} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译初始化失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 安装 -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 设置权限 -chown -R mysql:mysql ${mysqlPath} -chmod -R 755 ${mysqlPath} -chmod 644 ${mysqlPath}/conf/my.cnf - -# 软链接 -ln -sf ${mysqlPath}/bin/* /usr/bin/ - -# 启动服务 -systemctl start mysqld - -# 执行更新后的初始化 -if [[ "${1}" == "57" ]]; then - ${mysqlPath}/bin/mysql_upgrade -uroot -p${mysqlPassword} -fi -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DROP DATABASE test;" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DELETE FROM mysql.user WHERE user='';" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "FLUSH PRIVILEGES;" - -panel writePlugin mysql${1} ${mysqlVersion} - -echo -e "${HR}\nMySQL-${1} 升级完成\n${HR}" diff --git a/scripts/openresty/install.sh b/scripts/openresty/install.sh deleted file mode 100644 index 70f10c50..00000000 --- a/scripts/openresty/install.sh +++ /dev/null @@ -1,685 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel" -setupPath="/www" -openrestyPath="${setupPath}/server/openresty" -openrestyVersion="1.25.3.1" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install cmake tar unzip gd gd-devel git-core flex perl oniguruma oniguruma-devel libsodium-devel libxml2-devel libxslt-devel bison yajl yajl-devel curl curl-devel ncurses-devel libevent-devel readline-devel libuuid-devel brotli-devel icu libicu libicu-devel openssl openssl-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential cmake tar unzip libgd3 libgd-dev git flex perl libonig-dev libsodium-dev libxml2-dev libxslt1-dev bison libyajl-dev curl libcurl4-openssl-dev libncurses5-dev libevent-dev libreadline-dev uuid-dev libbrotli-dev icu-devtools libicu-dev openssl libssl-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 准备目录 -rm -rf ${openrestyPath} -mkdir -p ${openrestyPath} -cd ${openrestyPath} - -# 下载源码 -wget -T 120 -t 3 -O ${openrestyPath}/openresty-${openrestyVersion}.tar.gz ${downloadUrl}/openresty/openresty-${openrestyVersion}.tar.gz -wget -T 20 -t 3 -O ${openrestyPath}/openresty-${openrestyVersion}.tar.gz.checksum.txt ${downloadUrl}/openresty/openresty-${openrestyVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c openresty-${openrestyVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:OpenResty 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -tar -zxvf openresty-${openrestyVersion}.tar.gz -rm -f openresty-${openrestyVersion}.tar.gz -rm -f openresty-${openrestyVersion}.tar.gz.checksum.txt -mv openresty-${openrestyVersion} src -cd src - -# tls library -wget -T 120 -t 3 -O quictls-1.1.1w.7z ${downloadUrl}/tls/quictls-1.1.1w.7z -wget -T 20 -t 3 -O quictls-1.1.1w.7z.checksum.txt ${downloadUrl}/tls/quictls-1.1.1w.7z.checksum.txt - -if ! sha256sum --status -c quictls-1.1.1w.7z.checksum.txt; then - echo -e $HR - echo "错误:quictls 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -7z x quictls-1.1.1w.7z -rm -f quictls-1.1.1w.7z -rm -f quictls-1.1.1w.7z.checksum.txt -mv quictls-1.1.1w quictls -chmod -R 755 quictls - -# patch tls library -cd quictls -wget -T 20 -t 3 -O openssl-1.1.1f-sess_set_get_cb_yield.patch ${downloadUrl}/openresty/openssl/openssl-1.1.1f-sess_set_get_cb_yield.patch -wget -T 20 -t 3 -O openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt ${downloadUrl}/openresty/openssl/openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt - -if ! sha256sum --status -c openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt; then - echo -e $HR - echo "错误:OpenSSL 补丁文件 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -patch -p1 < openssl-1.1.1f-sess_set_get_cb_yield.patch -rm -f openssl-1.1.1f-sess_set_get_cb_yield.patch -rm -f openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt -cd ../ - -# pcre2 -wget -T 60 -t 3 -O pcre2-10.43.7z ${downloadUrl}/openresty/pcre/pcre2-10.43.7z -wget -T 20 -t 3 -O pcre2-10.43.7z.checksum.txt ${downloadUrl}/openresty/pcre/pcre2-10.43.7z.checksum.txt - -if ! sha256sum --status -c pcre2-10.43.7z.checksum.txt; then - echo -e $HR - echo "错误:pcre2 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -7z x pcre2-10.43.7z -rm -f pcre2-10.43.7z -rm -f pcre2-10.43.7z.checksum.txt -mv pcre2-10.43 pcre2 -chmod -R 755 pcre2 - -# ngx_cache_purge -wget -T 20 -t 3 -O ngx_cache_purge-2.3.tar.gz ${downloadUrl}/openresty/modules/ngx_cache_purge-2.3.tar.gz -wget -T 20 -t 3 -O ngx_cache_purge-2.3.tar.gz.checksum.txt ${downloadUrl}/openresty/modules/ngx_cache_purge-2.3.tar.gz.checksum.txt - -if ! sha256sum --status -c ngx_cache_purge-2.3.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:ngx_cache_purge 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -tar -zxvf ngx_cache_purge-2.3.tar.gz -rm -f ngx_cache_purge-2.3.tar.gz -rm -f ngx_cache_purge-2.3.tar.gz.checksum.txt -mv ngx_cache_purge-2.3 ngx_cache_purge - -# nginx-sticky-module -wget -T 20 -t 3 -O nginx-sticky-module.zip ${downloadUrl}/openresty/modules/nginx-sticky-module.zip -wget -T 20 -t 3 -O nginx-sticky-module.zip.checksum.txt ${downloadUrl}/openresty/modules/nginx-sticky-module.zip.checksum.txt - -if ! sha256sum --status -c nginx-sticky-module.zip.checksum.txt; then - echo -e $HR - echo "错误:nginx-sticky-module 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o nginx-sticky-module.zip -rm -f nginx-sticky-module.zip -rm -f nginx-sticky-module.zip.checksum.txt - -# nginx-dav-ext-module -wget -T 20 -t 3 -O nginx-dav-ext-module-3.0.0.tar.gz ${downloadUrl}/openresty/modules/nginx-dav-ext-module-3.0.0.tar.gz -wget -T 20 -t 3 -O nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt ${downloadUrl}/openresty/modules/nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt - -if ! sha256sum --status -c nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:nginx-dav-ext-module 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -tar -xvf nginx-dav-ext-module-3.0.0.tar.gz -rm -f nginx-dav-ext-module-3.0.0.tar.gz -rm -f nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt -mv nginx-dav-ext-module-3.0.0 nginx-dav-ext-module - -# waf -wget -T 60 -t 3 -O uthash-2.3.0.zip ${downloadUrl}/openresty/modules/uthash-2.3.0.zip -wget -T 20 -t 3 -O uthash-2.3.0.zip.checksum.txt ${downloadUrl}/openresty/modules/uthash-2.3.0.zip.checksum.txt - -if ! sha256sum --status -c uthash-2.3.0.zip.checksum.txt; then - echo -e $HR - echo "错误:uthash 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o uthash-2.3.0.zip -mv uthash-2.3.0 uthash -rm -f uthash-2.3.0.zip -rm -f uthash-2.3.0.zip.checksum.txt -cd ../ - -wget -T 20 -t 3 -O ngx_waf-6.1.9.zip ${downloadUrl}/openresty/modules/ngx_waf-6.1.9.zip -wget -T 20 -t 3 -O ngx_waf-6.1.9.zip.checksum.txt ${downloadUrl}/openresty/modules/ngx_waf-6.1.9.zip.checksum.txt - -if ! sha256sum --status -c ngx_waf-6.1.9.zip.checksum.txt; then - echo -e $HR - echo "错误:ngx_waf 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o ngx_waf-6.1.9.zip -mv ngx_waf-6.1.9 ngx_waf -rm -f ngx_waf-6.1.9.zip -rm -f ngx_waf-6.1.9.zip.checksum.txt - -cd ngx_waf/inc -wget -T 60 -t 3 -O libinjection-3.10.0.zip ${downloadUrl}/openresty/modules/libinjection-3.10.0.zip -wget -T 20 -t 3 -O libinjection-3.10.0.zip.checksum.txt ${downloadUrl}/openresty/modules/libinjection-3.10.0.zip.checksum.txt - -if ! sha256sum --status -c libinjection-3.10.0.zip.checksum.txt; then - echo -e $HR - echo "错误:libinjection 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o libinjection-3.10.0.zip -mv libinjection-3.10.0 libinjection -rm -f libinjection-3.10.0.zip -rm -f libinjection-3.10.0.zip.checksum.txt - -cd ../ -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:OpenResty waf拓展初始化失败,请截图错误信息寻求帮助。" - rm -rf ${openrestyPath} - exit 1 -fi -cd ${openrestyPath}/src - -# brotli -wget -T 20 -t 3 -O ngx_brotli-a71f931.zip ${downloadUrl}/openresty/modules/ngx_brotli-a71f931.zip -wget -T 20 -t 3 -O ngx_brotli-a71f931.zip.checksum.txt ${downloadUrl}/openresty/modules/ngx_brotli-a71f931.zip.checksum.txt - -if ! sha256sum --status -c ngx_brotli-a71f931.zip.checksum.txt; then - echo -e $HR - echo "错误:ngx_brotli 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o ngx_brotli-a71f931.zip -mv ngx_brotli-a71f931 ngx_brotli -rm -f ngx_brotli-a71f931.zip -rm -f ngx_brotli-a71f931.zip.checksum.txt -cd ngx_brotli/deps/brotli -mkdir out && cd out -cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-Ofast -march=native -mtune=native -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_CXX_FLAGS="-Ofast -march=native -mtune=native -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_INSTALL_PREFIX=./installed .. -cmake --build . --config Release --target brotlienc - -cd ${openrestyPath}/src -export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH -export LIB_UTHASH=${openrestyPath}/src/uthash - -# 临时 patch,去除 --without-pcre2 -sed -i '/# disable pcre2 by default/,/push @ngx_opts, '\''--without-pcre2'\'';/d' configure - -./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/quictls --with-pcre=${openrestyPath}/src/pcre2 --with-pcre-jit --with-http_v2_module --with-http_v3_module --with-http_slice_module --with-stream --with-stream_ssl_module --with-stream_realip_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-ld-opt="-Wl,-s -Wl,-Bsymbolic -Wl,--gc-sections" --with-cc-opt="-DNGX_LUA_ABORT_AT_PANIC -march=native -mtune=native -Ofast -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" --with-luajit-xcflags="-DLUAJIT_NUMMODE=2 -DLUAJIT_ENABLE_LUA52COMPAT" --with-file-aio --with-threads --with-compat --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/ngx_waf -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:OpenResty编译失败,请截图错误信息寻求帮助。" - rm -rf ${openrestyPath} - exit 1 -fi -make install -if [ ! -f "${openrestyPath}/nginx/sbin/nginx" ]; then - echo -e $HR - echo "错误:OpenResty安装失败,请截图错误信息寻求帮助。" - rm -rf ${openrestyPath} - exit 1 -fi - -# 设置软链接 -ln -sf ${openrestyPath}/nginx/html ${openrestyPath}/html -ln -sf ${openrestyPath}/nginx/conf ${openrestyPath}/conf -ln -sf ${openrestyPath}/nginx/logs ${openrestyPath}/logs -ln -sf ${openrestyPath}/nginx/sbin ${openrestyPath}/sbin -ln -sf ${openrestyPath}/nginx/sbin/nginx /usr/bin/openresty -rm -f ${openrestyPath}/conf/nginx.conf - -# 创建配置目录 -cd ${openrestyPath} -rm -f openresty-${openrestyVersion}.tar.gz -rm -rf src -mkdir -p /www/wwwroot/default -mkdir -p /www/wwwlogs -mkdir -p /www/server/vhost -mkdir -p /www/server/vhost -mkdir -p /www/server/vhost/rewrite -mkdir -p /www/server/vhost/ssl -mkdir -p /www/server/vhost/acme - -# 写入主配置文件 -cat > ${openrestyPath}/conf/nginx.conf << EOF -# 该文件为OpenResty主配置文件,不建议随意修改! -user www www; -worker_processes auto; -worker_cpu_affinity auto; -worker_rlimit_nofile 65535; -pcre_jit on; -quic_bpf on; -error_log /www/wwwlogs/openresty_error.log crit; -pid /www/server/openresty/nginx.pid; - -stream { - log_format tcp_format '\$time_local|\$remote_addr|\$protocol|\$status|\$bytes_sent|\$bytes_received|\$session_time|\$upstream_addr|\$upstream_bytes_sent|\$upstream_bytes_received|\$upstream_connect_time'; - - access_log /www/wwwlogs/tcp-access.log tcp_format; - error_log /www/wwwlogs/tcp-error.log; -} - -events { - use epoll; - worker_connections 65535; - multi_accept on; -} - -http { - include mime.types; - include proxy.conf; - include default.conf; - - default_type application/octet-stream; - keepalive_timeout 60; - - server_names_hash_bucket_size 512; - client_header_buffer_size 32k; - large_client_header_buffers 4 32k; - client_max_body_size 200m; - client_body_buffer_size 10M; - client_body_in_file_only off; - - variables_hash_max_size 2048; - variables_hash_bucket_size 128; - - http2 on; - http3 on; - quic_gso on; - aio threads; - aio_write on; - directio 512k; - sendfile on; - tcp_nopush on; - tcp_nodelay on; - - fastcgi_connect_timeout 300; - fastcgi_send_timeout 300; - fastcgi_read_timeout 300; - fastcgi_buffer_size 64k; - fastcgi_buffers 8 64k; - fastcgi_busy_buffers_size 256k; - fastcgi_temp_file_write_size 256k; - fastcgi_intercept_errors on; - - gzip on; - gzip_min_length 1k; - gzip_buffers 32 4k; - gzip_http_version 1.1; - gzip_comp_level 6; - gzip_types *; - gzip_vary on; - gzip_proxied any; - gzip_disable "MSIE [1-6]\."; - brotli on; - brotli_comp_level 6; - brotli_min_length 10; - brotli_window 1m; - brotli_types *; - brotli_static on; - - limit_conn_zone \$binary_remote_addr zone=perip:10m; - limit_conn_zone \$server_name zone=perserver:10m; - - server_tokens off; - access_log off; - - waf_http_status general=403 cc_deny=444; - - # 服务状态页 - server { - listen 80; - server_name 127.0.0.1; - allow 127.0.0.1; - - location /nginx_status { - stub_status on; - access_log off; - } - location ~ ^/phpfpm_status/(?\d+)$ { - fastcgi_pass unix:/tmp/php-cgi-\$version.sock; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME \$fastcgi_script_name; - } - } - include /www/server/vhost/*.conf; -} -EOF -# 写入pathinfo配置文件 -cat > ${openrestyPath}/conf/pathinfo.conf << EOF -set \$real_script_name \$fastcgi_script_name; -if (\$fastcgi_script_name ~ "^(.+?\.php)(/.+)$") { - set \$real_script_name \$1; - set \$path_info \$2; - } -fastcgi_param SCRIPT_FILENAME \$document_root\$real_script_name; -fastcgi_param SCRIPT_NAME \$real_script_name; -fastcgi_param PATH_INFO \$path_info; -EOF -# 写入默认站点页 -cat > ${openrestyPath}/html/index.html << EOF - - - - - - 未找到网站 - 耗子面板 - - - -
-

耗子面板

-

这是耗子面板的 OpenResty 默认页面!

-

当您看到此页面,说明无法在服务器上找到该域名对应的站点。

-

耗子面板 强力驱动

-
- - -EOF - -# 写入站点停止页 -cat > ${openrestyPath}/html/stop.html << EOF - - - - - - 网站已停止 - 耗子面板 - - - -
-

耗子面板

-

该网站已被管理员停止访问!

-

当您看到此页面,说明该网站已被服务器管理员停止对外访问。

-

耗子面板 强力驱动

-
- - -EOF - -# 写入 WAF 拦截页(战未来,暂时无法生效) -cat > ${openrestyPath}/html/block.html << EOF - - - - - - 请求被拦截 - 耗子面板 - - - -
-

耗子面板

-

本次请求判断为危险的攻击请求,已被拦截!

-

可能您的请求中包含了危险的攻击内容,或者您的请求被误判为攻击请求。

-

如果您认为这是误判,请联系服务器管理员解决。

-

耗子面板 强力驱动

-
- - -EOF - -# 处理文件权限 -chmod -R 755 ${openrestyPath} -chmod -R 755 /www/wwwroot -chown -R www:www /www/wwwroot -chmod -R 644 /www/server/vhost - -# 写入无php配置文件 -echo "" > ${openrestyPath}/conf/enable-php-0.conf - -# 自动为所有PHP版本创建配置文件 -if [ -d "${setupPath}/server/php" ]; then - cd ${setupPath}/server/php - phpList=$(ls -l | grep ^d | awk '{print $NF}') - for phpVersion in ${phpList}; do - if [ -d "${setupPath}/server/php/${phpVersion}" ]; then - # 写入PHP配置文件 - cat > ${openrestyPath}/conf/enable-php-${phpVersion}.conf << EOF -location ~ \.php$ { - try_files \$uri =404; - fastcgi_pass unix:/tmp/php-cgi-${phpVersion}.sock; - fastcgi_index index.php; - include fastcgi.conf; - include pathinfo.conf; -} -EOF - fi - done -fi - -# 写入代理默认配置文件 -cat > ${openrestyPath}/conf/proxy.conf << EOF -proxy_temp_path ${openrestyPath}/proxy_temp_dir; -proxy_cache_path ${openrestyPath}/proxy_cache_dir levels=1:2 keys_zone=cache_one:20m inactive=1d max_size=5g; -proxy_connect_timeout 60; -proxy_read_timeout 60; -proxy_send_timeout 60; -proxy_buffer_size 32k; -proxy_buffers 4 64k; -proxy_busy_buffers_size 128k; -proxy_temp_file_write_size 128k; -proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; -proxy_cache cache_one; -EOF - -# 写入默认站点配置文件 -cat > ${openrestyPath}/conf/default.conf << EOF -server -{ - listen 80 default_server reuseport; - listen [::]:80 default_server reuseport; - listen 443 ssl default_server reuseport; - listen [::]:443 ssl default_server reuseport; - server_name _; - index index.html; - root /www/server/openresty/html; - ssl_reject_handshake on; -} -EOF - -# 建立日志目录 -mkdir -p /www/wwwlogs/waf -chown www:www /www/wwwlogs/waf -chmod 755 /www/wwwlogs/waf - -# 写入服务文件 -cat > /etc/systemd/system/openresty.service << EOF -[Unit] -Description=The OpenResty Application Platform -After=syslog.target network-online.target remote-fs.target nss-lookup.target -Wants=network-online.target - -[Service] -Type=forking -PIDFile=/www/server/openresty/nginx.pid -ExecStartPre=/www/server/openresty/sbin/nginx -t -c /www/server/openresty/conf/nginx.conf -ExecStart=/www/server/openresty/sbin/nginx -c /www/server/openresty/conf/nginx.conf -ExecReload=/www/server/openresty/sbin/nginx -s reload -ExecStop=/www/server/openresty/sbin/nginx -s quit -LimitNOFILE=500000 - -[Install] -WantedBy=multi-user.target -EOF - -systemctl daemon-reload -systemctl enable openresty.service -systemctl restart openresty.service - -panel writePlugin openresty ${openrestyVersion} - -echo -e "${HR}\nOpenResty 安装完成\n${HR}" diff --git a/scripts/openresty/uninstall.sh b/scripts/openresty/uninstall.sh deleted file mode 100644 index bb675446..00000000 --- a/scripts/openresty/uninstall.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop openresty -systemctl disable openresty -rm -rf /etc/systemd/system/openresty.service -systemctl daemon-reload -pkill -9 nginx -rm -rf /www/server/openresty - -panel deletePlugin openresty - -echo -e "${HR}\nOpenResty 卸载完成\n${HR}" diff --git a/scripts/panel.service b/scripts/panel.service deleted file mode 100644 index e3434fdf..00000000 --- a/scripts/panel.service +++ /dev/null @@ -1,20 +0,0 @@ -[Unit] -Description=HaoZi Panel -After=syslog.target network.target -Wants=network.target - -[Service] -Type=simple -WorkingDirectory=/www/panel/ -ExecStart=/www/panel/panel --env="/www/panel/panel.conf" -ExecReload=kill -s HUP $MAINPID -ExecStop=kill -s QUIT $MAINPID -User=root -Restart=always -RestartSec=5 -LimitNOFILE=1048576 -LimitNPROC=1048576 -Delegate=yes - -[Install] -WantedBy=multi-user.target diff --git a/scripts/php/install.sh b/scripts/php/install.sh deleted file mode 100644 index 6a1ac75d..00000000 --- a/scripts/php/install.sh +++ /dev/null @@ -1,280 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/php" -setupPath="/www" -phpVersion="${1}" -phpVersionCode="" -phpPath="${setupPath}/server/php/${phpVersion}" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf install dnf-plugins-core -y - dnf install epel-release -y - dnf config-manager --set-enabled epel - dnf config-manager --set-enabled PowerTools - dnf config-manager --set-enabled powertools - dnf config-manager --set-enabled CRB - dnf config-manager --set-enabled Crb - dnf config-manager --set-enabled crb - /usr/bin/crb enable - dnf makecache - dnf groupinstall "Development Tools" -y - dnf install autoconf glibc-headers gdbm-devel gd gd-devel perl oniguruma-devel libsodium-devel libxml2-devel sqlite-devel libzip-devel bzip2-devel xz-devel libpng-devel libjpeg-devel libwebp-devel libavif-devel freetype-devel gmp-devel openssl-devel readline-devel libxslt-devel libcurl-devel pkgconfig libedit-devel zlib-devel pcre-devel crontabs libicu libicu-devel c-ares -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential autoconf libc6-dev libgdbm-dev libgd-tools libgd-dev perl libonig-dev libsodium-dev libxml2-dev libsqlite3-dev libzip-dev libbz2-dev liblzma-dev libpng-dev libjpeg-dev libwebp-dev libavif-dev libfreetype6-dev libgmp-dev libssl-dev libreadline-dev libxslt1-dev libcurl4-openssl-dev pkg-config libedit-dev zlib1g-dev libpcre3-dev cron libicu-dev libc-ares2 libc-ares-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 准备安装目录 -rm -rf ${phpPath} -mkdir -p ${phpPath} -cd ${phpPath} - -# 下载源码 -if [ "${phpVersion}" == "74" ]; then - phpVersionCode="7.4.33" -elif [ "${phpVersion}" == "80" ]; then - phpVersionCode="8.0.30" -elif [ "${phpVersion}" == "81" ]; then - phpVersionCode="8.1.29" -elif [ "${phpVersion}" == "82" ]; then - phpVersionCode="8.2.20" -elif [ "${phpVersion}" == "83" ]; then - phpVersionCode="8.3.8" -else - echo -e $HR - echo "错误:PHP-${phpVersion}不支持,请检查版本号是否正确。" - exit 1 -fi - -wget -T 120 -t 3 -O ${phpPath}/php-${phpVersionCode}.7z ${downloadUrl}/php-${phpVersionCode}.7z -wget -T 20 -t 3 -O ${phpPath}/php-${phpVersionCode}.7z.checksum.txt ${downloadUrl}/php-${phpVersionCode}.7z.checksum.txt - -if ! sha256sum --status -c php-${phpVersionCode}.7z.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion}源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${phpPath} - exit 1 -fi - -7z x php-${phpVersionCode}.7z -rm -f php-${phpVersionCode}.7z -rm -f php-${phpVersionCode}.7z.checksum.txt -mv php-* src -chmod -R 755 src - -if [ "${phpVersion}" -le "80" ]; then - wget -T 120 -t 3 -O ${phpPath}/openssl-1.1.1w.tar.gz ${downloadUrl}/openssl/openssl-1.1.1w.tar.gz - wget -T 20 -t 3 -O ${phpPath}/openssl-1.1.1w.tar.gz.checksum.txt ${downloadUrl}/openssl/openssl-1.1.1w.tar.gz.checksum.txt - - if ! sha256sum --status -c openssl-1.1.1w.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} OpenSSL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${phpPath} - exit 1 - fi - - tar -zxvf openssl-1.1.1w.tar.gz - rm -f openssl-1.1.1w.tar.gz - rm -f openssl-1.1.1w.tar.gz.checksum.txt - mv openssl-1.1.1w openssl - cd openssl - ./config --prefix=/usr/local/openssl-1.1 --openssldir=/usr/local/openssl-1.1 no-tests - make "-j${j}" - make install - echo "/usr/local/openssl-1.1/lib" > /etc/ld.so.conf.d/openssl-1.1.conf - ldconfig - cd .. - rm -rf openssl - - export CFLAGS="-I/usr/local/openssl-1.1/include -I/usr/local/curl/include" - export LIBS="-L/usr/local/openssl-1.1/lib -L/usr/local/curl/lib" -fi - -# 配置 -cd src -if [ "${phpVersion}" == "81" ] || [ "${phpVersion}" == "82" ] || [ "${phpVersion}" == "83" ]; then - ./configure --prefix=${phpPath} --with-config-file-path=${phpPath}/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-freetype --with-jpeg --with-zlib --enable-xml --disable-rpath --enable-bcmath --enable-shmop --with-curl --enable-mbregex --enable-mbstring --enable-pcntl --enable-ftp --enable-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --enable-soap --disable-fileinfo --enable-opcache --with-sodium --with-webp --with-avif -else - ./configure --prefix=${phpPath} --with-config-file-path=${phpPath}/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-freetype --with-jpeg --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --enable-pcntl --enable-ftp --enable-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-soap --disable-fileinfo --enable-opcache --with-sodium --with-webp -fi - -# 编译安装 -make "-j${j}" -make install -if [ ! -f "${phpPath}/bin/php" ]; then - echo -e $HR - echo "错误:PHP-${phpVersion}安装失败,请截图错误信息寻求帮助!" - rm -rf ${phpPath} - exit 1 -fi - -# 创建php配置 -mkdir -p ${phpPath}/etc -\cp php.ini-production ${phpPath}/etc/php.ini - -# 安装zip拓展 -cd ${phpPath}/src/ext/zip -${phpPath}/bin/phpize -./configure --with-php-config=${phpPath}/bin/php-config -make "-j${j}" -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PHP-${phpVersion} zip拓展安装失败,请截图错误信息寻求帮助。" - exit 1 -fi -cd ../../ - -# 写入拓展标记位 -echo ";下方标记位禁止删除,否则将导致PHP拓展无法正常安装!" >> ${phpPath}/etc/php.ini -echo ";haozi" >> ${phpPath}/etc/php.ini -# 写入zip拓展到php配置 -echo "extension=zip" >> ${phpPath}/etc/php.ini - -# 设置软链接 -rm -f /usr/bin/php-${phpVersion} -rm -f /usr/bin/pear -rm -f /usr/bin/pecl -ln -sf ${phpPath}/bin/php /usr/bin/php -ln -sf ${phpPath}/bin/php /usr/bin/php-${phpVersion} -ln -sf ${phpPath}/bin/phpize /usr/bin/phpize -ln -sf ${phpPath}/bin/pear /usr/bin/pear -ln -sf ${phpPath}/bin/pecl /usr/bin/pecl -ln -sf ${phpPath}/sbin/php-fpm /usr/bin/php-fpm-${phpVersion} - -# 设置fpm -cat > ${phpPath}/etc/php-fpm.conf << EOF -[global] -pid = ${phpPath}/var/run/php-fpm.pid -error_log = ${phpPath}/var/log/php-fpm.log -log_level = notice - -[www] -listen = /tmp/php-cgi-${phpVersion}.sock -listen.backlog = -1 -listen.allowed_clients = 127.0.0.1 -listen.owner = www -listen.group = www -listen.mode = 0666 -user = www -group = www -pm = dynamic -pm.max_children = 30 -pm.start_servers = 5 -pm.min_spare_servers = 5 -pm.max_spare_servers = 10 -request_terminate_timeout = 100 -request_slowlog_timeout = 30 -pm.status_path = /phpfpm_status/${phpVersion} -slowlog = var/log/slow.log -EOF - -# 设置PHP进程数 -memTotal=$(free -m | grep Mem | awk '{print $2}') -if [[ ${memTotal} -gt 1024 && ${memTotal} -le 2048 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 50#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 10#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 2048 && ${memTotal} -le 4096 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 80#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 20#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 4096 && ${memTotal} -le 8192 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 150#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 10#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 10#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 30#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 8192 && ${memTotal} -le 16384 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 200#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 15#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 15#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 30#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 16384 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 300#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 20#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 20#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 50#" ${phpPath}/etc/php-fpm.conf -fi -sed -i "s#listen.backlog.*#listen.backlog = 8192#" ${phpPath}/etc/php-fpm.conf -# 最大上传限制100M -sed -i 's/post_max_size =.*/post_max_size = 100M/g' ${phpPath}/etc/php.ini -sed -i 's/upload_max_filesize =.*/upload_max_filesize = 100M/g' ${phpPath}/etc/php.ini -# 时区PRC -sed -i 's/;date.timezone =.*/date.timezone = PRC/g' ${phpPath}/etc/php.ini -sed -i 's/short_open_tag =.*/short_open_tag = On/g' ${phpPath}/etc/php.ini -sed -i 's/;cgi.fix_pathinfo=.*/cgi.fix_pathinfo=1/g' ${phpPath}/etc/php.ini -# 最大运行时间 -sed -i 's/max_execution_time =.*/max_execution_time = 86400/g' ${phpPath}/etc/php.ini -sed -i 's/;sendmail_path =.*/sendmail_path = \/usr\/sbin\/sendmail -t -i/g' ${phpPath}/etc/php.ini -# 禁用函数 -sed -i 's/disable_functions =.*/disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv/g' ${phpPath}/etc/php.ini -sed -i 's/display_errors = Off/display_errors = On/g' ${phpPath}/etc/php.ini -sed -i 's/error_reporting =.*/error_reporting = E_ALL \& \~E_NOTICE/g' ${phpPath}/etc/php.ini - -# 设置SSL根证书 -#sed -i "s#;openssl.cafile=#openssl.cafile=/etc/pki/tls/certs/ca-bundle.crt#" ${phpPath}/etc/php.ini -#sed -i "s#;curl.cainfo =#curl.cainfo = /etc/pki/tls/certs/ca-bundle.crt#" ${phpPath}/etc/php.ini - -# 关闭php外显 -sed -i 's/expose_php = On/expose_php = Off/g' ${phpPath}/etc/php.ini - -# 写入openresty 调用php配置文件 -cat > /www/server/openresty/conf/enable-php-${phpVersion}.conf << EOF -location ~ \.php$ { - try_files \$uri =404; - fastcgi_pass unix:/tmp/php-cgi-${phpVersion}.sock; - fastcgi_index index.php; - include fastcgi.conf; - include pathinfo.conf; -} -EOF - -# 添加php-fpm到服务 -\cp ${phpPath}/src/sapi/fpm/php-fpm.service /lib/systemd/system/php-fpm-${phpVersion}.service -sed -i "/PrivateTmp/d" /lib/systemd/system/php-fpm-${phpVersion}.service -systemctl daemon-reload - -# 启动php -systemctl enable php-fpm-${phpVersion}.service -systemctl start php-fpm-${phpVersion}.service - -panel writePlugin php${phpVersion} ${phpVersionCode} - -echo -e "${HR}\nPHP-${phpVersion} 安装完成\n${HR}" diff --git a/scripts/php/uninstall.sh b/scripts/php/uninstall.sh deleted file mode 100644 index a2f0947c..00000000 --- a/scripts/php/uninstall.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -setupPath="/www" -phpVersion="${1}" -phpPath="${setupPath}/server/php/${phpVersion}" - -systemctl stop php-fpm-${phpVersion} -systemctl disable php-fpm-${phpVersion} -rm -rf /lib/systemd/system/php-fpm-${phpVersion}.service -systemctl daemon-reload - -# 检查是否存在phpMyAdmin -if [ -d "${setupPath}/server/phpmyadmin" ]; then - sed -i "s/enable-php-${phpVersion}/enable-php-0/g" ${setupPath}/server/vhost/phpmyadmin.conf - systemctl reload openresty -fi - -rm -rf ${phpPath} -rm -f /usr/bin/php-${phpVersion} - -panel deletePlugin php${phpVersion} - -echo -e "${HR}\nPHP-${phpVersion} 卸载完成\n${HR}" diff --git a/scripts/php_extensions/Swow.sh b/scripts/php_extensions/Swow.sh deleted file mode 100644 index 3894e3d0..00000000 --- a/scripts/php_extensions/Swow.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -swowVersion="1.4.1" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swow') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 swow" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf swow - rm -rf swow-${swowVersion}.zip - wget -T 60 -t 3 -O swow-${swowVersion}.zip ${downloadUrl}/swow-${swowVersion}.zip - wget -T 20 -t 3 -O swow-${swowVersion}.zip.checksum.txt ${downloadUrl}/swow-${swowVersion}.zip.checksum.txt - - if ! sha256sum --status -c swow-${swowVersion}.zip.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} swow 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - unzip swow-${swowVersion}.zip - mv swow-${swowVersion} swow - rm -f swow-${swowVersion}.zip - rm -f swow-${swowVersion}.zip.checksum.txt - cd swow/ext - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swow 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swow 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=swow' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swow 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swow$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 swow" - exit 1 - fi - - sed -i '/extension=swow/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swow 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/Zend OPcache.sh b/scripts/php_extensions/Zend OPcache.sh deleted file mode 100644 index d106d814..00000000 --- a/scripts/php_extensions/Zend OPcache.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -action="$1" # 操作 -phpVersion="$2" # PHP版本 - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^zend_extension=opcache$') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 Zend OPcache" - exit 1 - fi - - if [ "${phpVersion}" -ge "80" ]; then - sed -i '/;haozi/a\zend_extension=opcache\nopcache.enable = 1\nopcache.enable_cli=1\nopcache.memory_consumption=128\nopcache.interned_strings_buffer=32\nopcache.max_accelerated_files=100000\nopcache.revalidate_freq=3\nopcache.save_comments=0\nopcache.jit_buffer_size=128m\nopcache.jit=1205' /www/server/php/${phpVersion}/etc/php.ini - else - sed -i '/;haozi/a\zend_extension=opcache\nopcache.enable = 1\nopcache.enable_cli=1\nopcache.memory_consumption=128\nopcache.interned_strings_buffer=32\nopcache.max_accelerated_files=100000\nopcache.revalidate_freq=3\nopcache.save_comments=0' /www/server/php/${phpVersion}/etc/php.ini - fi - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} Zend OPcache 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^zend_extension=opcache$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 Zend OPcache" - exit 1 - fi - - sed -i '/^opcache.*$/d' /www/server/php/${phpVersion}/etc/php.ini - sed -i '/zend_extension=opcache/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} Zend OPcache 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/igbinary.sh b/scripts/php_extensions/igbinary.sh deleted file mode 100644 index 5216f369..00000000 --- a/scripts/php_extensions/igbinary.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -igbinaryVersion="3.2.15" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=igbinary') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 igbinary" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf igbinary - rm -rf igbinary-${igbinaryVersion}.zip - wget -T 60 -t 3 -O igbinary-${igbinaryVersion}.zip ${downloadUrl}/igbinary-${igbinaryVersion}.zip - wget -T 20 -t 3 -O igbinary-${igbinaryVersion}.zip.checksum.txt ${downloadUrl}/igbinary-${igbinaryVersion}.zip.checksum.txt - - if ! sha256sum --status -c igbinary-${igbinaryVersion}.zip.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} igbinary 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - unzip igbinary-${igbinaryVersion}.zip - mv igbinary-${igbinaryVersion} igbinary - rm -f igbinary-${igbinaryVersion}.zip - rm -f igbinary-${igbinaryVersion}.zip.checksum.txt - cd igbinary - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} igbinary 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} igbinary 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=igbinary' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} igbinary 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=igbinary$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 igbinary" - exit 1 - fi - - sed -i '/extension=igbinary/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} igbinary 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/imagick.sh b/scripts/php_extensions/imagick.sh deleted file mode 100644 index 3efb39e0..00000000 --- a/scripts/php_extensions/imagick.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -imagickVersion="3.7.0" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=imagick$') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 imagick" - exit 1 - fi - - # 安装依赖 - if [ "${OS}" == "centos" ]; then - dnf install ImageMagick ImageMagick-devel -y - elif [ "${OS}" == "debian" ]; then - apt-get install imagemagick libmagickwand-dev -y - else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf imagick - rm -rf imagick-${imagickVersion}.tar.gz - wget -T 60 -t 3 -O imagick-${imagickVersion}.tar.gz ${downloadUrl}/imagick-${imagickVersion}.tar.gz - wget -T 20 -t 3 -O imagick-${imagickVersion}.tar.gz.checksum.txt ${downloadUrl}/imagick-${imagickVersion}.tar.gz.checksum.txt - - if ! sha256sum --status -c imagick-${imagickVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} imagick 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - tar -zxvf imagick-${imagickVersion}.tar.gz - rm -f imagick-${imagickVersion}.tar.gz - rm -f imagick-${imagickVersion}.tar.gz.checksum.txt - mv imagick-${imagickVersion} imagick - cd imagick - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} imagick 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} imagick 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=imagick' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} imagick 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=imagick$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 imagick" - exit 1 - fi - - sed -i '/extension=imagick/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} imagick 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/ionCube Loader.sh b/scripts/php_extensions/ionCube Loader.sh deleted file mode 100644 index 33d2947b..00000000 --- a/scripts/php_extensions/ionCube Loader.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep 'ioncube_loader_lin') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 ionCube" - exit 1 - fi - - mkdir /usr/local/ioncube - cd /usr/local/ioncube - wget -T 60 -t 3 -O /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so ${downloadUrl}/ioncube_loader_lin_${phpVersion}.so - wget -T 20 -t 3 -O /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so.checksum.txt ${downloadUrl}/ioncube_loader_lin_${phpVersion}.so.checksum.txt - - if ! sha256sum --status -c /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} ionCube 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - rm -f /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so.checksum.txt - - sed -i -e "/;haozi/a\zend_extension=/usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so" /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ionCube 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep 'ioncube_loader_lin') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 ionCube" - exit 1 - fi - - rm -f /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so - sed -i '/ioncube_loader_lin/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ionCube 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/official.sh b/scripts/php_extensions/official.sh deleted file mode 100644 index 3d624e6a..00000000 --- a/scripts/php_extensions/official.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -action="$1" # 操作 -phpVersion="$2" # PHP版本 -extensionName="$3" # 扩展名称 -addArgs="" # 附加参数 - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep "^extension=${extensionName}$") - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 ${extensionName}" - exit 1 - fi - - # 安装依赖 - if [ "${extensionName}" == "snmp" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y net-snmp-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libsnmp-dev - fi - fi - if [ "${extensionName}" == "ldap" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y openldap-devel - ln -sf /usr/lib64/libldap* /usr/lib - elif [ "${OS}" == "debian" ]; then - apt-get install -y libldap2-dev - ln -sf /usr/lib/x86_64-linux-gnu/libldap* /usr/lib - fi - fi - if [ "${extensionName}" == "imap" ]; then - if [ "${OS}" == "centos" ]; then - # RHEL 9 的仓库中没有 libc-client-devel,待考虑 - dnf install -y libc-client-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libc-client-dev - fi - addArgs="--with-imap --with-imap-ssl --with-kerberos" - fi - if [ "${extensionName}" == "enchant" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y enchant-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libenchant-2-dev - fi - fi - if [ "${extensionName}" == "pspell" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y aspell-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libpspell-dev - fi - fi - if [ "${extensionName}" == "gmp" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y gmp-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libgmp-dev - fi - fi - if [ "${extensionName}" == "gettext" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y gettext-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libgettextpo-dev - fi - fi - if [ "${extensionName}" == "bz2" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y bzip2-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libbz2-dev - fi - fi - if [ "${extensionName}" == "zip" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y libzip-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libzip-dev - fi - fi - if [ "${extensionName}" == "pdo_pgsql" ]; then - addArgs="--with-pdo-pgsql=/www/server/postgresql" - fi - - # 安装扩展 - if [ ! -d /www/server/php/${phpVersion}/src/ext/${extensionName} ]; then - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 源码不存在" - exit 1 - fi - cd /www/server/php/${phpVersion}/src/ext/${extensionName} - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config ${addArgs} - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 安装失败" - exit 1 - fi - - sed -i "/;haozi/a\extension=${extensionName}" /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep "^extension=${extensionName}$") - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 ${extensionName}" - exit 1 - fi - - sed -i "/extension=${extensionName}/d" /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/redis.sh b/scripts/php_extensions/redis.sh deleted file mode 100644 index 442c4405..00000000 --- a/scripts/php_extensions/redis.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -phpredisVersion="5.3.7" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=redis$') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 redis" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf phpredis - rm -rf phpredis-${phpredisVersion}.tar.gz - wget -T 60 -t 3 -O phpredis-${phpredisVersion}.tar.gz ${downloadUrl}/phpredis-${phpredisVersion}.tar.gz - wget -T 20 -t 3 -O phpredis-${phpredisVersion}.tar.gz.checksum.txt ${downloadUrl}/phpredis-${phpredisVersion}.tar.gz.checksum.txt - - if ! sha256sum --status -c phpredis-${phpredisVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} redis 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - tar -zxvf phpredis-${phpredisVersion}.tar.gz - mv phpredis-${phpredisVersion} phpredis - rm -f phpredis-${phpredisVersion}.tar.gz - rm -f phpredis-${phpredisVersion}.tar.gz.checksum.txt - cd phpredis - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} redis 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} redis 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=redis' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} redis 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=redis$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 redis" - exit 1 - fi - - sed -i '/extension=redis/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} redis 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/swoole.sh b/scripts/php_extensions/swoole.sh deleted file mode 100644 index e6f4101c..00000000 --- a/scripts/php_extensions/swoole.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -swooleVersion="5.1.2" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swoole') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 swoole" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf swoole - rm -rf swoole-src-${swooleVersion}.zip - wget -T 60 -t 3 -O swoole-src-${swooleVersion}.zip ${downloadUrl}/swoole-src-${swooleVersion}.zip - wget -T 20 -t 3 -O swoole-src-${swooleVersion}.zip.checksum.txt ${downloadUrl}/swoole-src-${swooleVersion}.zip.checksum.txt - - if ! sha256sum --status -c swoole-src-${swooleVersion}.zip.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} swoole 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - unzip swoole-src-${swooleVersion}.zip - mv swoole-src-${swooleVersion} swoole - rm -f swoole-src-${swooleVersion}.zip - rm -f swoole-src-${swooleVersion}.zip.checksum.txt - cd swoole - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swoole 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swoole 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=swoole' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swoole 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swoole$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 swoole" - exit 1 - fi - - sed -i '/extension=swoole/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swoole 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/phpmyadmin/install.sh b/scripts/phpmyadmin/install.sh deleted file mode 100644 index 34531868..00000000 --- a/scripts/phpmyadmin/install.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/phpmyadmin" -setupPath="/www" -phpmyadminPath="${setupPath}/server/phpmyadmin" -phpmyadminVersion="5.2.1" -randomDir="$(cat /dev/urandom | head -n 16 | md5sum | head -c 10)" - -# 准备安装目录 -rm -rf ${phpmyadminPath} -mkdir -p ${phpmyadminPath} -cd ${phpmyadminPath} - -wget -T 60 -t 3 -O phpMyAdmin-${phpmyadminVersion}-all-languages.zip ${downloadUrl}/phpMyAdmin-${phpmyadminVersion}-all-languages.zip -wget -T 20 -t 3 -O phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt ${downloadUrl}/phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt - -if ! sha256sum --status -c phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt; then - echo -e $HR - echo "错误:phpMyAdmin 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${phpmyadminPath} - exit 1 -fi - -unzip -o phpMyAdmin-${phpmyadminVersion}-all-languages.zip -mv phpMyAdmin-${phpmyadminVersion}-all-languages phpmyadmin_${randomDir} -chown -R www:www ${phpmyadminPath} -chmod -R 755 ${phpmyadminPath} -rm -rf phpMyAdmin-${phpmyadminVersion}-all-languages.zip -rm -rf phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt - -# 判断PHP版本 -phpVersion="" -if [ -d "/www/server/php/74" ]; then - phpVersion="74" -fi -if [ -d "/www/server/php/80" ]; then - phpVersion="80" -fi -if [ -d "/www/server/php/81" ]; then - phpVersion="81" -fi -if [ -d "/www/server/php/82" ]; then - phpVersion="82" -fi - -if [ "${phpVersion}" == "" ]; then - echo -e $HR - echo "错误:未安装 PHP" - rm -rf ${phpmyadminPath} - exit 1 -fi - -# 写入 phpMyAdmin 配置文件 -cat > /www/server/vhost/phpmyadmin.conf << EOF -# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 - listen 888; - # port标记位结束 - # server_name标记位开始 - server_name phpmyadmin; - # server_name标记位结束 - # index标记位开始 - index index.php; - # index标记位结束 - # root标记位开始 - root /www/server/phpmyadmin; - # root标记位结束 - - # php标记位开始 - include enable-php-${phpVersion}.conf; - # php标记位结束 - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - location ~ /tmp/ { - return 403; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log /www/wwwlogs/phpmyadmin.log; - error_log /www/wwwlogs/phpmyadmin.log; -} -EOF -# 设置文件权限 -chown -R root:root /www/server/vhost/phpmyadmin.conf -chmod -R 644 /www/server/vhost/phpmyadmin.conf -chmod -R 755 ${phpmyadminPath} -chown -R www:www ${phpmyadminPath} - -# 放行端口 -if [ "${OS}" == "centos" ]; then - firewall-cmd --permanent --zone=public --add-port=888/tcp > /dev/null 2>&1 - firewall-cmd --reload -elif [ "${OS}" == "debian" ]; then - ufw allow 888/tcp > /dev/null 2>&1 - ufw reload -fi - -panel writePlugin phpmyadmin 5.2.1 -systemctl reload openresty - -echo -e "${HR}\phpMyAdmin 安装完成\n${HR}" diff --git a/scripts/phpmyadmin/uninstall.sh b/scripts/phpmyadmin/uninstall.sh deleted file mode 100644 index 5566716d..00000000 --- a/scripts/phpmyadmin/uninstall.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -setupPath="/www" -phpmyadminPath="${setupPath}/server/phpmyadmin" - - -rm -rf ${setupPath}/server/vhost/phpmyadmin.conf -rm -rf ${phpmyadminPath} -panel deletePlugin phpmyadmin -systemctl reload openresty - -echo -e "${HR}\phpMyAdmin 卸载完成\n${HR}" diff --git a/scripts/podman/install.sh b/scripts/podman/install.sh deleted file mode 100644 index 9cf07ad8..00000000 --- a/scripts/podman/install.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -podmanVersion="4.0.0" - -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf install podman -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install podman containers-storage -y - # Debian下不清楚是不是Bug,需要手动复制存储配置到正确位置 - cp /usr/share/containers/storage.conf /etc/containers/storage.conf -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -systemctl enable podman -systemctl enable podman.socket -systemctl enable podman-restart -systemctl start podman -systemctl start podman.socket -systemctl start podman-restart - -panel writePlugin podman ${podmanVersion} -echo -e ${HR} -echo "podman 安装完成" -echo -e ${HR} diff --git a/scripts/podman/uninstall.sh b/scripts/podman/uninstall.sh deleted file mode 100644 index 29d142e3..00000000 --- a/scripts/podman/uninstall.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - - -systemctl stop podman-restart -systemctl stop podman -systemctl disable podman-restart -systemctl disable podman - -if [ "${OS}" == "centos" ]; then - dnf remove podman -y -elif [ "${OS}" == "debian" ]; then - apt-get remove podman -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -panel deletePlugin podman -echo -e $HR -echo "podman 卸载完成" -echo -e $HR diff --git a/scripts/podman/update.sh b/scripts/podman/update.sh deleted file mode 100644 index da0f48bd..00000000 --- a/scripts/podman/update.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -podmanVersion="4.0.0" - -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf update podman -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get upgrade podman -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -systemctl restart podman - -panel writePlugin podman ${podmanVersion} -echo -e ${HR} -echo "podman 安装完成" -echo -e ${HR} diff --git a/scripts/postgresql/install.sh b/scripts/postgresql/install.sh deleted file mode 100644 index 39c15080..00000000 --- a/scripts/postgresql/install.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/postgresql" -setupPath="/www" -postgresqlPath="${setupPath}/server/postgresql" -postgresqlVersion="" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "15" ]]; then - postgresqlVersion="15.7" -elif [[ "${1}" == "16" ]]; then - postgresqlVersion="16.3" -else - echo -e $HR - echo "错误:不支持的 PostgreSQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install make gettext zlib-devel readline-devel libicu-devel libxml2-devel libxslt-devel openssl-devel systemd-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential make gettext zlib1g-dev libreadline-dev libicu-dev libxml2-dev libxslt-dev libssl-dev libsystemd-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -postgresqlUserCheck=$(cat /etc/passwd | grep postgres) -if [ "${postgresqlUserCheck}" == "" ]; then - groupadd postgres - useradd -g postgres postgres -fi - -# 准备目录 -rm -rf ${postgresqlPath} -mkdir -p ${postgresqlPath} -cd ${postgresqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z ${downloadUrl}/postgresql-${postgresqlVersion}.7z -wget -T 20 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z.checksum.txt ${downloadUrl}/postgresql-${postgresqlVersion}.7z.checksum.txt - -if ! sha256sum --status -c postgresql-${postgresqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:PostgreSQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${postgresqlPath} - exit 1 -fi - -7z x postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z.checksum.txt -mv postgresql-${postgresqlVersion} src -chmod -R 755 src - -# 编译 -cd src -./configure --prefix=${postgresqlPath} --enable-nls='zh_CN en' --with-icu --with-ssl=openssl --with-systemd --with-libxml --with-libxslt -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译初始化失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 安装失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi - -cd ${postgresqlPath} -rm -rf ${postgresqlPath}/src - -# 配置 -mkdir -p ${postgresqlPath}/data -mkdir -p ${postgresqlPath}/logs -chown -R postgres:postgres ${postgresqlPath} -chmod -R 700 ${postgresqlPath} - -echo "export PATH=${postgresqlPath}/bin:\$PATH" >> /etc/profile -source /etc/profile - -mkdir -p /home/postgres -cd /home/postgres -if [ -f /home/postgres/.bash_profile ]; then - echo "export PGHOME=${postgresqlPath}" >> /home/postgres/.bash_profile - echo "export PGDATA=${postgresqlPath}/data" >> /home/postgres/.bash_profile - echo "export PATH=${postgresqlPath}/bin:\$PATH " >> /home/postgres/.bash_profile - echo "MANPATH=$PGHOME/share/man:$MANPATH" >> /home/postgres/.bash_profile - echo "LD_LIBRARY_PATH=$PGHOME/lib:$LD_LIBRARY_PATH" >> /home/postgres/.bash_profile - source /home/postgres/.bash_profile -fi -if [ -f /home/postgres/.profile ]; then - echo "export PGHOME=${postgresqlPath}" >> /home/postgres/.profile - echo "export PGDATA=${postgresqlPath}/data" >> /home/postgres/.profile - echo "export PATH=${postgresqlPath}/bin:\$PATH " >> /home/postgres/.profile - echo "MANPATH=$PGHOME/share/man:$MANPATH" >> /home/postgres/.profile - echo "LD_LIBRARY_PATH=$PGHOME/lib:$LD_LIBRARY_PATH" >> /home/postgres/.profile - source /home/postgres/.profile -fi - -# 初始化 -su - postgres -c "${postgresqlPath}/bin/initdb -D ${postgresqlPath}/data" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 初始化失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi - -# 配置慢查询日志 -cat >> ${postgresqlPath}/data/postgresql.conf << EOF -logging_collector = on -log_destination = 'stderr' -log_directory = '${postgresqlPath}/logs' -log_filename = 'postgresql-%Y-%m-%d.log' -log_statement = all -log_min_duration_statement = 5000 -EOF - -# 写入服务 -cat > /etc/systemd/system/postgresql.service << EOF -[Unit] -Description=PostgreSQL database server -Documentation=man:postgres(1) -After=network-online.target -Wants=network-online.target - -[Service] -Type=notify -User=postgres -ExecStart=${postgresqlPath}/bin/postgres -D ${postgresqlPath}/data -ExecReload=/bin/kill -HUP \$MAINPID -KillMode=mixed -KillSignal=SIGINT -TimeoutSec=infinity -LimitNOFILE=500000 - -[Install] -WantedBy=multi-user.target -EOF - -# 在 /etc/systemd/logind.conf 设置 RemoveIPC=no,不然会删除 /dev/shm 下的共享内存文件 -checkRemoveIPC=$(cat /etc/systemd/logind.conf | grep '^RemoveIPC=no.*$') -if [ "${checkRemoveIPC}" == "" ]; then - echo "RemoveIPC=no" >> /etc/systemd/logind.conf - systemctl restart systemd-logind -fi - -# 启动服务 -systemctl daemon-reload -systemctl enable postgresql -systemctl start postgresql - -panel writePlugin postgresql${1} ${postgresqlVersion} - -echo -e "${HR}\nPostgreSQL-${1} 安装完成\n${HR}" diff --git a/scripts/postgresql/uninstall.sh b/scripts/postgresql/uninstall.sh deleted file mode 100644 index c9231f42..00000000 --- a/scripts/postgresql/uninstall.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop postgresql -systemctl disable postgresql -rm -rf /etc/systemd/system/postgresql.service -systemctl daemon-reload -pkill -9 postgresql -rm -rf /www/server/postgresql - -userdel -r postgres -groupdel postgres - -sed -i '/export PATH=\/www\/server\/postgresql/d' /etc/profile -source /etc/profile - -panel deletePlugin postgresql${1} - -echo -e "${HR}\nPostgreSQL-${1} 卸载完成\n${HR}" diff --git a/scripts/postgresql/update.sh b/scripts/postgresql/update.sh deleted file mode 100644 index 8f8de29d..00000000 --- a/scripts/postgresql/update.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/postgresql" -setupPath="/www" -postgresqlPath="${setupPath}/server/postgresql" -postgresqlVersion="" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "15" ]]; then - postgresqlVersion="15.7" -elif [[ "${1}" == "16" ]]; then - postgresqlVersion="16.3" -else - echo -e $HR - echo "错误:不支持的 PostgreSQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install make gettext zlib-devel readline-devel libicu-devel libxml2-devel libxslt-devel openssl-devel systemd-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential make gettext zlib1g-dev libreadline-dev libicu-dev libxml2-dev libxslt-dev libssl-dev libsystemd-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -# 停止已有服务 -systemctl stop postgresql - -# 准备目录 -rm -rf ${postgresqlPath}/src -cd ${postgresqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z ${downloadUrl}/postgresql-${postgresqlVersion}.7z -wget -T 20 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z.checksum.txt ${downloadUrl}/postgresql-${postgresqlVersion}.7z.checksum.txt - -if ! sha256sum --status -c postgresql-${postgresqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:PostgreSQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 -fi - -7z x postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z.checksum.txt -mv postgresql-${postgresqlVersion} src -chmod -R 755 src - -# 编译 -cd src -./configure --prefix=${postgresqlPath} --enable-nls='zh_CN en' --with-icu --with-ssl=openssl --with-systemd --with-libxml --with-libxslt -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译初始化失败,请截图错误信息寻求帮助。" - exit 1 -fi -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译失败,请截图错误信息寻求帮助。" - exit 1 -fi -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -cd ${postgresqlPath} -rm -rf ${postgresqlPath}/src - -# 配置 -chown -R postgres:postgres ${postgresqlPath} -chmod -R 700 ${postgresqlPath} - -panel writePlugin postgresql${1} ${postgresqlVersion} - -systemctl daemon-reload -systemctl restart postgresql - -echo -e "${HR}\nPostgreSQL-${1} 升级完成\n${HR}" diff --git a/scripts/pureftpd/install.sh b/scripts/pureftpd/install.sh deleted file mode 100644 index b1d633b3..00000000 --- a/scripts/pureftpd/install.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/pure-ftpd" -setupPath="/www" -pureftpdPath="${setupPath}/server/pure-ftpd" -pureftpdVersion="1.0.50" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 准备安装目录 -rm -rf ${pureftpdPath} -mkdir -p ${pureftpdPath} -cd ${pureftpdPath} - -wget -T 120 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz -wget -T 20 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${pureftpdPath} - exit 1 -fi - -tar -xvf pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt -mv pure-ftpd-${pureftpdVersion} src -cd src - -./configure --prefix=${pureftpdPath} CFLAGS=-O2 --with-puredb --with-quotas --with-cookie --with-virtualhosts --with-diraliases --with-sysquotas --with-ratios --with-altlog --with-paranoidmsg --with-shadow --with-welcomemsg --with-throttling --with-uploadscript --with-language=simplified-chinese --with-rfc2640 --with-ftpwho --with-tls -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译配置失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make install -if [ ! -f "${pureftpdPath}/bin/pure-pw" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 修改 pure-ftpd 配置文件 -sed -i "s!# PureDB\s*/etc/pureftpd.pdb!PureDB ${pureftpdPath}/etc/pureftpd.pdb!" ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# ChrootEveryone\s*yes!ChrootEveryone yes!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!NoAnonymous\s*no!NoAnonymous yes!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!AnonymousCanCreateDirs\s*yes!AnonymousCanCreateDirs no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!AnonymousCantUpload\s*yes!AnonymousCantUpload no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!PAMAuthentication\s*yes!PAMAuthentication no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!UnixAuthentication\s*yes!UnixAuthentication no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# PassivePortRange\s*30000 50000!PassivePortRange 39000 40000!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!PassivePortRange\s*30000 50000!PassivePortRange 39000 40000!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!LimitRecursion\s*10000 8!LimitRecursion 20000 8!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# TLS!TLS!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i "s!# CertFile\s*/etc/ssl/private/pure-ftpd.pem!CertFile ${pureftpdPath}/etc/pure-ftpd.pem!" ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# Bind\s*127.0.0.1,21!Bind 0.0.0.0,21!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i "s!# PIDFile\s*/var/run/pure-ftpd.pid!PIDFile ${pureftpdPath}/etc/pure-ftpd.pid!" ${pureftpdPath}/etc/pure-ftpd.conf -touch ${pureftpdPath}/etc/pureftpd.passwd -touch ${pureftpdPath}/etc/pureftpd.pdb - -openssl dhparam -out ${pureftpdPath}/etc/pure-ftpd-dhparams.pem 2048 -openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -sha256 -keyout ${pureftpdPath}/etc/pure-ftpd.pem -out ${pureftpdPath}/etc/pure-ftpd.pem -subj "/C=CN/ST=Tianjin/L=Tianjin/O=HaoZi Technology Co., Ltd./OU=HaoZi Panel/CN=Panel" -chmod 600 ${pureftpdPath}/etc/*.pem - -# 添加系统服务 -ln -sf ${pureftpdPath}/bin/pure-pw /usr/bin/pure-pw - -cat > /etc/systemd/system/pure-ftpd.service << EOF -[Unit] -Description=Pure-FTPd FTP server -After=syslog.target network.target - -[Service] -Type=forking -PIDFile=${pureftpdPath}/etc/pure-ftpd.pid -ExecStart=${pureftpdPath}/sbin/pure-ftpd ${pureftpdPath}/etc/pure-ftpd.conf -ExecStartPost=/bin/sleep 2 -ExecStop=/bin/kill -TERM \$MAINPID - -[Install] -WantedBy=multi-user.target -EOF - -# 添加防火墙规则 -if [ "${OS}" == "centos" ]; then - firewall-cmd --zone=public --add-port=21/tcp --permanent - firewall-cmd --zone=public --add-port=39000-40000/tcp --permanent - firewall-cmd --reload -elif [ "${OS}" == "debian" ]; then - ufw allow 21/tcp - ufw allow 39000:40000/tcp - ufw reload -fi - -systemctl daemon-reload -systemctl enable pure-ftpd.service -systemctl start pure-ftpd.service - -panel writePlugin pureftpd 1.0.50 - -echo -e "${HR}\nPure-Ftpd-${pureftpdVersion} 安装完成\n${HR}" diff --git a/scripts/pureftpd/uninstall.sh b/scripts/pureftpd/uninstall.sh deleted file mode 100644 index 34067713..00000000 --- a/scripts/pureftpd/uninstall.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -systemctl stop pure-ftpd -systemctl disable pure-ftpd -rm -f /etc/systemd/system/pure-ftpd.service -systemctl daemon-reload -pkill -9 pure-ftpd -rm -rf /www/server/pure-ftpd -rm -f /usr/bin/pure-pw - -panel deletePlugin pureftpd - -echo -e "${HR}\nPure-Ftpd 卸载完成\n${HR}" diff --git a/scripts/pureftpd/update.sh b/scripts/pureftpd/update.sh deleted file mode 100644 index f4fa90c2..00000000 --- a/scripts/pureftpd/update.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/pure-ftpd" -setupPath="/www" -pureftpdPath="${setupPath}/server/pure-ftpd" -pureftpdVersion="1.0.50" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 准备安装目录 -cp ${pureftpdPath}/etc/pureftpd.passwd /tmp/pureftpd.passwd -cp ${pureftpdPath}/etc/pureftpd.pdb /tmp/pureftpd.pdb -cp ${pureftpdPath}/etc/pureftpd.conf /tmp/pureftpd.conf -systemctl stop pure-ftpd.service -rm -rf ${pureftpdPath} -mkdir -p ${pureftpdPath} -cd ${pureftpdPath} - -wget -T 60 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz -wget -T 20 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${pureftpdPath} - exit 1 -fi - -tar -xvf pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt -mv pure-ftpd-${pureftpdVersion} src -cd src - -./configure --prefix=${pureftpdPath} CFLAGS=-O2 --with-puredb --with-quotas --with-cookie --with-virtualhosts --with-diraliases --with-sysquotas --with-ratios --with-altlog --with-paranoidmsg --with-shadow --with-welcomemsg --with-throttling --with-uploadscript --with-language=simplified-chinese --with-rfc2640 --with-ftpwho --with-tls -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译配置失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make install -if [ ! -f "${pureftpdPath}/bin/pure-pw" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 还原配置 -cp /tmp/pureftpd.passwd ${pureftpdPath}/etc/pureftpd.passwd -cp /tmp/pureftpd.pdb ${pureftpdPath}/etc/pureftpd.pdb -cp /tmp/pureftpd.conf ${pureftpdPath}/etc/pureftpd.conf - -systemctl start pure-ftpd.service - -panel writePlugin pureftpd 1.0.50 - -echo -e "${HR}\nPure-Ftpd-${pureftpdVersion} 升级完成\n${HR}" diff --git a/scripts/redis/install.sh b/scripts/redis/install.sh deleted file mode 100644 index 98e20459..00000000 --- a/scripts/redis/install.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/redis" -setupPath="/www" -redisPath="${setupPath}/server/redis" -redisVersion="7.2.5" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -if ! id -u "redis" > /dev/null 2>&1; then - groupadd redis - useradd -s /sbin/nologin -g redis redis -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install systemd-devel openssl-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential libsystemd-dev libssl-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 准备目录 -rm -rf ${redisPath} -mkdir -p ${redisPath} -cd ${redisPath} - -# 下载源码 -wget -T 120 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz ${downloadUrl}/redis-${redisVersion}.tar.gz -wget -T 20 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz.checksum.txt ${downloadUrl}/redis-${redisVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c redis-${redisVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Redis源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${redisPath} - exit 1 -fi - -tar -zxvf redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz.checksum.txt -mv redis-${redisVersion}/* ./ && rm -rf redis-${redisVersion} -mkdir -p ${redisPath}/bin - -make BUILD_TLS=yes USE_SYSTEMD=yes -j${cpuCore} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Redis编译失败,请截图错误信息寻求帮助。" - rm -rf ${redisPath} - exit 1 -fi -make PREFIX=${redisPath} install -if [ ! -f "${redisPath}/bin/redis-server" ]; then - echo -e $HR - echo "错误:Redis安装失败,请截图错误信息寻求帮助。" - rm -rf ${redisPath} - exit 1 -fi - -# 设置软链接 -ln -sf ${redisPath}/bin/redis-cli /usr/bin/redis-cli -ln -sf ${redisPath}/bin/redis-server /usr/bin/redis-server -ln -sf ${redisPath}/bin/redis-sentinel /usr/bin/redis-sentinel -ln -sf ${redisPath}/bin/redis-benchmark /usr/bin/redis-benchmark -ln -sf ${redisPath}/bin/redis-check-aof /usr/bin/redis-check-aof -ln -sf ${redisPath}/bin/redis-check-rdb /usr/bin/redis-check-rdb - -# 设置配置文件 -VM_OVERCOMMIT_MEMORY=$(cat /etc/sysctl.conf|grep vm.overcommit_memory) -NET_CORE_SOMAXCONN=$(cat /etc/sysctl.conf|grep net.core.somaxconn) -if [ -z "${VM_OVERCOMMIT_MEMORY}" ] && [ -z "${NET_CORE_SOMAXCONN}" ];then - echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf - echo "net.core.somaxconn = 1024" >> /etc/sysctl.conf - sysctl -p -fi - -sed -i 's/dir .\//dir \/www\/server\/redis\//g' ${redisPath}/redis.conf -sed -i 's/# supervised.*/supervised systemd/g' ${redisPath}/redis.conf -sed -i 's/daemonize.*/daemonize no/g' ${redisPath}/redis.conf - -if [ "${ARCH}" == "aarch64" ]; then - echo "ignore-warnings ARM64-COW-BUG" >> ${redisPath}/redis.conf -fi - -chown -R redis:redis ${redisPath} -chmod -R 755 ${redisPath} - -# 设置服务 -cp -r utils/systemd-redis_server.service /etc/systemd/system/redis.service -sed -i "s!ExecStart=.*!ExecStart=${redisPath}/bin/redis-server ${redisPath}/redis.conf!g" /etc/systemd/system/redis.service -sed -i "s!#User=.*!User=redis!g" /etc/systemd/system/redis.service -sed -i "s!#Group=.*!Group=redis!g" /etc/systemd/system/redis.service -sed -i "s!#WorkingDirectory=.*!WorkingDirectory=${redisPath}!g" /etc/systemd/system/redis.service - -systemctl daemon-reload -systemctl enable redis -systemctl start redis - -panel writePlugin redis ${redisVersion} - -echo -e "${HR}\nRedis 安装完成\n${HR}" diff --git a/scripts/redis/uninstall.sh b/scripts/redis/uninstall.sh deleted file mode 100644 index 079b6bc4..00000000 --- a/scripts/redis/uninstall.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop redis -systemctl disable redis -rm -rf /etc/systemd/system/redis.service -systemctl daemon-reload -pkill -9 redis -rm -rf /www/server/redis - -rm -rf /usr/bin/redis-cli -rm -rf /usr/bin/redis-server -rm -rf /usr/bin/redis-benchmark -rm -rf /usr/bin/redis-check-aof -rm -rf /usr/bin/redis-check-rdb -rm -rf /usr/bin/redis-sentinel - -panel deletePlugin redis - -echo -e "${HR}\nRedis 卸载完成\n${HR}" diff --git a/scripts/redis/update.sh b/scripts/redis/update.sh deleted file mode 100644 index 326b287a..00000000 --- a/scripts/redis/update.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/redis" -setupPath="/www" -redisPath="${setupPath}/server/redis" -redisVersion="7.2.5" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -# 读取信息 -redisConfig="${redisPath}/redis.conf" -redisPort=$(cat ${redisConfig} | grep 'port ' | grep -v '#' | awk '{print $2}') -redisPass=$(cat ${redisConfig} | grep 'requirepass ' | grep -v '#' | awk '{print $2}') -redisHost=$(cat ${redisConfig} | grep 'bind ' | grep -v '#' | awk '{print $2}') -redisDir=$(cat ${redisConfig} | grep 'dir ' | grep -v '#' | awk '{print $2}') - -# 备份 -if [ -z "${redisPass}" ]; then - redis-cli -p ${redisPort} << EOF -SAVE -EOF -else - redis-cli -p ${redisPort} -a ${redisPass} << EOF -SAVE -EOF -fi -mv ${redisDir}/dump.rdb /tmp/dump.rdb.bak - -# 准备目录 -rm -rf ${redisPath} -mkdir -p ${redisPath} -cd ${redisPath} - -# 下载源码 -wget -T 120 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz ${downloadUrl}/redis-${redisVersion}.tar.gz -wget -T 20 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz.checksum.txt ${downloadUrl}/redis-${redisVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c redis-${redisVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Redis源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${redisPath} - exit 1 -fi - -tar -zxvf redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz.checksum.txt -mv redis-${redisVersion}/* ./ && rm -rf redis-${redisVersion} -mkdir -p ${redisPath}/bin - -make BUILD_TLS=yes USE_SYSTEMD=yes -j${cpuCore} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Redis编译失败,请截图错误信息寻求帮助。" - exit 1 -fi -make PREFIX=${redisPath} install -if [ ! -f "${redisPath}/bin/redis-server" ]; then - echo -e $HR - echo "错误:Redis升级失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 设置软链接 -ln -sf ${redisPath}/bin/redis-cli /usr/bin/redis-cli -ln -sf ${redisPath}/bin/redis-server /usr/bin/redis-server -ln -sf ${redisPath}/bin/redis-sentinel /usr/bin/redis-sentinel -ln -sf ${redisPath}/bin/redis-benchmark /usr/bin/redis-benchmark -ln -sf ${redisPath}/bin/redis-check-aof /usr/bin/redis-check-aof -ln -sf ${redisPath}/bin/redis-check-rdb /usr/bin/redis-check-rdb - -# 设置配置文件 -sed -i 's/dir .\//dir \/www\/server\/redis\//g' ${redisPath}/redis.conf -sed -i 's/# supervised.*/supervised systemd/g' ${redisPath}/redis.conf -sed -i 's/daemonize.*/daemonize no/g' ${redisPath}/redis.conf - -if [ "${ARCH}" == "aarch64" ]; then - echo "ignore-warnings ARM64-COW-BUG" >> ${redisPath}/redis.conf -fi - -# 恢复配置 -if [ -n "${redisPass}" ]; then - sed -i "s!# requirepass .*!requirepass ${redisPass}!g" ${redisPath}/redis.conf -fi -if [ -n "${redisHost}" ]; then - sed -i "s!bind .*!bind ${redisHost}!g" ${redisPath}/redis.conf -fi -if [ -n "${redisPort}" ]; then - sed -i "s!port .*!port ${redisPort}!g" ${redisPath}/redis.conf -fi -if [ -n "${redisDir}" ]; then - sed -i "s!dir .*!dir ${redisDir}!g" ${redisPath}/redis.conf -fi - -# 恢复数据 -if [ -f "/tmp/dump.rdb.bak" ]; then - mv /tmp/dump.rdb.bak ${redisDir}/dump.rdb -fi - -chown -R redis:redis ${redisPath} -chmod -R 755 ${redisPath} - -# 设置服务 -cp -r utils/systemd-redis_server.service /etc/systemd/system/redis.service -sed -i "s!ExecStart=.*!ExecStart=${redisPath}/bin/redis-server ${redisPath}/redis.conf!g" /etc/systemd/system/redis.service -sed -i "s!#User=.*!User=redis!g" /etc/systemd/system/redis.service -sed -i "s!#Group=.*!Group=redis!g" /etc/systemd/system/redis.service -sed -i "s!#WorkingDirectory=.*!WorkingDirectory=${redisPath}!g" /etc/systemd/system/redis.service - -systemctl daemon-reload -systemctl restart redis - -panel writePlugin redis ${redisVersion} - -echo -e "${HR}\nRedis 升级完成\n${HR}" diff --git a/scripts/rsync/install.sh b/scripts/rsync/install.sh deleted file mode 100644 index 680e9785..00000000 --- a/scripts/rsync/install.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y rsync -elif [ "${OS}" == "debian" ]; then - apt-get install -y rsync -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 写入配置 -cat > /etc/rsyncd.conf << EOF -uid = root -gid = root -port = 873 -use chroot = no -read only = no -dont compress = *.jpg *.jpeg *.png *.gif *.webp *.avif *.mp4 *.avi *.mov *.mkv *.mp3 *.wav *.aac *.flac *.zip *.rar *.7z *.gz *.tgz *.tar *.pdf *.epub *.iso *.exe *.apk *.dmg *.rpm *.deb *.msi -hosts allow = 127.0.0.1/32 ::1/128 -# hosts deny = -max connections = 100 -timeout = 1800 -lock file = /var/run/rsync.lock -pid file = /var/run/rsyncd.pid -log file = /var/log/rsyncd.log - -EOF - -touch /etc/rsyncd.secrets -chmod 644 /etc/rsyncd.conf -chmod 600 /etc/rsyncd.secrets - -# 写入服务文件 -cat > /etc/systemd/system/rsyncd.service << EOF -[Unit] -Description=fast remote file copy program daemon -After=network-online.target remote-fs.target nss-lookup.target -Wants=network-online.target -ConditionPathExists=/etc/rsyncd.conf - -[Service] -ExecStart=/usr/bin/rsync --daemon --no-detach "\$OPTIONS" -ExecReload=/bin/kill -HUP \$MAINPID -KillMode=process -Restart=on-failure -RestartSec=5s - -[Install] -WantedBy=multi-user.target -EOF - -systemctl daemon-reload -systemctl enable rsyncd.service -systemctl restart rsyncd.service - -panel writePlugin rsync 3.2.7 diff --git a/scripts/rsync/uninstall.sh b/scripts/rsync/uninstall.sh deleted file mode 100644 index 912f38de..00000000 --- a/scripts/rsync/uninstall.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -systemctl stop rsync -systemctl disable rsync -rm -f /etc/systemd/system/rsyncd.service -systemctl daemon-reload - -if [ "${OS}" == "centos" ]; then - dnf remove -y rsync -elif [ "${OS}" == "debian" ]; then - apt-get purge -y rsync -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -# 删除配置 -rm -rf /etc/rsyncd.conf -rm -rf /etc/rsyncd.secrets - -panel deletePlugin rsync diff --git a/scripts/rsync/update.sh b/scripts/rsync/update.sh deleted file mode 100644 index b1ffe37d..00000000 --- a/scripts/rsync/update.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y rsync -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y rsync -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel writePlugin rsync 3.2.7 diff --git a/scripts/s3fs/install.sh b/scripts/s3fs/install.sh deleted file mode 100644 index 642998d1..00000000 --- a/scripts/s3fs/install.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y s3fs-fuse -elif [ "${OS}" == "debian" ]; then - apt-get install -y s3fs -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -panel writePlugin s3fs 1.9 diff --git a/scripts/s3fs/uninstall.sh b/scripts/s3fs/uninstall.sh deleted file mode 100644 index 9d806a3e..00000000 --- a/scripts/s3fs/uninstall.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf remove -y s3fs-fuse -elif [ "${OS}" == "debian" ]; then - apt-get purge -y s3fs -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel deletePlugin s3fs diff --git a/scripts/s3fs/update.sh b/scripts/s3fs/update.sh deleted file mode 100644 index da39845b..00000000 --- a/scripts/s3fs/update.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y s3fs-fuse -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y s3fs -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel writePlugin s3fs 1.9 diff --git a/scripts/supervisor/install.sh b/scripts/supervisor/install.sh deleted file mode 100644 index 466eff31..00000000 --- a/scripts/supervisor/install.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y supervisor - sed -i 's#files = supervisord.d/\*.ini#files = supervisord.d/*.conf#g' /etc/supervisord.conf - systemctl enable supervisord - systemctl start supervisord -elif [ "${OS}" == "debian" ]; then - apt-get install -y supervisor - systemctl enable supervisor - systemctl start supervisor -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -panel writePlugin supervisor 4.2.5 diff --git a/scripts/supervisor/uninstall.sh b/scripts/supervisor/uninstall.sh deleted file mode 100644 index 797dff7e..00000000 --- a/scripts/supervisor/uninstall.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - systemctl stop supervisord - systemctl disable supervisord - dnf remove -y supervisor -elif [ "${OS}" == "debian" ]; then - systemctl stop supervisor - systemctl disable supervisor - apt-get purge -y supervisor -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel deletePlugin supervisor diff --git a/scripts/supervisor/update.sh b/scripts/supervisor/update.sh deleted file mode 100644 index e56b0169..00000000 --- a/scripts/supervisor/update.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y supervisor -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y supervisor -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel writePlugin supervisor 4.2.5 diff --git a/scripts/uninstall_panel.sh b/scripts/uninstall_panel.sh deleted file mode 100644 index 436a78bb..00000000 --- a/scripts/uninstall_panel.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -LOGO="+----------------------------------------------------\n| 耗子面板卸载脚本\n+----------------------------------------------------\n| Copyright © 2022-"$(date +%Y)" 耗子科技 All rights reserved.\n+----------------------------------------------------" -HR="+----------------------------------------------------" -download_Url="" -setup_Path="/www" - -Prepare_System() { - if [ $(whoami) != "root" ]; then - echo -e $HR - echo "错误:请使用root用户运行卸载命令。" - exit 1 - fi - - isInstalled=$(systemctl status panel 2>&1 | grep "Active") - if [ "${isInstalled}" == "" ]; then - echo -e $HR - echo "错误:耗子面板未安装,无需卸载。" - exit 1 - fi - - if ! id -u "www" > /dev/null 2>&1; then - groupadd www - useradd -s /sbin/nologin -g www www - fi -} - -Remove_Swap() { - swapFile="${setup_Path}/swap" - if [ -f "${swapFile}" ]; then - swapoff ${swapFile} - rm -f ${swapFile} - sed -i '/swap/d' /etc/fstab - fi - - mount -a - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:检测到系统的 /etc/fstab 文件配置有误,请检查排除后重试,问题解决前勿重启系统。" - exit 1 - fi -} - -Remove_Panel() { - systemctl stop panel - systemctl disable panel - rm -f /etc/systemd/system/panel.service - rm -f /usr/bin/panel - rm -rf ${setup_Path} -} - -clear -echo -e "${LOGO}" - -# 卸载确认 -echo -e "高危操作,卸载面板前请务必备份好所有数据,提前卸载面板所有插件!" -echo -e "卸载面板后,所有数据将被清空,无法恢复!" -read -r -p "输入 y 并回车以确认卸载面板:" uninstall -if [ "${uninstall}" != 'y' ]; then - echo "输入不正确,已退出卸载。" - exit -fi - -echo -e "${LOGO}" -echo '正在卸载耗子面板...' -echo -e $HR - -Prepare_System -Remove_Swap -Remove_Panel - -clear - -echo -e "${LOGO}" -echo '耗子面板卸载完成。' -echo '感谢您的使用,欢迎您再次使用耗子面板。' -echo -e $HR - -rm -f uninstall_panel.sh -rm -f uninstall_panel.sh.checksum.txt diff --git a/scripts/update_panel.sh b/scripts/update_panel.sh deleted file mode 100644 index ff27a18e..00000000 --- a/scripts/update_panel.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -if [ "${OS}" == "unknown" ]; then - echo -e $HR - echo "错误:该系统不支持安装耗子面板,请更换 Debian 12.x / RHEL 9.x 安装。" - exit 1 -fi - -oldVersion=$(panel getSetting version) -oldVersion=${oldVersion#v} -panelPath="/www/panel" - -# 大于 -function version_gt() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -V | head -n 1)" != "$1"; } -# 小于 -function version_lt() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -rV | head -n 1)" != "$1"; } -# 大于等于 -function version_ge() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -rV | head -n 1)" == "$1"; } -# 小于等于 -function version_le() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -V | head -n 1)" == "$1"; } - -if [ -z "$oldVersion" ]; then - if [ -f "$panelPath/database/panel.db" ]; then - echo "DB_FILE=$panelPath/database/panel.db" >> $panelPath/panel.conf - oldVersion=$(panel getSetting version) - oldVersion=${oldVersion#v} - sed -i '/DB_FILE/d' $panelPath/panel.conf - else - echo "错误:无法获取面板版本" - echo "Error: can't get panel version" - exit 1 - fi -fi - -# 判断版本号是否合法 -versionPattern="^[0-9]+\.[0-9]+\.[0-9]+$" -if [[ ! $oldVersion =~ $versionPattern ]]; then - if [ -f "$panelPath/database/panel.db" ]; then - echo "DB_FILE=$panelPath/database/panel.db" >> $panelPath/panel.conf - oldVersion=$(panel getSetting version) - oldVersion=${oldVersion#v} - sed -i '/DB_FILE/d' $panelPath/panel.conf - else - echo "错误:面板版本号不合法" - echo "Error: panel version is illegal" - exit 1 - fi -fi - -echo $HR - -if version_lt "$oldVersion" "2.1.8"; then - echo "更新面板到 v2.1.8 ..." - echo "Update panel to v2.1.8 ..." - oldEntrance=$(panel getSetting entrance) - echo "APP_ENTRANCE=$oldEntrance" >> $panelPath/panel.conf - panel deleteSetting entrance -fi - -if version_lt "$oldVersion" "2.1.30"; then - echo "更新面板到 v2.1.30 ..." - echo "Update panel to v2.1.30 ..." - sed -i '/APP_HOST/d' $panelPath/panel.conf - echo "APP_SSL=false" >> $panelPath/panel.conf - mv $panelPath/database/panel.db $panelPath/storage/panel.db - openssl req -x509 -nodes -days 36500 -newkey ec:<(openssl ecparam -name secp384r1) -keyout $panelPath/storage/ssl.key -out $panelPath/storage/ssl.crt -subj "/C=CN/ST=Tianjin/L=Tianjin/O=HaoZi Technology Co., Ltd./OU=HaoZi Panel/CN=Panel" -fi - -if version_lt "$oldVersion" "2.2.0"; then - echo "更新面板到 v2.2.0 ..." - echo "Update panel to v2.2.0 ..." - echo "APP_LOCALE=zh_CN" >> $panelPath/panel.conf -fi - -if version_lt "$oldVersion" "2.2.4"; then - echo "更新面板到 v2.2.4 ..." - echo "Update panel to v2.2.4 ..." - if [ "${OS}" == "centos" ]; then - dnf makecache - dnf install -y p7zip p7zip-plugins rsyslog - systemctl enable rsyslog - systemctl start rsyslog - else - apt-get update -y - apt-get install -y p7zip p7zip-full - fi -fi - -if version_lt "$oldVersion" "2.2.10"; then - echo "更新面板到 v2.2.10 ..." - echo "Update panel to v2.2.10 ..." - if [ -f "/usr/bin/podman" ]; then - panel writePlugin podman 4.0.0 - if [ "${OS}" == "debian" ]; then - apt-get install containers-storage -y - cp /usr/share/containers/storage.conf /etc/containers/storage.conf - fi - systemctl enable podman - systemctl enable podman.socket - systemctl enable podman-restart - systemctl start podman - systemctl start podman.socket - systemctl start podman-restart - fi -fi - -if version_lt "$oldVersion" "2.2.14"; then - echo "更新面板到 v2.2.14 ..." - echo "Update panel to v2.2.14 ..." - if [ -f "/www/server/openresty/bin/openresty" ]; then - mkdir -p /www/server/vhost/acme - chmod -R 644 /www/server/vhost/acme - fi -fi - -if version_lt "$oldVersion" "2.2.16"; then - echo "更新面板到 v2.2.16 ..." - echo "Update panel to v2.2.16 ..." - if [ -f "/www/server/mysql/bin/mysql" ]; then - ln -sf /www/server/mysql/bin/* /usr/bin/ - rm -f /etc/profile.d/mysql.sh - source /etc/profile - fi -fi - -if version_lt "$oldVersion" "2.2.20"; then - echo "更新面板到 v2.2.20 ..." - echo "Update panel to v2.2.20 ..." - echo "SESSION_LIFETIME=120" >> $panelPath/panel.conf -fi - -echo $HR -echo "更新结束" -echo "Update finished" diff --git a/storage/.gitignore b/storage/.gitignore deleted file mode 100644 index 03e21671..00000000 --- a/storage/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -panel.db -cert.pem -key.pem \ No newline at end of file diff --git a/storage/README.md b/storage/README.md new file mode 100644 index 00000000..47cabd95 --- /dev/null +++ b/storage/README.md @@ -0,0 +1,3 @@ +# storage + +storage 目录存放应用运行时产生的文件,如上传的文件、缓存文件等。 \ No newline at end of file diff --git a/app/http/middleware/.gitignore b/storage/logs/.gitkeep similarity index 100% rename from app/http/middleware/.gitignore rename to storage/logs/.gitkeep diff --git a/storage/sessions/.gitkeep b/storage/sessions/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/storage/temp/.gitignore b/storage/temp/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/storage/temp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/tests/setting/setting_test.go b/tests/setting/setting_test.go deleted file mode 100644 index b34629c3..00000000 --- a/tests/setting/setting_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package setting - -import ( - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/tests" -) - -type SettingTestSuite struct { - suite.Suite - tests.TestCase - setting internal.Setting -} - -func TestSettingTestSuite(t *testing.T) { - suite.Run(t, &SettingTestSuite{ - setting: services.NewSettingImpl(), - }) -} - -func (s *SettingTestSuite) SetupTest() { - -} - -func (s *SettingTestSuite) TestGet() { - a := s.setting.Get("test") - b := s.setting.Get("test", "test") - s.Equal("", a) - s.Equal("test", b) -} - -func (s *SettingTestSuite) TestSet() { - err := s.setting.Set("test", "test") - s.Nil(err) - err = s.setting.Delete("test") - s.Nil(err) -} - -func (s *SettingTestSuite) TestDelete() { - err := s.setting.Set("test", "test") - s.Nil(err) - err = s.setting.Delete("test") - s.Nil(err) -} diff --git a/tests/test_case.go b/tests/test_case.go deleted file mode 100644 index 9ec12946..00000000 --- a/tests/test_case.go +++ /dev/null @@ -1,15 +0,0 @@ -package tests - -import ( - "github.com/goravel/framework/testing" - - "github.com/TheTNB/panel/v2/bootstrap" -) - -func init() { - bootstrap.Boot() -} - -type TestCase struct { - testing.TestCase -} diff --git a/tests/user/user_test.go b/tests/user/user_test.go deleted file mode 100644 index f29461f6..00000000 --- a/tests/user/user_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package user - -import ( - "testing" - - "github.com/goravel/framework/facades" - "github.com/stretchr/testify/suite" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/tests" -) - -type UserTestSuite struct { - suite.Suite - tests.TestCase - user internal.User -} - -func TestUserTestSuite(t *testing.T) { - suite.Run(t, &UserTestSuite{ - user: services.NewUserImpl(), - }) -} - -func (s *UserTestSuite) SetupTest() { - -} - -func (s *UserTestSuite) TestCreate() { - user, err := s.user.Create("haozi", "123456") - s.Nil(err) - s.Equal("haozi", user.Username) - _, err = facades.Orm().Query().Where("username", "haozi").Delete(&models.User{}) - s.Nil(err) -} - -func (s *UserTestSuite) TestUpdate() { - user, err := s.user.Create("haozi", "123456") - s.Nil(err) - s.Equal("haozi", user.Username) - user.Username = "haozi2" - user, err = s.user.Update(user) - s.Nil(err) - s.Equal("haozi2", user.Username) - _, err = facades.Orm().Query().Where("username", "haozi").Delete(&models.User{}) - s.Nil(err) -} diff --git a/web/.env.development b/web/.env.development new file mode 100644 index 00000000..7714a753 --- /dev/null +++ b/web/.env.development @@ -0,0 +1,17 @@ +VITE_APP_TITLE = '耗子面板' + +# 资源公共路径,需要以 /开头和结尾 +VITE_PUBLIC_PATH = '/' + +# 是否hash路由模式 +VITE_USE_HASH = false + +# axios base api +VITE_BASE_API = '/api' + +# 是否启用代理(只对本地vite server生效) +VITE_USE_PROXY = true + +# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod' +VITE_PROXY_TYPE = 'dev' + diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 00000000..75a18902 --- /dev/null +++ b/web/.env.production @@ -0,0 +1,20 @@ +VITE_APP_TITLE = '耗子面板' + +# 资源公共路径,需要以 /开头和结尾 +VITE_PUBLIC_PATH = '/' + +# 是否hash路由模式 +VITE_USE_HASH = false + +# base api +VITE_BASE_API = '/api' + +# 是否启用压缩 +VITE_USE_COMPRESS = false + +# 压缩类型 +VITE_COMPRESS_TYPE = gzip + + + + diff --git a/web/.eslintrc-auto-import.json b/web/.eslintrc-auto-import.json new file mode 100644 index 00000000..ed79adcd --- /dev/null +++ b/web/.eslintrc-auto-import.json @@ -0,0 +1,76 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": true, + "WritableComputedRef": true, + "computed": true, + "createApp": true, + "customRef": true, + "defineAsyncComponent": true, + "defineComponent": true, + "effectScope": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "inject": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onUnmounted": true, + "onUpdated": true, + "provide": true, + "reactive": true, + "readonly": true, + "ref": true, + "resolveComponent": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "toRaw": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "unref": true, + "useAttrs": true, + "useCssModule": true, + "useCssVars": true, + "useLink": true, + "useRoute": true, + "useRouter": true, + "useSlots": true, + "watch": true, + "watchEffect": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "onWatcherCleanup": true, + "useId": true, + "useModel": true, + "useTemplateRef": true + } +} diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 00000000..5528a658 --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,17 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + extends: [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript', + '@vue/eslint-config-prettier/skip-formatting', + '@unocss', + '.eslintrc-auto-import.json' + ], + parserOptions: { + ecmaVersion: 'latest' + } +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 00000000..531b8e9e --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + + +# Editor directories and files +.vscode +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.env + +stats.html + +types/components.d.ts +types/auto-imports.d.ts diff --git a/web/.prettierrc.json b/web/.prettierrc.json new file mode 100644 index 00000000..66e23359 --- /dev/null +++ b/web/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "tabWidth": 2, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "none" +} \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..6c7acc25 --- /dev/null +++ b/web/README.md @@ -0,0 +1,11 @@ +# 耗子面板 + +这是耗子面板的前端部分,使用 Vue3 + Vite + UnoCSS + Naive UI 开发。 + +预了解更多请移步 [耗子面板](https://github.com/TheTNB/panel)。 + +# Rat Panel + +This is the frontend part of Rat Panel, developed using Vue3 + Vite + UnoCSS + Naive UI. + +For more information, please visit [Rat Panel](https://github.com/TheTNB/panel). diff --git a/web/build/config/define.ts b/web/build/config/define.ts new file mode 100644 index 00000000..9f5d81b7 --- /dev/null +++ b/web/build/config/define.ts @@ -0,0 +1,13 @@ +import dayjs from 'dayjs' + +/** + * * 此处定义的是全局常量,启动或打包后将添加到window中 + * https://vitejs.cn/config/#define + */ + +// 项目构建时间 +const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss')) + +export const viteDefine = { + _BUILD_TIME_ +} diff --git a/web/build/config/index.ts b/web/build/config/index.ts new file mode 100644 index 00000000..967ddda3 --- /dev/null +++ b/web/build/config/index.ts @@ -0,0 +1,2 @@ +export * from './define' +export * from './proxy' diff --git a/web/build/config/proxy.ts b/web/build/config/proxy.ts new file mode 100644 index 00000000..3c89f9c0 --- /dev/null +++ b/web/build/config/proxy.ts @@ -0,0 +1,16 @@ +import type { ProxyOptions } from 'vite' +import { getProxyConfig } from '../../settings/proxy-config' + +export function createViteProxy(isUseProxy = true, proxyType: ProxyType) { + if (!isUseProxy) return undefined + + const proxyConfig = getProxyConfig(proxyType) + const proxy: Record = { + [proxyConfig.prefix]: { + target: proxyConfig.target, + changeOrigin: true, + rewrite: (path: string) => path.replace(new RegExp(`^${proxyConfig.prefix}`), '') + } + } + return proxy +} diff --git a/web/build/plugins/html.ts b/web/build/plugins/html.ts new file mode 100644 index 00000000..e92cb54f --- /dev/null +++ b/web/build/plugins/html.ts @@ -0,0 +1,16 @@ +import { createHtmlPlugin } from 'vite-plugin-html' + +export function setupHtmlPlugin(viteEnv: ViteEnv) { + const { VITE_APP_TITLE } = viteEnv + + const htmlPlugin = createHtmlPlugin({ + minify: true, + inject: { + data: { + title: VITE_APP_TITLE + } + }, + viteNext: true + }) + return htmlPlugin +} diff --git a/web/build/plugins/index.ts b/web/build/plugins/index.ts new file mode 100644 index 00000000..cadc8b9e --- /dev/null +++ b/web/build/plugins/index.ts @@ -0,0 +1,28 @@ +import type { PluginOption } from 'vite' +import vue from '@vitejs/plugin-vue' +import unocss from 'unocss/vite' +import { visualizer } from 'rollup-plugin-visualizer' +import viteCompression from 'vite-plugin-compression' + +import unplugins from './unplugin' +import { setupHtmlPlugin } from './html' + +export function setupVitePlugins(viteEnv: ViteEnv, isBuild: boolean): PluginOption[] { + const plugins = [vue(), ...unplugins, unocss(), setupHtmlPlugin(viteEnv)] + + if (viteEnv.VITE_USE_COMPRESS) { + plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' })) + } + + if (isBuild) { + plugins.push( + visualizer({ + open: true, + gzipSize: true, + brotliSize: true + }) + ) + } + + return plugins +} diff --git a/web/build/plugins/unplugin.ts b/web/build/plugins/unplugin.ts new file mode 100644 index 00000000..cc0534be --- /dev/null +++ b/web/build/plugins/unplugin.ts @@ -0,0 +1,33 @@ +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' + +/** + * * unplugin-icons插件,自动引入iconify图标 + * usage: https://github.com/antfu/unplugin-icons + * 图标库: https://icones.js.org/ + */ +import Icons from 'unplugin-icons/vite' +import IconsResolver from 'unplugin-icons/resolver' + +export default [ + AutoImport({ + imports: ['vue', 'vue-router'], + dts: 'types/auto-imports.d.ts', + eslintrc: { + enabled: true + } + }), + Icons({ + compiler: 'vue3', + scale: 1, + defaultClass: 'inline-block' + }), + Components({ + resolvers: [ + NaiveUiResolver(), + IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }) + ], + dts: 'types/components.d.ts' + }) +] diff --git a/web/build/utils.ts b/web/build/utils.ts new file mode 100644 index 00000000..ccdc54c5 --- /dev/null +++ b/web/build/utils.ts @@ -0,0 +1,38 @@ +import path from 'node:path' + +/** + * * 项目根路径 + * @descrition 结尾不带/ + */ +export function getRootPath() { + return path.resolve(process.cwd()) +} + +/** + * * 项目src路径 + * @param srcName src目录名称(默认: "src") + * @descrition 结尾不带斜杠 + */ +export function getSrcPath(srcName = 'src') { + return path.resolve(getRootPath(), srcName) +} + +/** + * * 转换env配置 + * @param envOptions + * @descrition boolean和数字类型转换 + */ +export function convertEnv(envOptions: Record): ViteEnv { + const result: any = {} + if (!envOptions) return result + + for (const envKey in envOptions) { + let envVal = envOptions[envKey] + if (['true', 'false'].includes(envVal)) envVal = envVal === 'true' + + if (['VITE_PORT'].includes(envKey)) envVal = +envVal + + result[envKey] = envVal + } + return result +} diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 00000000..d4945dbc --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..fbb3111b --- /dev/null +++ b/web/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + <%= title %> + + +
+ +
+ logo +
+
+
+
+
+
+
+
+
<%= title %>
+
+ +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000..43daf4cf --- /dev/null +++ b/web/package.json @@ -0,0 +1,72 @@ +{ + "private": true, + "type": "module", + "repository": { + "url": "https://github.com/TheTNB/panel-frontend" + }, + "license": "MIT", + "author": { + "name": "HaoZi Tech", + "email": "admin@haozi.net", + "url": "https://git.haozi.net/org" + }, + "scripts": { + "dev": "vite", + "build": "run-p type-check build-only", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/" + }, + "dependencies": { + "@guolao/vue-monaco-editor": "^1.5.4", + "@vueuse/core": "^11.0.3", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "axios": "^1.7.7", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", + "echarts": "^5.5.1", + "install": "^0.13.0", + "lodash-es": "^4.17.21", + "monaco-editor-nginx": "^2.0.2", + "pinia": "^2.2.2", + "vue": "^3.5.5", + "vue-echarts": "^7.0.3", + "vue-i18n": "^10.0.1", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@iconify/json": "^2.2.248", + "@iconify/vue": "^4.1.2", + "@rushstack/eslint-patch": "^1.10.4", + "@tsconfig/node20": "^20.1.4", + "@types/crypto-js": "^4.2.2", + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.16.5", + "@unocss/eslint-config": "^0.62.3", + "@vitejs/plugin-vue": "^5.1.3", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/tsconfig": "^0.5.1", + "colord": "^2.9.3", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.28.0", + "naive-ui": "^2.39.0", + "npm-run-all2": "^6.2.3", + "prettier": "^3.3.3", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.78.0", + "typescript": "^5.5.4", + "unocss": "^0.62.3", + "unplugin-auto-import": "^0.18.3", + "unplugin-icons": "^0.19.3", + "unplugin-vue-components": "^0.27.4", + "vite": "^5.4.5", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-html": "^3.2.2", + "vite-plugin-mock": "^3.0.2", + "vue-tsc": "^2.1.6" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 00000000..6da11660 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,4618 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@guolao/vue-monaco-editor': + specifier: ^1.5.4 + version: 1.5.4(monaco-editor@0.51.0)(vue@3.5.6(typescript@5.6.2)) + '@vueuse/core': + specifier: ^11.0.3 + version: 11.1.0(vue@3.5.6(typescript@5.6.2)) + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 + axios: + specifier: ^1.7.7 + version: 1.7.7 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + echarts: + specifier: ^5.5.1 + version: 5.5.1 + install: + specifier: ^0.13.0 + version: 0.13.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + monaco-editor-nginx: + specifier: ^2.0.2 + version: 2.0.2(@babel/runtime@7.25.6)(@nginx/reference-lib@1.1.2)(monaco-editor@0.51.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pinia: + specifier: ^2.2.2 + version: 2.2.2(typescript@5.6.2)(vue@3.5.6(typescript@5.6.2)) + vue: + specifier: ^3.5.5 + version: 3.5.6(typescript@5.6.2) + vue-echarts: + specifier: ^7.0.3 + version: 7.0.3(@vue/runtime-core@3.5.6)(echarts@5.5.1)(vue@3.5.6(typescript@5.6.2)) + vue-i18n: + specifier: ^10.0.1 + version: 10.0.1(vue@3.5.6(typescript@5.6.2)) + vue-router: + specifier: ^4.4.5 + version: 4.4.5(vue@3.5.6(typescript@5.6.2)) + devDependencies: + '@iconify/json': + specifier: ^2.2.248 + version: 2.2.249 + '@iconify/vue': + specifier: ^4.1.2 + version: 4.1.2(vue@3.5.6(typescript@5.6.2)) + '@rushstack/eslint-patch': + specifier: ^1.10.4 + version: 1.10.4 + '@tsconfig/node20': + specifier: ^20.1.4 + version: 20.1.4 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/node': + specifier: ^20.16.5 + version: 20.16.5 + '@unocss/eslint-config': + specifier: ^0.62.3 + version: 0.62.4(eslint@8.57.0)(typescript@5.6.2) + '@vitejs/plugin-vue': + specifier: ^5.1.3 + version: 5.1.3(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))(vue@3.5.6(typescript@5.6.2)) + '@vue/eslint-config-prettier': + specifier: ^9.0.0 + version: 9.0.0(eslint@8.57.0)(prettier@3.3.3) + '@vue/eslint-config-typescript': + specifier: ^13.0.0 + version: 13.0.0(eslint-plugin-vue@9.28.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.6.2) + '@vue/tsconfig': + specifier: ^0.5.1 + version: 0.5.1 + colord: + specifier: ^2.9.3 + version: 2.9.3 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-plugin-vue: + specifier: ^9.28.0 + version: 9.28.0(eslint@8.57.0) + naive-ui: + specifier: ^2.39.0 + version: 2.39.0(vue@3.5.6(typescript@5.6.2)) + npm-run-all2: + specifier: ^6.2.3 + version: 6.2.3 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.12.0(rollup@4.21.3) + sass: + specifier: ^1.78.0 + version: 1.78.0 + typescript: + specifier: ^5.5.4 + version: 5.6.2 + unocss: + specifier: ^0.62.3 + version: 0.62.4(postcss@8.4.47)(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + unplugin-auto-import: + specifier: ^0.18.3 + version: 0.18.3(@vueuse/core@11.1.0(vue@3.5.6(typescript@5.6.2)))(rollup@4.21.3) + unplugin-icons: + specifier: ^0.19.3 + version: 0.19.3(@vue/compiler-sfc@3.5.6) + unplugin-vue-components: + specifier: ^0.27.4 + version: 0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.6(typescript@5.6.2)) + vite: + specifier: ^5.4.5 + version: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + vite-plugin-html: + specifier: ^3.2.2 + version: 3.2.2(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + vite-plugin-mock: + specifier: ^3.0.2 + version: 3.0.2(esbuild@0.23.1)(mockjs@1.1.0)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + vue-tsc: + specifier: ^2.1.6 + version: 2.1.6(typescript@5.6.2) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} + engines: {node: '>=6.9.0'} + + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==} + peerDependencies: + vue: ^3.0.11 + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.1': + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@guolao/vue-monaco-editor@1.5.4': + resolution: {integrity: sha512-eyBAqxJeDpV4mZYZSpNvh3xUgKCld5eEe0dBtjJhsy2+L0MB6PYFZ/FbPHNwskgp2RoIpfn1DLrIhXXE3lVbwQ==} + peerDependencies: + '@vue/composition-api': ^1.7.1 + monaco-editor: '>=0.43.0' + vue: ^2.6.14 || >=3.0.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@iconify/json@2.2.249': + resolution: {integrity: sha512-nOvcrdep4qB8L3WedGWC6238rU+oYfqLpTfQp8uV0Avxg7aXPvl9rGW0vnaq53exSgfvhQ1h7JcVUwJUuDHrzQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.1.33': + resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + + '@iconify/vue@4.1.2': + resolution: {integrity: sha512-CQnYqLiQD5LOAaXhBrmj1mdL2/NCJvwcC4jtW2Z8ukhThiFkLDkutarTOV2trfc9EXqUqRs0KqXOL9pZ/IyysA==} + peerDependencies: + vue: '>=3' + + '@intlify/core-base@10.0.1': + resolution: {integrity: sha512-6kpRGjhos95ph7QmEtP4tnWFTW102s71CLQAQwfsIGqOAcoJhzcYFpzIQ0gKXzqAIXsMD/hwM5qJ4ewqMHw3gg==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@10.0.1': + resolution: {integrity: sha512-fPeykrcgVT5eOIlshTHiPCN8FV3AZyBOdMS3XaXzfQ6eL5wqfc29I/EdIv5YXVW5X8e/BgYeWjBC0Cuznsl/2g==} + engines: {node: '>= 16'} + + '@intlify/shared@10.0.1': + resolution: {integrity: sha512-b4h7IWdZl710DnAhET8lgfgZ4Y9A2IZx/gbli3Ec/zHtYCoPqLHmiM7kUNBrSZj7d/SSjcMMZHuz5I09x3PYZw==} + engines: {node: '>= 16'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@monaco-editor/loader@1.4.0': + resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + + '@nginx/reference-lib@1.1.2': + resolution: {integrity: sha512-d8HeRHsIOPfVFOpDOi1XjiGxfYyyRmdPd+a+V9vDpyjBWGHVwSSOwzvuPGJtc4V4KK6ZfYHm15FD1etiKYS6vQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.25': + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.21.3': + resolution: {integrity: sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.21.3': + resolution: {integrity: sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.21.3': + resolution: {integrity: sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.21.3': + resolution: {integrity: sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.21.3': + resolution: {integrity: sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.21.3': + resolution: {integrity: sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.21.3': + resolution: {integrity: sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.21.3': + resolution: {integrity: sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.3': + resolution: {integrity: sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.21.3': + resolution: {integrity: sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.21.3': + resolution: {integrity: sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.21.3': + resolution: {integrity: sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.21.3': + resolution: {integrity: sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.21.3': + resolution: {integrity: sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.21.3': + resolution: {integrity: sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.21.3': + resolution: {integrity: sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==} + cpu: [x64] + os: [win32] + + '@rushstack/eslint-patch@1.10.4': + resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + + '@tsconfig/node20@20.1.4': + resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==} + + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.7': + resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + + '@types/node@20.16.5': + resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/scope-manager@8.6.0': + resolution: {integrity: sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/types@8.6.0': + resolution: {integrity: sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.6.0': + resolution: {integrity: sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/utils@8.6.0': + resolution: {integrity: sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/visitor-keys@8.6.0': + resolution: {integrity: sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@unocss/astro@0.62.4': + resolution: {integrity: sha512-98KfkbrNhBLx2+uYxMiGsldIeIZ6/PbL4yaGRHeHoiHd7p4HmIyCF+auYe4Psntx3Yr8kU+XSIAhGDYebvTidQ==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + vite: + optional: true + + '@unocss/cli@0.62.4': + resolution: {integrity: sha512-p4VyS40mzn4LCOkIsbIRzN0Zi50rRepesREi2S1+R4Kpvd4QFeeuxTuZNHEyi2uCboQ9ZWl1gfStCXIrNECwTg==} + engines: {node: '>=14'} + hasBin: true + + '@unocss/config@0.62.4': + resolution: {integrity: sha512-XKudKxxW8P44JvlIdS6HBpfE3qZA9rhbemy6/sb8HyZjKYjgeM9jx5yjk+9+4hXNma/KlwDXwjAqY29z0S0SrA==} + engines: {node: '>=14'} + + '@unocss/core@0.62.4': + resolution: {integrity: sha512-Cc+Vo6XlaQpyVejkJrrzzWtiK9pgMWzVVBpm9VCVtwZPUjD4GSc+g7VQCPXSsr7m03tmSuRySJx72QcASmauNQ==} + + '@unocss/eslint-config@0.62.4': + resolution: {integrity: sha512-bulYXf80MhTlN2oG/d1r23/FJTNiGxK/gG+p9nl/UeJZc5NOnes7fsqVscFTXDxVQGVIFgeKfch3DRmzlxHe9Q==} + engines: {node: '>=14'} + + '@unocss/eslint-plugin@0.62.4': + resolution: {integrity: sha512-L4pm8L96OvE99FK+fZHQBXxsu+B/yvhf471Mf5o3idaq+pzptfpZcKKRXCeQKSAYbC80IV4Fm1V5dFxOHbDdPg==} + engines: {node: '>=14'} + + '@unocss/extractor-arbitrary-variants@0.62.4': + resolution: {integrity: sha512-e4hJfBMyFr6T6dYSTTjNv9CQwaU1CVEKxDlYP0GpfSgxsV58pguID9j1mt0/XZD6LvEDzwxj9RTRWKpUSWqp+Q==} + + '@unocss/inspector@0.62.4': + resolution: {integrity: sha512-bRcnI99gZecNzrUr6kDMdwGHkhUuTPyvvadRdaOxHc9Ow3ANNyqymeFM1q5anZEUZt8h15TYN0mdyQyIWkU3zg==} + + '@unocss/postcss@0.62.4': + resolution: {integrity: sha512-kWdHy7UsSP4bDu8I7sCKeO0VuzvVpNHmn2rifK5gNstUx5dZ1H/SoyXTHx5sKtgfZBRzdNXFu2nZ3PzYGvEFbw==} + engines: {node: '>=14'} + peerDependencies: + postcss: ^8.4.21 + + '@unocss/preset-attributify@0.62.4': + resolution: {integrity: sha512-ei5nNT58GON9iyCGRRiIrphzyQbBIZ9iEqSBhIY0flcfi1uAPUXV32aO2slqJnWWAIwbRSb1GMpwYR8mmfuz8g==} + + '@unocss/preset-icons@0.62.4': + resolution: {integrity: sha512-n9m2nRTxyiw0sqOwSioO3rro0kaPW0JJzWlzcfdwQ+ZORNR5WyJL298fLXYUFbZG3EOF+zSPg6CMDWudKk/tlA==} + + '@unocss/preset-mini@0.62.4': + resolution: {integrity: sha512-1O+QpQFx7FT61aheAZEYemW5e4AGib8TFGm+rWLudKq2IBNnXHcS5xsq5QvqdC7rp9Dn3lnW5du6ijow5kCBuw==} + + '@unocss/preset-tagify@0.62.4': + resolution: {integrity: sha512-8b2Kcsvt93xu1JqDqcD3QvvW0L5rqvH7ev3BlNEVx6n8ayBqfB5HEd4ILKr7wSC90re+EnCgnMm7EP2FiQAJkw==} + + '@unocss/preset-typography@0.62.4': + resolution: {integrity: sha512-ZVh+NbcibMmD6ve8Deub/G+XAFcGPuzE2Fx/tMAfWfYlfyOAtrMxuL+AARMthpRxdE0JOtggXNTrJb0ZhGYl9g==} + + '@unocss/preset-uno@0.62.4': + resolution: {integrity: sha512-2S6+molIz8dH/al0nfkU7i/pMS0oERPr4k9iW80Byt4cKDIhh/0jhZrC83kgZRtCf5hclSBO4oCoMTi1JF7SBw==} + + '@unocss/preset-web-fonts@0.62.4': + resolution: {integrity: sha512-kaxgYBVyMdBlErseN8kWLiaS2N5OMlwg5ktAxUlei275fMoY7inQjOwppnjDVveJbN9SP6TcqqFpBIPfUayPkQ==} + + '@unocss/preset-wind@0.62.4': + resolution: {integrity: sha512-YOzfQ11AmAnl1ZkcWLMMxCdezLjRKavLNk38LumUMtcdsa0DAy+1JjTp+KEvVQAnD+Et/ld5X+YcBWJkVy5WFQ==} + + '@unocss/reset@0.62.4': + resolution: {integrity: sha512-CtxjeDgN39fY/eZDLIXN4wy7C8W7+SD+41AlzGVU5JwhcXmnb1XoDpOd2lzMxc/Yy3F5dIJt2+MRDj9RnpX9Ew==} + + '@unocss/rule-utils@0.62.4': + resolution: {integrity: sha512-XUwLbLUzL+VSHCJNK5QBHC9RbFehumge1/XJmsRfmh0+oxgJoO1gvEvxi57gYEmdJdMRJHRJZ66se6+cB0Ymvw==} + engines: {node: '>=14'} + + '@unocss/transformer-attributify-jsx@0.62.4': + resolution: {integrity: sha512-z9DDqS2DibDR9gno55diKfAVegeJ9uoyQXQhH3R0KY4YMF49N1fWy/t74gOiHtlPmvjQtDRZYgjgaMCc2w8oWg==} + + '@unocss/transformer-compile-class@0.62.4': + resolution: {integrity: sha512-8yadY9T7LToJwSsrmYU3rUKlnDgPGVRvON7z9g1IjUCmFCGx7Gpg84x9KpKUG6eUTshPQFUI0YUHocrYFevAEA==} + + '@unocss/transformer-directives@0.62.4': + resolution: {integrity: sha512-bq9ZDG6/mr6X2mAogAo0PBVrLSLT0900MPqnj/ixadYHc7mRpX+y6bc/1AgWytZIFYSdNzf7XDoquZuwf42Ucg==} + + '@unocss/transformer-variant-group@0.62.4': + resolution: {integrity: sha512-W1fxMc2Lzxu4E+6JBQEBzK+AwoCQYI+EL2FT2BCUsAno37f3JdnwFFEVscck0epSdmdtidsSLDognyX8h10r8A==} + + '@unocss/vite@0.62.4': + resolution: {integrity: sha512-JKq3V6bcevYl9X5Jl3p9crArbhzI8JVWQkOxKV2nGLFaqvnc47vMSDxlU4MUdRWp3aQvzDw132tcx27oSbrojw==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 + + '@vitejs/plugin-vue@5.1.3': + resolution: {integrity: sha512-3xbWsKEKXYlmX82aOHufFQVnkbMC/v8fLpWwh6hWOUrK5fbbtBh9Q/WWse27BFgSy2/e2c0fz5Scgya9h2GLhw==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.5': + resolution: {integrity: sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==} + + '@volar/source-map@2.4.5': + resolution: {integrity: sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==} + + '@volar/typescript@2.4.5': + resolution: {integrity: sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==} + + '@vue/compiler-core@3.5.6': + resolution: {integrity: sha512-r+gNu6K4lrvaQLQGmf+1gc41p3FO2OUJyWmNqaIITaJU6YFiV5PtQSFZt8jfztYyARwqhoCayjprC7KMvT3nRA==} + + '@vue/compiler-dom@3.5.6': + resolution: {integrity: sha512-xRXqxDrIqK8v8sSScpistyYH0qYqxakpsIvqMD2e5sV/PXQ1mTwtXp4k42yHK06KXxKSmitop9e45Ui/3BrTEw==} + + '@vue/compiler-sfc@3.5.6': + resolution: {integrity: sha512-pjWJ8Kj9TDHlbF5LywjVso+BIxCY5wVOLhkEXRhuCHDxPFIeX1zaFefKs8RYoHvkSMqRWt93a0f2gNJVJixHwg==} + + '@vue/compiler-ssr@3.5.6': + resolution: {integrity: sha512-VpWbaZrEOCqnmqjE83xdwegtr5qO/2OPUC6veWgvNqTJ3bYysz6vY3VqMuOijubuUYPRpG3OOKIh9TD0Stxb9A==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/eslint-config-prettier@9.0.0': + resolution: {integrity: sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==} + peerDependencies: + eslint: '>= 8.0.0' + prettier: '>= 3.0.0' + + '@vue/eslint-config-typescript@13.0.0': + resolution: {integrity: sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + eslint-plugin-vue: ^9.0.0 + typescript: '>=4.7.4' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@2.1.6': + resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.6': + resolution: {integrity: sha512-shZ+KtBoHna5GyUxWfoFVBCVd7k56m6lGhk5e+J9AKjheHF6yob5eukssHRI+rzvHBiU1sWs/1ZhNbLExc5oYQ==} + + '@vue/runtime-core@3.5.6': + resolution: {integrity: sha512-FpFULR6+c2lI+m1fIGONLDqPQO34jxV8g6A4wBOgne8eSRHP6PQL27+kWFIx5wNhhjkO7B4rgtsHAmWv7qKvbg==} + + '@vue/runtime-dom@3.5.6': + resolution: {integrity: sha512-SDPseWre45G38ENH2zXRAHL1dw/rr5qp91lS4lt/nHvMr0MhsbCbihGAWLXNB/6VfFOJe2O+RBRkXU+CJF7/sw==} + + '@vue/server-renderer@3.5.6': + resolution: {integrity: sha512-zivnxQnOnwEXVaT9CstJ64rZFXMS5ZkKxCjDQKiMSvUhXRzFLWZVbaBiNF4HGDqGNNsTgmjcCSmU6TB/0OOxLA==} + peerDependencies: + vue: 3.5.6 + + '@vue/shared@3.5.6': + resolution: {integrity: sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==} + + '@vue/tsconfig@0.5.1': + resolution: {integrity: sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==} + + '@vueuse/core@11.1.0': + resolution: {integrity: sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==} + + '@vueuse/metadata@11.1.0': + resolution: {integrity: sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==} + + '@vueuse/shared@11.1.0': + resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} + + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-require@4.2.1: + resolution: {integrity: sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + + bundle-require@5.0.0: + resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + + connect-history-api-fallback@1.6.0: + resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} + engines: {node: '>=0.8'} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==} + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + date-fns-tz@2.0.1: + resolution: {integrity: sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==} + peerDependencies: + date-fns: 2.x + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dotenv-expand@8.0.3: + resolution: {integrity: sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + echarts@5.5.1: + resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.28.0: + resolution: {integrity: sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fdir@6.3.0: + resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + highlight.js@11.10.0: + resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==} + engines: {node: '>=12.0.0'} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + importx@0.4.4: + resolution: {integrity: sha512-Lo1pukzAREqrBnnHC+tj+lreMTAvyxtkKsMxLY8H15M/bvLl54p3YuoTI70Tz7Il0AsgSlD7Lrk/FaApRcBL7w==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + jiti@2.0.0-beta.3: + resolution: {integrity: sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.7.1: + resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + + mockjs@1.1.0: + resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==} + hasBin: true + + monaco-editor-nginx@2.0.2: + resolution: {integrity: sha512-F/qejb0w0hxIyxbu2JMCe+MhnOV7bxGbUynnyMXk7Cy25Na5fD99u5QqRr7WfRhIn0xcdhBEkJ/jikQsxVnEOA==} + peerDependencies: + '@babel/runtime': '>=7.10.0' + '@nginx/reference-lib': '>=1.1.0' + monaco-editor: '>=0.22.3' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + monaco-editor@0.51.0: + resolution: {integrity: sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + naive-ui@2.39.0: + resolution: {integrity: sha512-5oUJzRG+rtLSH8eRU+fJvVYiQids2BxF9jp+fwGoAqHOptEINrBlgBu9uy+95RHE5FLJ7Q/z41o+qkoGnUrKxQ==} + peerDependencies: + vue: ^3.0.0 + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + + node-html-parser@5.4.2: + resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-normalize-package-bin@3.0.1: + resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-run-all2@6.2.3: + resolution: {integrity: sha512-5RsxC7jEc/RjxOYBVdEfrJf5FsJ0pHA7jr2/OxrThXknajETCTYjigOCG3iaGjdYIKEQlDuCG0ir0T1HTva8pg==} + engines: {node: ^14.18.0 || ^16.13.0 || >=18.0.0, npm: '>= 8'} + hasBin: true + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ofetch@1.3.4: + resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-manager-detector@0.2.0: + resolution: {integrity: sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@0.2.0: + resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia@2.2.2: + resolution: {integrity: sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==} + peerDependencies: + '@vue/composition-api': ^1.4.0 + typescript: '>=4.4.4' + vue: ^2.6.14 || ^3.3.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + typescript: + optional: true + + pkg-types@1.2.0: + resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-package-json-fast@3.0.2: + resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup-plugin-visualizer@5.12.0: + resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rollup: + optional: true + + rollup@4.21.3: + resolution: {integrity: sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.78.0: + resolution: {integrity: sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + seemly@0.3.8: + resolution: {integrity: sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.9.1: + resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==} + engines: {node: ^14.18.0 || >=16.0.0} + + terser@5.32.0: + resolution: {integrity: sha512-v3Gtw3IzpBJ0ugkxEX8U0W6+TnPKRRCWGh1jC/iM/e3Ki5+qvO1L1EAZ56bZasc64aXHwRHNIQEzm6//i5cemQ==} + engines: {node: '>=10'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinyexec@0.3.0: + resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + + tinyglobby@0.2.6: + resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} + engines: {node: '>=12.0.0'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + tsx@4.19.1: + resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + unconfig@0.5.5: + resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unimport@3.12.0: + resolution: {integrity: sha512-5y8dSvNvyevsnw4TBQkIQR1Rjdbb+XjVSwQwxltpnVZrStBvvPkMPcZrh1kg5kY77kpx6+D4Ztd3W6FOBH/y2Q==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unocss@0.62.4: + resolution: {integrity: sha512-SaGbxXQkk8GDPeJpWsBCZ8a23Knu4ixVTt6pvcQWKjOCGTd9XBd+vLZzN2WwdwgBPVwmMmx5wp+/gPHKFNOmIw==} + engines: {node: '>=14'} + peerDependencies: + '@unocss/webpack': 0.62.4 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@unocss/webpack': + optional: true + vite: + optional: true + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-auto-import@0.18.3: + resolution: {integrity: sha512-q3FUtGQjYA2e+kb1WumyiQMjHM27MrTQ05QfVwtLRVhyYe+KF6TblBYaEX9L6Z0EibsqaXAiW+RFfkcQpfaXzg==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-icons@0.19.3: + resolution: {integrity: sha512-EUegRmsAI6+rrYr0vXjFlIP+lg4fSC4zb62zAZKx8FGXlWAGgEGBCa3JDe27aRAXhistObLPbBPhwa/0jYLFkQ==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + + unplugin-vue-components@0.27.4: + resolution: {integrity: sha512-1XVl5iXG7P1UrOMnaj2ogYa5YTq8aoh5jwDPQhemwO/OrXW+lPQKDXd1hMz15qxQPxgb/XXlbgo3HQ2rLEbmXQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@1.14.1: + resolution: {integrity: sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==} + engines: {node: '>=14.0.0'} + peerDependencies: + webpack-sources: ^3 + peerDependenciesMeta: + webpack-sources: + optional: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} + peerDependencies: + vue: ^3.0.11 + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-html@3.2.2: + resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-mock@3.0.2: + resolution: {integrity: sha512-bD//HvkTygGmk+LsIAdf0jGNlCv4iWv0kZlH9UEgWT6QYoUwfjQAE4SKxHRw2tfLgVhbPQVv/+X3YlNWvueGUA==} + engines: {node: '>=16.0.0'} + peerDependencies: + esbuild: '>=0.17' + mockjs: '>=1.1.0' + vite: '>=4.0.0' + + vite@5.4.6: + resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==} + peerDependencies: + vue: ^3.0.0 + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-demi@0.13.11: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-echarts@7.0.3: + resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==} + peerDependencies: + '@vue/runtime-core': ^3.0.0 + echarts: ^5.5.1 + vue: ^2.7.0 || ^3.1.1 + peerDependenciesMeta: + '@vue/runtime-core': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@10.0.1: + resolution: {integrity: sha512-SQVlSm/1S6AaG1wexvwq3ebXUrrkx75ZHD78UAs4/rYD/X3tsQxfm6ElpT4ZPegJQEgRtOJjGripqSrfqAENtg==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-router@4.4.5: + resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.1.6: + resolution: {integrity: sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.6: + resolution: {integrity: sha512-zv+20E2VIYbcJOzJPUWp03NOGFhMmpCKOfSxVTmCYyYFFko48H9tmuQFzYj7tu4qX1AeXlp9DmhIP89/sSxxhw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vueuc@0.4.58: + resolution: {integrity: sha512-Wnj/N8WbPRSxSt+9ji1jtDHPzda5h2OH/0sFBhvdxDRuyCZbjGg3/cKMaKqEoe+dErTexG2R+i6Q8S/Toq1MYg==} + peerDependencies: + vue: ^3.0.11 + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zrender@5.6.0: + resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.0 + tinyexec: 0.3.0 + + '@antfu/utils@0.7.10': {} + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/parser@7.25.6': + dependencies: + '@babel/types': 7.25.6 + + '@babel/runtime@7.25.6': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/types@7.25.6': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.6(typescript@5.6.2))': + dependencies: + vue: 3.5.6(typescript@5.6.2) + + '@emotion/hash@0.8.0': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@guolao/vue-monaco-editor@1.5.4(monaco-editor@0.51.0)(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.51.0) + monaco-editor: 0.51.0 + vue: 3.5.6(typescript@5.6.2) + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@iconify/json@2.2.249': + dependencies: + '@iconify/types': 2.0.0 + pathe: 1.1.2 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.1.33': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/types': 2.0.0 + debug: 4.3.7 + kolorist: 1.8.0 + local-pkg: 0.5.0 + mlly: 1.7.1 + transitivePeerDependencies: + - supports-color + + '@iconify/vue@4.1.2(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.6(typescript@5.6.2) + + '@intlify/core-base@10.0.1': + dependencies: + '@intlify/message-compiler': 10.0.1 + '@intlify/shared': 10.0.1 + + '@intlify/message-compiler@10.0.1': + dependencies: + '@intlify/shared': 10.0.1 + source-map-js: 1.2.1 + + '@intlify/shared@10.0.1': {} + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@juggle/resize-observer@3.4.0': {} + + '@monaco-editor/loader@1.4.0(monaco-editor@0.51.0)': + dependencies: + monaco-editor: 0.51.0 + state-local: 1.0.7 + + '@nginx/reference-lib@1.1.2': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgr/core@0.1.1': {} + + '@polka/url@1.0.0-next.25': {} + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/pluginutils@5.1.0(rollup@4.21.3)': + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.21.3 + + '@rollup/rollup-android-arm-eabi@4.21.3': + optional: true + + '@rollup/rollup-android-arm64@4.21.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.21.3': + optional: true + + '@rollup/rollup-darwin-x64@4.21.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.21.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.21.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.21.3': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.21.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.21.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.21.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.21.3': + optional: true + + '@rushstack/eslint-patch@1.10.4': {} + + '@tsconfig/node20@20.1.4': {} + + '@types/crypto-js@4.2.2': {} + + '@types/estree@1.0.5': {} + + '@types/katex@0.16.7': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.7 + + '@types/lodash@4.17.7': {} + + '@types/node@20.16.5': + dependencies: + undici-types: 6.19.8 + + '@types/web-bluetooth@0.0.20': {} + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.3.7 + eslint: 8.57.0 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/scope-manager@8.6.0': + dependencies: + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + debug: 4.3.7 + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/types@8.6.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.2)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.3.7 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.6.0(typescript@5.6.2)': + dependencies: + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.6.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.6.0': + dependencies: + '@typescript-eslint/types': 8.6.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.0': {} + + '@unocss/astro@0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/reset': 0.62.4 + '@unocss/vite': 0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + optionalDependencies: + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - rollup + - supports-color + + '@unocss/cli@0.62.4(rollup@4.21.3)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + '@unocss/preset-uno': 0.62.4 + cac: 6.7.14 + chokidar: 3.6.0 + colorette: 2.0.20 + consola: 3.2.3 + magic-string: 0.30.11 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + tinyglobby: 0.2.6 + transitivePeerDependencies: + - rollup + - supports-color + + '@unocss/config@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + unconfig: 0.5.5 + transitivePeerDependencies: + - supports-color + + '@unocss/core@0.62.4': {} + + '@unocss/eslint-config@0.62.4(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@unocss/eslint-plugin': 0.62.4(eslint@8.57.0)(typescript@5.6.2) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@unocss/eslint-plugin@0.62.4(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/utils': 8.6.0(eslint@8.57.0)(typescript@5.6.2) + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + magic-string: 0.30.11 + synckit: 0.9.1 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@unocss/extractor-arbitrary-variants@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/inspector@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/rule-utils': 0.62.4 + gzip-size: 6.0.0 + sirv: 2.0.4 + + '@unocss/postcss@0.62.4(postcss@8.4.47)': + dependencies: + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + '@unocss/rule-utils': 0.62.4 + css-tree: 2.3.1 + postcss: 8.4.47 + tinyglobby: 0.2.6 + transitivePeerDependencies: + - supports-color + + '@unocss/preset-attributify@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/preset-icons@0.62.4': + dependencies: + '@iconify/utils': 2.1.33 + '@unocss/core': 0.62.4 + ofetch: 1.3.4 + transitivePeerDependencies: + - supports-color + + '@unocss/preset-mini@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/extractor-arbitrary-variants': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/preset-tagify@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/preset-typography@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + + '@unocss/preset-uno@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + '@unocss/preset-wind': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/preset-web-fonts@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + ofetch: 1.3.4 + + '@unocss/preset-wind@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/reset@0.62.4': {} + + '@unocss/rule-utils@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + magic-string: 0.30.11 + + '@unocss/transformer-attributify-jsx@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/transformer-compile-class@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/transformer-directives@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/rule-utils': 0.62.4 + css-tree: 2.3.1 + + '@unocss/transformer-variant-group@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/vite@0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + '@unocss/inspector': 0.62.4 + chokidar: 3.6.0 + magic-string: 0.30.11 + tinyglobby: 0.2.6 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - rollup + - supports-color + + '@vitejs/plugin-vue@5.1.3(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))(vue@3.5.6(typescript@5.6.2))': + dependencies: + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + vue: 3.5.6(typescript@5.6.2) + + '@volar/language-core@2.4.5': + dependencies: + '@volar/source-map': 2.4.5 + + '@volar/source-map@2.4.5': {} + + '@volar/typescript@2.4.5': + dependencies: + '@volar/language-core': 2.4.5 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vue/compiler-core@3.5.6': + dependencies: + '@babel/parser': 7.25.6 + '@vue/shared': 3.5.6 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.6': + dependencies: + '@vue/compiler-core': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/compiler-sfc@3.5.6': + dependencies: + '@babel/parser': 7.25.6 + '@vue/compiler-core': 3.5.6 + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-ssr': 3.5.6 + '@vue/shared': 3.5.6 + estree-walker: 2.0.2 + magic-string: 0.30.11 + postcss: 8.4.47 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.6': + dependencies: + '@vue/compiler-dom': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/eslint-config-prettier@9.0.0(eslint@8.57.0)(prettier@3.3.3)': + dependencies: + eslint: 8.57.0 + eslint-config-prettier: 9.1.0(eslint@8.57.0) + eslint-plugin-prettier: 5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3) + prettier: 3.3.3 + transitivePeerDependencies: + - '@types/eslint' + + '@vue/eslint-config-typescript@13.0.0(eslint-plugin-vue@9.28.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + eslint: 8.57.0 + eslint-plugin-vue: 9.28.0(eslint@8.57.0) + vue-eslint-parser: 9.4.3(eslint@8.57.0) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@2.1.6(typescript@5.6.2)': + dependencies: + '@volar/language-core': 2.4.5 + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.6 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.2 + + '@vue/reactivity@3.5.6': + dependencies: + '@vue/shared': 3.5.6 + + '@vue/runtime-core@3.5.6': + dependencies: + '@vue/reactivity': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/runtime-dom@3.5.6': + dependencies: + '@vue/reactivity': 3.5.6 + '@vue/runtime-core': 3.5.6 + '@vue/shared': 3.5.6 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.6(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.6 + '@vue/shared': 3.5.6 + vue: 3.5.6(typescript@5.6.2) + + '@vue/shared@3.5.6': {} + + '@vue/tsconfig@0.5.1': {} + + '@vueuse/core@11.1.0(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 11.1.0 + '@vueuse/shared': 11.1.0(vue@3.5.6(typescript@5.6.2)) + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@11.1.0': {} + + '@vueuse/shared@11.1.0(vue@3.5.6(typescript@5.6.2))': + dependencies: + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/xterm@5.5.0': {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + async-validator@4.2.5: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: {} + + bundle-require@4.2.1(esbuild@0.23.1): + dependencies: + esbuild: 0.23.1 + load-tsconfig: 0.2.5 + + bundle-require@5.0.0(esbuild@0.23.1): + dependencies: + esbuild: 0.23.1 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.7.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@12.1.0: {} + + commander@2.20.3: {} + + commander@8.3.0: {} + + computeds@0.0.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.7: {} + + connect-history-api-fallback@1.6.0: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + consola@2.15.3: {} + + consola@3.2.3: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + csstype@3.0.11: {} + + csstype@3.1.3: {} + + date-fns-tz@2.0.1(date-fns@2.30.0): + dependencies: + date-fns: 2.30.0 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.25.6 + + dayjs@1.11.13: {} + + de-indent@1.0.2: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-lazy-prop@2.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + + dotenv-expand@8.0.3: {} + + dotenv@16.4.5: {} + + duplexer@0.1.2: {} + + echarts@5.5.1: + dependencies: + tslib: 2.3.0 + zrender: 5.6.0 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + entities@2.2.0: {} + + entities@4.5.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3): + dependencies: + eslint: 8.57.0 + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.1 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.0) + + eslint-plugin-vue@9.28.0(eslint@8.57.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + eslint: 8.57.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@8.57.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.7 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + + esutils@2.0.3: {} + + evtd@0.2.4: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fdir@6.3.0(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + follow-redirects@1.15.9: {} + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + has-flag@4.0.0: {} + + he@1.2.0: {} + + highlight.js@11.10.0: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.32.0 + + ignore@5.3.2: {} + + immutable@4.3.7: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + importx@0.4.4: + dependencies: + bundle-require: 5.0.0(esbuild@0.23.1) + debug: 4.3.7 + esbuild: 0.23.1 + jiti: 2.0.0-beta.3 + jiti-v1: jiti@1.21.6 + pathe: 1.1.2 + tsx: 4.19.1 + transitivePeerDependencies: + - supports-color + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + install@0.13.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jiti@1.21.6: {} + + jiti@2.0.0-beta.3: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@3.0.2: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + load-tsconfig@0.2.5: {} + + local-pkg@0.5.0: + dependencies: + mlly: 1.7.1 + pkg-types: 1.2.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.7.0 + + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mdn-data@2.0.30: {} + + memorystream@0.3.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + mlly@1.7.1: + dependencies: + acorn: 8.12.1 + pathe: 1.1.2 + pkg-types: 1.2.0 + ufo: 1.5.4 + + mockjs@1.1.0: + dependencies: + commander: 12.1.0 + + monaco-editor-nginx@2.0.2(@babel/runtime@7.25.6)(@nginx/reference-lib@1.1.2)(monaco-editor@0.51.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.6 + '@nginx/reference-lib': 1.1.2 + monaco-editor: 0.51.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + monaco-editor@0.51.0: {} + + mrmime@2.0.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + naive-ui@2.39.0(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.6(typescript@5.6.2)) + '@types/katex': 0.16.7 + '@types/lodash': 4.17.7 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.1.3 + date-fns: 2.30.0 + date-fns-tz: 2.0.1(date-fns@2.30.0) + evtd: 0.2.4 + highlight.js: 11.10.0 + lodash: 4.17.21 + lodash-es: 4.17.21 + seemly: 0.3.8 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.6(typescript@5.6.2)) + vooks: 0.2.12(vue@3.5.6(typescript@5.6.2)) + vue: 3.5.6(typescript@5.6.2) + vueuc: 0.4.58(vue@3.5.6(typescript@5.6.2)) + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.7.0 + + node-fetch-native@1.6.4: {} + + node-html-parser@5.4.2: + dependencies: + css-select: 4.3.0 + he: 1.2.0 + + normalize-path@3.0.0: {} + + npm-normalize-package-bin@3.0.1: {} + + npm-run-all2@6.2.3: + dependencies: + ansi-styles: 6.2.1 + cross-spawn: 7.0.3 + memorystream: 0.3.1 + minimatch: 9.0.5 + pidtree: 0.6.0 + read-package-json-fast: 3.0.2 + shell-quote: 1.8.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ofetch@1.3.4: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@0.2.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.7.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + pathe@0.2.0: {} + + pathe@1.1.2: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.0: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pidtree@0.6.0: {} + + pinia@2.2.2(typescript@5.6.2)(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + optionalDependencies: + typescript: 5.6.2 + + pkg-types@1.2.0: + dependencies: + confbox: 0.1.7 + mlly: 1.7.1 + pathe: 1.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.47: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.0 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.3.3: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-package-json-fast@3.0.2: + dependencies: + json-parse-even-better-errors: 3.0.2 + npm-normalize-package-bin: 3.0.1 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regenerator-runtime@0.14.1: {} + + relateurl@0.2.7: {} + + require-directory@2.1.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup-plugin-visualizer@5.12.0(rollup@4.21.3): + dependencies: + open: 8.4.2 + picomatch: 2.3.1 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.21.3 + + rollup@4.21.3: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.21.3 + '@rollup/rollup-android-arm64': 4.21.3 + '@rollup/rollup-darwin-arm64': 4.21.3 + '@rollup/rollup-darwin-x64': 4.21.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.3 + '@rollup/rollup-linux-arm-musleabihf': 4.21.3 + '@rollup/rollup-linux-arm64-gnu': 4.21.3 + '@rollup/rollup-linux-arm64-musl': 4.21.3 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.3 + '@rollup/rollup-linux-riscv64-gnu': 4.21.3 + '@rollup/rollup-linux-s390x-gnu': 4.21.3 + '@rollup/rollup-linux-x64-gnu': 4.21.3 + '@rollup/rollup-linux-x64-musl': 4.21.3 + '@rollup/rollup-win32-arm64-msvc': 4.21.3 + '@rollup/rollup-win32-ia32-msvc': 4.21.3 + '@rollup/rollup-win32-x64-msvc': 4.21.3 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.78.0: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.7 + source-map-js: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scule@1.3.0: {} + + seemly@0.3.8: {} + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.1: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + state-local@1.0.7: {} + + statuses@1.5.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.0: + dependencies: + js-tokens: 9.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.9.1: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.7.0 + + terser@5.32.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-table@0.2.0: {} + + tinyexec@0.3.0: {} + + tinyglobby@0.2.6: + dependencies: + fdir: 6.3.0(picomatch@4.0.2) + picomatch: 4.0.2 + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + treemate@0.3.11: {} + + ts-api-utils@1.3.0(typescript@5.6.2): + dependencies: + typescript: 5.6.2 + + tslib@2.3.0: {} + + tslib@2.7.0: {} + + tsx@4.19.1: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.6.2: {} + + ufo@1.5.4: {} + + unconfig@0.5.5: + dependencies: + '@antfu/utils': 0.7.10 + defu: 6.1.4 + importx: 0.4.4 + transitivePeerDependencies: + - supports-color + + undici-types@6.19.8: {} + + unimport@3.12.0(rollup@4.21.3): + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + acorn: 8.12.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + mlly: 1.7.1 + pathe: 1.1.2 + pkg-types: 1.2.0 + scule: 1.3.0 + strip-literal: 2.1.0 + unplugin: 1.14.1 + transitivePeerDependencies: + - rollup + - webpack-sources + + universalify@2.0.1: {} + + unocss@0.62.4(postcss@8.4.47)(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + '@unocss/astro': 0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + '@unocss/cli': 0.62.4(rollup@4.21.3) + '@unocss/core': 0.62.4 + '@unocss/postcss': 0.62.4(postcss@8.4.47) + '@unocss/preset-attributify': 0.62.4 + '@unocss/preset-icons': 0.62.4 + '@unocss/preset-mini': 0.62.4 + '@unocss/preset-tagify': 0.62.4 + '@unocss/preset-typography': 0.62.4 + '@unocss/preset-uno': 0.62.4 + '@unocss/preset-web-fonts': 0.62.4 + '@unocss/preset-wind': 0.62.4 + '@unocss/transformer-attributify-jsx': 0.62.4 + '@unocss/transformer-compile-class': 0.62.4 + '@unocss/transformer-directives': 0.62.4 + '@unocss/transformer-variant-group': 0.62.4 + '@unocss/vite': 0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + optionalDependencies: + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - postcss + - rollup + - supports-color + + unpipe@1.0.0: {} + + unplugin-auto-import@0.18.3(@vueuse/core@11.1.0(vue@3.5.6(typescript@5.6.2)))(rollup@4.21.3): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + minimatch: 9.0.5 + unimport: 3.12.0(rollup@4.21.3) + unplugin: 1.14.1 + optionalDependencies: + '@vueuse/core': 11.1.0(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - rollup + - webpack-sources + + unplugin-icons@0.19.3(@vue/compiler-sfc@3.5.6): + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/utils': 2.1.33 + debug: 4.3.7 + kolorist: 1.8.0 + local-pkg: 0.5.0 + unplugin: 1.14.1 + optionalDependencies: + '@vue/compiler-sfc': 3.5.6 + transitivePeerDependencies: + - supports-color + - webpack-sources + + unplugin-vue-components@0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + chokidar: 3.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + minimatch: 9.0.5 + mlly: 1.7.1 + unplugin: 1.14.1 + vue: 3.5.6(typescript@5.6.2) + optionalDependencies: + '@babel/parser': 7.25.6 + transitivePeerDependencies: + - rollup + - supports-color + - webpack-sources + + unplugin@1.14.1: + dependencies: + acorn: 8.12.1 + webpack-virtual-modules: 0.6.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + vdirs@0.1.8(vue@3.5.6(typescript@5.6.2)): + dependencies: + evtd: 0.2.4 + vue: 3.5.6(typescript@5.6.2) + + vite-plugin-compression@0.5.1(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + chalk: 4.1.2 + debug: 4.3.7 + fs-extra: 10.1.0 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - supports-color + + vite-plugin-html@3.2.2(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + '@rollup/pluginutils': 4.2.1 + colorette: 2.0.20 + connect-history-api-fallback: 1.6.0 + consola: 2.15.3 + dotenv: 16.4.5 + dotenv-expand: 8.0.3 + ejs: 3.1.10 + fast-glob: 3.3.2 + fs-extra: 10.1.0 + html-minifier-terser: 6.1.0 + node-html-parser: 5.4.2 + pathe: 0.2.0 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + + vite-plugin-mock@3.0.2(esbuild@0.23.1)(mockjs@1.1.0)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + bundle-require: 4.2.1(esbuild@0.23.1) + chokidar: 3.6.0 + connect: 3.7.0 + debug: 4.3.7 + esbuild: 0.23.1 + fast-glob: 3.3.2 + mockjs: 1.1.0 + path-to-regexp: 6.3.0 + picocolors: 1.1.0 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - supports-color + + vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.21.3 + optionalDependencies: + '@types/node': 20.16.5 + fsevents: 2.3.3 + sass: 1.78.0 + terser: 5.32.0 + + vooks@0.2.12(vue@3.5.6(typescript@5.6.2)): + dependencies: + evtd: 0.2.4 + vue: 3.5.6(typescript@5.6.2) + + vscode-uri@3.0.8: {} + + vue-demi@0.13.11(vue@3.5.6(typescript@5.6.2)): + dependencies: + vue: 3.5.6(typescript@5.6.2) + + vue-demi@0.14.10(vue@3.5.6(typescript@5.6.2)): + dependencies: + vue: 3.5.6(typescript@5.6.2) + + vue-echarts@7.0.3(@vue/runtime-core@3.5.6)(echarts@5.5.1)(vue@3.5.6(typescript@5.6.2)): + dependencies: + echarts: 5.5.1 + vue: 3.5.6(typescript@5.6.2) + vue-demi: 0.13.11(vue@3.5.6(typescript@5.6.2)) + optionalDependencies: + '@vue/runtime-core': 3.5.6 + transitivePeerDependencies: + - '@vue/composition-api' + + vue-eslint-parser@9.4.3(eslint@8.57.0): + dependencies: + debug: 4.3.7 + eslint: 8.57.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + vue-i18n@10.0.1(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@intlify/core-base': 10.0.1 + '@intlify/shared': 10.0.1 + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + + vue-router@4.4.5(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + + vue-tsc@2.1.6(typescript@5.6.2): + dependencies: + '@volar/typescript': 2.4.5 + '@vue/language-core': 2.1.6(typescript@5.6.2) + semver: 7.6.3 + typescript: 5.6.2 + + vue@3.5.6(typescript@5.6.2): + dependencies: + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-sfc': 3.5.6 + '@vue/runtime-dom': 3.5.6 + '@vue/server-renderer': 3.5.6(vue@3.5.6(typescript@5.6.2)) + '@vue/shared': 3.5.6 + optionalDependencies: + typescript: 5.6.2 + + vueuc@0.4.58(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.6(typescript@5.6.2)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.8 + vdirs: 0.1.8(vue@3.5.6(typescript@5.6.2)) + vooks: 0.2.12(vue@3.5.6(typescript@5.6.2)) + vue: 3.5.6(typescript@5.6.2) + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zrender@5.6.0: + dependencies: + tslib: 2.3.0 diff --git a/web/public/favicon.png b/web/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c3fa8f2ff314bbe2d5b65fd31f76c7a77240f69c GIT binary patch literal 1265 zcmVPx(t4TybR9HvNS8Iq>RTTcdeeRhVQ+!m~LzEt4l$JC1o(hA0gi?`M(2uC-AxaUZ z_CQU7{zxH$iaDv2l@UZH(gQR?DS}FfoOA9JhW1d&M?{#EuRGH@cYm>N_YSvd#$=qP z)~|Eed$0BFZ>{~Uy@@)DqG&vT`2eN^xC}s3{dj-BC3TVf0l+p%ZAp@RU-QMR>@fhPfvWa?0O~XV>c_$XTk^jJa2&vE073w>>QZzJ zz*hib09keH+$tT%CG98ad!t9vPym;cboJxCF|aF5(|f}(oB`mJ_x?lY+#~==sZ=`H z*w}a#$?KhS6TJ7IklZ3^6M!XImh~FE7SK6&eh>uj0~pb*ggt8DsQ3O>0MndvkJ>K1 z_v@W=PSX7(n*lUb7auB@%eMzXQ1ss4?3^=YyW(`kA?XnSQ%T<47l2)o76I5ua+pn$ zbOL}mpF(mP0F&$0aUAcBqG%m}Ig*x0+C#E?XVm~XlJfvu3E+tL{tl9DB(LoYz^PKH zbU{;7(@c^JBw3!9CrR>hOH0e7TrRf@z%ofY09Z~(lQck*^byJ3l732(WNAL1Z*k7O z3LpZoIZe}fVHjFHEjwKe90hPSfG+@yF|Ppjk+gsh^`Q4imSs2Pa=9(u`{yL>v+*DZ z_IdAzIp;cT*OKNYN%F3ZwYIies-7p=EqpzT@UwEcyx2K+3(47%$^c$BN6xwNmL5qf zB)#FB`#DY1V__H$2e4Aos5*cnNlpP^j9444x5XsC>?;LZC2b@5EP#<#gjNHRW(Gkp zRMMN0-nQN@X{@B}#bR+op-?c!Mt3_U8v$Gd;HV@!y4{v#+0-x$D{PtdWZ+~R$0PFj z{9VqujR5wNT;jc-L-K|?Kc@iv2%rtXjx5X6)zx*Nsi~>l(a~`%ilXZPd=B7=IF460 zH#ZL-GGs^)hT%A?0g|`%3P933X__u-XlU38zy#)ZNmj`lB<&>m5`an{Z*6Uz9t6P- zNiUMTr{++S<@l`{005>92DDfE3j>m7k+fpF5Wq>JkYq=>TwZQU76ie!#zwJNTpvZz zAplze><6&4dSpLH)AYW^#>N&&9~%SJR4hJI0G407AEc7jknBp+bVeA4&!lO(Dh$KJ z)gyO;QjNbN`jG|NcHuC=@#Rr*I6q zRURx+6n*ngZS>zlCh0o^U?O49u|YRUYgs51nn_xl9qcruB(JGF%?gFWT#{=BnSzCJ z9IxztoJLV(SFn|X@J2`hJZ-@%I#9ps$&)8vn#<+pSG(0E0|mya9l!wq+wBG3-rjD7 bU$H*`Gomc1cwX^900000NkvXXu0mjfT|7=6 literal 0 HcmV?d00001 diff --git a/web/public/loading/index.css b/web/public/loading/index.css new file mode 100644 index 00000000..4a887e81 --- /dev/null +++ b/web/public/loading/index.css @@ -0,0 +1,91 @@ +.loading-container { + position: fixed; + left: 0; + top: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} + +.loading-spin__container { + width: 56px; + height: 56px; + margin: 36px 0; +} + +.loading-spin { + position: relative; + height: 100%; + animation: loadingSpin 1s linear infinite; +} + +.left-0 { + left: 0; +} + +.right-0 { + right: 0; +} + +.top-0 { + top: 0; +} + +.bottom-0 { + bottom: 0; +} + +.loading-spin-item { + position: absolute; + height: 16px; + width: 16px; + background-color: var(--primary-color); + border-radius: 8px; + -webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes loadingSpin { + from { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes loadingPulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.loading-delay-500 { + -webkit-animation-delay: 500ms; + animation-delay: 500ms; +} + +.loading-delay-1000 { + -webkit-animation-delay: 1000ms; + animation-delay: 1000ms; +} + +.loading-delay-1500 { + -webkit-animation-delay: 1500ms; + animation-delay: 1500ms; +} + +.loading-title { + font-size: 28px; + font-weight: 500; + color: #6a6a6a; +} diff --git a/web/public/loading/index.js b/web/public/loading/index.js new file mode 100644 index 00000000..6c474ec1 --- /dev/null +++ b/web/public/loading/index.js @@ -0,0 +1,9 @@ +function addThemeColorCssVars() { + const key = '__THEME_COLOR__' + const defaultColor = '#66CCFF' + const themeColor = window.localStorage.getItem(key) || defaultColor + const cssVars = `--primary-color: ${themeColor}` + document.documentElement.style.cssText = cssVars +} + +addThemeColorCssVars() diff --git a/web/public/loading/logo.png b/web/public/loading/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5e2126eaa2754558b10e807450cb2f3b513d95 GIT binary patch literal 10253 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 LQ4?7qj|%@EiEU(x literal 0 HcmV?d00001 diff --git a/web/public/robots.txt b/web/public/robots.txt new file mode 100644 index 00000000..1f53798b --- /dev/null +++ b/web/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/web/settings/.gitignore b/web/settings/.gitignore new file mode 100644 index 00000000..de7b192a --- /dev/null +++ b/web/settings/.gitignore @@ -0,0 +1 @@ +proxy-config.ts \ No newline at end of file diff --git a/web/settings/proxy-config.ts.example b/web/settings/proxy-config.ts.example new file mode 100644 index 00000000..0924353b --- /dev/null +++ b/web/settings/proxy-config.ts.example @@ -0,0 +1,18 @@ +const proxyConfigMappings: Record = { + dev: { + prefix: '/api', + target: 'http://localhost:8080' + }, + test: { + prefix: '/api', + target: 'http://localhost:8080' + }, + prod: { + prefix: '/api', + target: 'http://localhost:8080' + } +} + +export function getProxyConfig(envType: ProxyType = 'dev'): ProxyConfig { + return proxyConfigMappings[envType] +} diff --git a/web/settings/theme.json b/web/settings/theme.json new file mode 100644 index 00000000..b4e1d049 --- /dev/null +++ b/web/settings/theme.json @@ -0,0 +1,25 @@ +{ + "isMobile": false, + "darkMode": false, + "sider": { + "width": 220, + "collapsedWidth": 64, + "collapsed": false + }, + "tab": { + "visible": true, + "height": 50 + }, + "header": { + "visible": true, + "height": 60 + }, + "primaryColor": "#66CCFF", + "otherColor": { + "info": "#2080F0", + "success": "#18A058", + "warning": "#F0A020", + "error": "#D03050" + }, + "language": "zh_CN" +} diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 00000000..a6fc6986 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/api/panel/cert/index.ts b/web/src/api/panel/cert/index.ts new file mode 100644 index 00000000..f6897b75 --- /dev/null +++ b/web/src/api/panel/cert/index.ts @@ -0,0 +1,59 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // CA 供应商列表 + caProviders: (): Promise> => request.get('/cert/caProviders'), + // DNS 供应商列表 + dnsProviders: (): Promise> => request.get('/cert/dnsProviders'), + // 证书算法列表 + algorithms: (): Promise> => request.get('/cert/algorithms'), + // ACME 用户列表 + users: (page: number, limit: number): Promise> => + request.get('/cert/users', { params: { page, limit } }), + // ACME 用户详情 + userInfo: (id: number): Promise> => request.get(`/cert/users/${id}`), + // ACME 用户添加 + userAdd: (data: any): Promise> => request.post('/cert/users', data), + // ACME 用户更新 + userUpdate: (id: number, data: any): Promise> => + request.put(`/cert/users/${id}`, data), + // ACME 用户删除 + userDelete: (id: number): Promise> => + request.delete(`/cert/users/${id}`), + // DNS 记录列表 + dns: (page: number, limit: number): Promise> => + request.get('/cert/dns', { params: { page, limit } }), + // DNS 记录详情 + dnsInfo: (id: number): Promise> => request.get(`/cert/dns/${id}`), + // DNS 记录添加 + dnsAdd: (data: any): Promise> => request.post('/cert/dns', data), + // DNS 记录更新 + dnsUpdate: (id: number, data: any): Promise> => + request.put(`/cert/dns/${id}`, data), + // DNS 记录删除 + dnsDelete: (id: number): Promise> => request.delete(`/cert/dns/${id}`), + // 证书列表 + certs: (page: number, limit: number): Promise> => + request.get('/cert/certs', { params: { page, limit } }), + // 证书详情 + certInfo: (id: number): Promise> => request.get(`/cert/certs/${id}`), + // 证书添加 + certAdd: (data: any): Promise> => request.post('/cert/certs', data), + // 证书更新 + certUpdate: (id: number, data: any): Promise> => + request.put(`/cert/certs/${id}`, data), + // 证书删除 + certDelete: (id: number): Promise> => + request.delete(`/cert/certs/${id}`), + // 签发 + obtain: (id: number): Promise> => request.post(`/cert/obtain`, { id }), + // 续签 + renew: (id: number): Promise> => request.post(`/cert/renew`, { id }), + // 获取 DNS 记录 + manualDNS: (id: number): Promise> => + request.post(`/cert/manualDNS`, { id }), + // 部署 + deploy: (id: number, website_id: number): Promise> => + request.post(`/cert/deploy`, { id, website_id }) +} diff --git a/web/src/api/panel/container/index.ts b/web/src/api/panel/container/index.ts new file mode 100644 index 00000000..eaf25972 --- /dev/null +++ b/web/src/api/panel/container/index.ts @@ -0,0 +1,106 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取容器列表 + containerList: (page: number, limit: number): Promise> => + request.get('/container/list', { params: { page, limit } }), + // 添加容器 + containerCreate: (config: any): Promise> => + request.post('/container/create', config), + // 删除容器 + containerRemove: (id: string): Promise> => + request.post(`/container/remove`, { id }), + // 启动容器 + containerStart: (id: string): Promise> => + request.post(`/container/start`, { id }), + // 停止容器 + containerStop: (id: string): Promise> => + request.post(`/container/stop`, { id }), + // 重启容器 + containerRestart: (id: string): Promise> => + request.post(`/container/restart`, { id }), + // 暂停容器 + containerPause: (id: string): Promise> => + request.post(`/container/pause`, { id }), + // 恢复容器 + containerUnpause: (id: string): Promise> => + request.post(`/container/unpause`, { id }), + // 获取容器详情 + containerDetail: (id: string): Promise> => + request.get(`/container/detail`, { params: { id } }), + // 杀死容器 + containerKill: (id: string): Promise> => + request.post(`/container/kill`, { id }), + // 重命名容器 + containerRename: (id: string, name: string): Promise> => + request.post(`/container/rename`, { id, name }), + // 获取容器状态 + containerStats: (id: string): Promise> => + request.get(`/container/stats`, { params: { id } }), + // 检查容器是否存在 + containerExist: (id: string): Promise> => + request.get(`/container/exist`, { params: { id } }), + // 获取容器日志 + containerLogs: (id: string): Promise> => + request.get(`/container/logs`, { params: { id } }), + // 清理容器 + containerPrune: (): Promise> => request.post(`/container/prune`), + // 获取网络列表 + networkList: (page: number, limit: number): Promise> => + request.get(`/container/network/list`, { params: { page, limit } }), + // 创建网络 + networkCreate: (config: any): Promise> => + request.post(`/container/network/create`, config), + // 删除网络 + networkRemove: (id: string): Promise> => + request.post(`/container/network/remove`, { id }), + // 检查网络是否存在 + networkExist: (id: string): Promise> => + request.get(`/container/network/exist`, { params: { id } }), + // 获取网络详情 + networkInspect: (id: string): Promise> => + request.get(`/container/network/inspect`, { params: { id } }), + // 连接容器到网络 + networkConnect: (config: any): Promise> => + request.post(`/container/network/connect`, config), + // 断开容器到网络的连接 + networkDisconnect: (config: any): Promise> => + request.post(`/container/network/disconnect`, config), + // 清理网络 + networkPrune: (): Promise> => request.post(`/container/network/prune`), + // 获取镜像列表 + imageList: (page: number, limit: number): Promise> => + request.get(`/container/image/list`, { params: { page, limit } }), + // 检查镜像是否存在 + imageExist: (id: string): Promise> => + request.get(`/container/image/exist`, { params: { id } }), + // 拉取镜像 + imagePull: (config: any): Promise> => + request.post(`/container/image/pull`, config), + // 删除镜像 + imageRemove: (id: string): Promise> => + request.post(`/container/image/remove`, { id }), + // 获取镜像详情 + imageInspect: (id: string): Promise> => + request.get(`/container/image/inspect`, { params: { id } }), + // 清理镜像 + imagePrune: (): Promise> => request.post(`/container/image/prune`), + // 获取卷列表 + volumeList: (page: number, limit: number): Promise> => + request.get(`/container/volume/list`, { params: { page, limit } }), + // 创建卷 + volumeCreate: (config: any): Promise> => + request.post(`/container/volume/create`, config), + // 检查卷是否存在 + volumeExist: (id: string): Promise> => + request.get(`/container/volume/exist`, { params: { id } }), + // 删除卷 + volumeRemove: (id: string): Promise> => + request.post(`/container/volume/remove`, { id }), + // 获取卷详情 + volumeInspect: (id: string): Promise> => + request.get(`/container/volume/inspect`, { params: { id } }), + // 清理卷 + volumePrune: (): Promise> => request.post(`/container/volume/prune`) +} diff --git a/web/src/api/panel/cron/index.ts b/web/src/api/panel/cron/index.ts new file mode 100644 index 00000000..110953cd --- /dev/null +++ b/web/src/api/panel/cron/index.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取任务列表 + list: (page: number, limit: number): Promise> => + request.get('/cron/list', { params: { page, limit } }), + // 获取任务脚本 + script: (id: number): Promise> => request.get('/cron/' + id), + // 添加任务 + add: (task: any): Promise> => request.post('/cron/add', task), + // 修改任务 + update: (id: number, name: string, time: string, script: string): Promise> => + request.put('/cron/' + id, { name, time, script }), + // 删除任务 + delete: (id: number): Promise> => request.delete('/cron/' + id), + // 获取任务日志 + log: (id: number): Promise> => request.get('/cron/log/' + id), + // 修改任务状态 + status: (id: number, status: boolean): Promise> => + request.post('/cron/status', { id, status }) +} diff --git a/web/src/api/panel/file/index.ts b/web/src/api/panel/file/index.ts new file mode 100644 index 00000000..431b9a23 --- /dev/null +++ b/web/src/api/panel/file/index.ts @@ -0,0 +1,59 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 创建文件/文件夹 + create: (path: string, dir: boolean): Promise> => + request.post('/file/create', { path, dir }), + // 获取文件内容 + content: (path: string): Promise> => + request.get('/file/content', { params: { path } }), + // 保存文件 + save: (path: string, content: string): Promise> => + request.post('/file/save', { path, content }), + // 删除文件 + delete: (path: string): Promise> => + request.post('/file/delete', { path }), + // 上传文件 + upload: (path: string, formData: FormData, onProgress: any): Promise> => { + formData.append('path', path) + return request.post('/file/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (progressEvent: any) => { + onProgress({ percent: Math.ceil((progressEvent.loaded / progressEvent.total) * 100) }) + } + }) + }, + // 移动文件 + move: (source: string, target: string): Promise> => + request.post('/file/move', { source, target }), + // 复制文件 + copy: (source: string, target: string): Promise> => + request.post('/file/copy', { source, target }), + // 远程下载 + remoteDownload: (url: string, path: string): Promise> => + request.post('/file/remoteDownload', { url, path }), + // 获取文件信息 + info: (path: string): Promise> => + request.get('/file/info', { params: { path } }), + // 修改文件权限 + permission: ( + path: string, + mode: string, + owner: string, + group: string + ): Promise> => + request.post('/file/permission', { path, mode, owner, group }), + // 压缩文件 + archive: (paths: string[], file: string): Promise> => + request.post('/file/archive', { paths, file }), + // 解压文件 + unArchive: (file: string, path: string): Promise> => + request.post('/file/unArchive', { file, path }), + // 搜索文件 + search: (keyword: string): Promise> => + request.post('/file/search', { keyword }), + // 获取文件列表 + list: (path: string, page: number, limit: number): Promise> => + request.get('/file/list', { params: { path, page, limit } }) +} diff --git a/web/src/api/panel/info/index.ts b/web/src/api/panel/info/index.ts new file mode 100644 index 00000000..acf1e9bb --- /dev/null +++ b/web/src/api/panel/info/index.ts @@ -0,0 +1,28 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 面板信息 + panel: (): Promise => fetch('/api/info/panel'), + // 面板菜单 + menu: (): Promise> => request.get('/info/menu'), + // 首页插件 + homePlugins: (): Promise> => request.get('/info/homePlugins'), + // 实时监控 + nowMonitor: (): Promise> => request.get('/info/nowMonitor'), + // 系统信息 + systemInfo: (): Promise> => request.get('/info/systemInfo'), + // 统计信息 + countInfo: (): Promise> => request.get('/info/countInfo'), + // 已安装的数据库和PHP + installedDbAndPhp: (): Promise> => + request.get('/info/installedDbAndPhp'), + // 检查更新 + checkUpdate: (): Promise> => request.get('/info/checkUpdate'), + // 更新日志 + updateInfo: (): Promise> => request.get('/info/updateInfo'), + // 更新面板 + update: (): Promise> => request.post('/info/update', null), + // 重启面板 + restart: (): Promise> => request.post('/info/restart') +} diff --git a/web/src/api/panel/monitor/index.ts b/web/src/api/panel/monitor/index.ts new file mode 100644 index 00000000..e578bc5d --- /dev/null +++ b/web/src/api/panel/monitor/index.ts @@ -0,0 +1,18 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 开关 + switch: (monitor: boolean): Promise> => + request.post('/monitor/switch', { monitor }), + // 保存天数 + saveDays: (days: number): Promise> => + request.post('/monitor/saveDays', { days }), + // 清空监控记录 + clear: (): Promise> => request.post('/monitor/clear'), + // 监控记录 + list: (start: number, end: number): Promise> => + request.get('/monitor/list', { params: { start, end } }), + // 开关和天数 + switchAndDays: (): Promise> => request.get('/monitor/switchAndDays') +} diff --git a/web/src/api/panel/plugin/index.ts b/web/src/api/panel/plugin/index.ts new file mode 100644 index 00000000..59ea5823 --- /dev/null +++ b/web/src/api/panel/plugin/index.ts @@ -0,0 +1,23 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取插件列表 + list: (page: number, limit: number): Promise> => + request.get('/plugin/list', { params: { page, limit } }), + // 安装插件 + install: (slug: string): Promise> => + request.post('/plugin/install', { slug }), + // 卸载插件 + uninstall: (slug: string): Promise> => + request.post('/plugin/uninstall', { slug }), + // 更新插件 + update: (slug: string): Promise> => + request.post('/plugin/update', { slug }), + // 设置首页显示 + updateShow: (slug: string, show: boolean): Promise> => + request.post('/plugin/updateShow', { slug, show }), + // 插件是否已安装 + isInstalled: (slug: string): Promise> => + request.get('/plugin/isInstalled', { params: { slug } }) +} diff --git a/web/src/api/panel/safe/index.ts b/web/src/api/panel/safe/index.ts new file mode 100644 index 00000000..df11e7cf --- /dev/null +++ b/web/src/api/panel/safe/index.ts @@ -0,0 +1,34 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取防火墙状态 + firewallStatus: (): Promise> => request.get('/safe/firewallStatus'), + // 设置防火墙状态 + setFirewallStatus: (status: boolean): Promise> => + request.post('/safe/firewallStatus', { status }), + // 获取防火墙规则 + firewallRules: (page: number, limit: number): Promise> => + request.get('/safe/firewallRules', { params: { page, limit } }), + // 添加防火墙规则 + addFirewallRule: (port: string, protocol: string): Promise> => + request.post('/safe/firewallRules', { port, protocol }), + // 删除防火墙规则 + deleteFirewallRule: (port: string, protocol: string): Promise> => + request.delete('/safe/firewallRules', { data: { port, protocol } }), + // 获取SSH状态 + sshStatus: (): Promise> => request.get('/safe/sshStatus'), + // 设置SSH状态 + setSshStatus: (status: boolean): Promise> => + request.post('/safe/sshStatus', { status }), + // 获取SSH端口 + sshPort: (): Promise> => request.get('/safe/sshPort'), + // 设置SSH端口 + setSshPort: (port: number): Promise> => + request.post('/safe/sshPort', { port }), + // 获取Ping状态 + pingStatus: (): Promise> => request.get('/safe/pingStatus'), + // 设置Ping状态 + setPingStatus: (status: boolean): Promise> => + request.post('/safe/pingStatus', { status }) +} diff --git a/web/src/api/panel/setting/index.ts b/web/src/api/panel/setting/index.ts new file mode 100644 index 00000000..1384ec4f --- /dev/null +++ b/web/src/api/panel/setting/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取设置 + list: (): Promise> => request.get('/setting/list'), + // 保存设置 + update: (settings: any): Promise> => + request.post('/setting/update', settings), + // 获取HTTPS设置 + getHttps: (): Promise> => request.get('/setting/https'), + // 保存HTTPS设置 + updateHttps: (https: any): Promise> => + request.post('/setting/https', https) +} diff --git a/web/src/api/panel/ssh/index.ts b/web/src/api/panel/ssh/index.ts new file mode 100644 index 00000000..642c7335 --- /dev/null +++ b/web/src/api/panel/ssh/index.ts @@ -0,0 +1,14 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取信息 + info: (): Promise> => request.get('/ssh/info'), + // 保存信息 + saveInfo: ( + host: string, + port: number, + user: string, + password: string + ): Promise> => request.post('/ssh/info', { host, port, user, password }) +} diff --git a/web/src/api/panel/system/service/index.ts b/web/src/api/panel/system/service/index.ts new file mode 100644 index 00000000..343b2416 --- /dev/null +++ b/web/src/api/panel/system/service/index.ts @@ -0,0 +1,29 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 服务状态 + status: (service: string): Promise> => + request.get('/system/service/status', { params: { service } }), + // 是否启用服务 + isEnabled: (service: string): Promise> => + request.get('/system/service/isEnabled', { params: { service } }), + // 启用服务 + enable: (service: string): Promise> => + request.post('/system/service/enable', { service }), + // 禁用服务 + disable: (service: string): Promise> => + request.post('/system/service/disable', { service }), + // 重启服务 + restart: (service: string): Promise> => + request.post('/system/service/restart', { service }), + // 重载服务 + reload: (service: string): Promise> => + request.post('/system/service/reload', { service }), + // 启动服务 + start: (service: string): Promise> => + request.post('/system/service/start', { service }), + // 停止服务 + stop: (service: string): Promise> => + request.post('/system/service/stop', { service }) +} diff --git a/web/src/api/panel/task/index.ts b/web/src/api/panel/task/index.ts new file mode 100644 index 00000000..1af7d840 --- /dev/null +++ b/web/src/api/panel/task/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取状态 + status: (): Promise> => request.get('/task/status'), + // 获取任务列表 + list: (page: number, limit: number): Promise> => + request.get('/task/list', { params: { page, limit } }), + // 获取任务日志 + log: (id: number): Promise> => + request.get('/task/log', { params: { id } }), + // 删除任务 + delete: (id: number): Promise> => request.post('/task/delete', { id }) +} diff --git a/web/src/api/panel/user/index.ts b/web/src/api/panel/user/index.ts new file mode 100644 index 00000000..a0be3c30 --- /dev/null +++ b/web/src/api/panel/user/index.ts @@ -0,0 +1,17 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 登录 + login: (username: string, password: string): Promise> => + request.post('/user/login', { + username, + password + }), + // 登出 + logout: (): Promise> => request.post('/user/logout'), + // 是否登录 + isLogin: (): Promise> => request.get('/user/isLogin'), + // 获取用户信息 + info: (): Promise> => request.get('/user/info') +} diff --git a/web/src/api/panel/website/index.ts b/web/src/api/panel/website/index.ts new file mode 100644 index 00000000..1c8dca05 --- /dev/null +++ b/web/src/api/panel/website/index.ts @@ -0,0 +1,50 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 列表 + list: (page: number, limit: number): Promise> => + request.get('/websites', { params: { page, limit } }), + // 添加 + add: (data: any): Promise> => request.post('/websites', data), + // 删除 + delete: (data: any): Promise> => request.post('/websites/delete', data), + // 获取默认配置 + defaultConfig: (): Promise> => request.get('/website/defaultConfig'), + // 保存默认配置 + saveDefaultConfig: (index: string, stop: string): Promise> => + request.post('/website/defaultConfig', { index, stop }), + // 网站配置 + config: (id: number): Promise> => + request.get('/websites/' + id + '/config'), + // 保存网站配置 + saveConfig: (id: number, data: any): Promise> => + request.post('/websites/' + id + '/config', data), + // 清空日志 + clearLog: (id: number): Promise> => + request.delete('/websites/' + id + '/log'), + // 更新备注 + updateRemark: (id: number, remark: string): Promise> => + request.post('/websites/' + id + '/updateRemark', { remark }), + // 获取备份列表 + backupList: (page: number, limit: number): Promise> => + request.get('/website/backupList', { params: { page, limit } }), + // 创建备份 + createBackup: (id: number): Promise> => + request.post('/websites/' + id + '/createBackup', {}), + // 上传备份 + uploadBackup: (data: any): Promise> => + request.put('/website/uploadBackup', data), + // 删除备份 + deleteBackup: (name: string): Promise> => + request.delete('/website/deleteBackup', { data: { name } }), + // 恢复备份 + restoreBackup: (id: number, name: number): Promise> => + request.post('/websites/' + id + '/restoreBackup', { name }), + // 重置配置 + resetConfig: (id: number): Promise> => + request.post('/websites/' + id + '/resetConfig'), + // 修改状态 + status: (id: number, status: boolean): Promise> => + request.post('/websites/' + id + '/status', { status }) +} diff --git a/web/src/api/plugins/fail2ban/index.ts b/web/src/api/plugins/fail2ban/index.ts new file mode 100644 index 00000000..3bf8e67e --- /dev/null +++ b/web/src/api/plugins/fail2ban/index.ts @@ -0,0 +1,24 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 保护列表 + jails: (page: number, limit: number): Promise> => + request.get('/plugins/fail2ban/jails', { params: { page, limit } }), + // 添加保护 + add: (data: any): Promise> => request.post('/plugins/fail2ban/jails', data), + // 删除保护 + delete: (name: string): Promise> => + request.delete('/plugins/fail2ban/jails', { params: { name } }), + // 封禁列表 + jail: (name: string): Promise> => + request.get('/plugins/fail2ban/jails/' + name), + // 解封 IP + unban: (name: string, ip: string): Promise> => + request.post('/plugins/fail2ban/unban', { name, ip }), + // 获取白名单 + whitelist: (): Promise> => request.get('/plugins/fail2ban/whiteList'), + // 设置白名单 + setWhitelist: (ip: string): Promise> => + request.post('/plugins/fail2ban/whiteList', { ip }) +} diff --git a/web/src/api/plugins/frp/index.ts b/web/src/api/plugins/frp/index.ts new file mode 100644 index 00000000..dfe43919 --- /dev/null +++ b/web/src/api/plugins/frp/index.ts @@ -0,0 +1,11 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取配置 + config: (service: string): Promise> => + request.get('/plugins/frp/config', { params: { service } }), + // 保存配置 + saveConfig: (service: string, config: string): Promise> => + request.post('/plugins/frp/config', { service, config }) +} diff --git a/web/src/api/plugins/gitea/index.ts b/web/src/api/plugins/gitea/index.ts new file mode 100644 index 00000000..0b7005d9 --- /dev/null +++ b/web/src/api/plugins/gitea/index.ts @@ -0,0 +1,10 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取配置 + config: (): Promise> => request.get('/plugins/gitea/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/gitea/config', { config }) +} diff --git a/web/src/api/plugins/mysql/index.ts b/web/src/api/plugins/mysql/index.ts new file mode 100644 index 00000000..81622432 --- /dev/null +++ b/web/src/api/plugins/mysql/index.ts @@ -0,0 +1,63 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/mysql/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/mysql/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/mysql/config', { config }), + // 获取错误日志 + errorLog: (): Promise> => request.get('/plugins/mysql/errorLog'), + // 清空错误日志 + clearErrorLog: (): Promise> => request.post('/plugins/mysql/clearErrorLog'), + // 获取慢查询日志 + slowLog: (): Promise> => request.get('/plugins/mysql/slowLog'), + // 清空慢查询日志 + clearSlowLog: (): Promise> => request.post('/plugins/mysql/clearSlowLog'), + // 获取 root 密码 + rootPassword: (): Promise> => request.get('/plugins/mysql/rootPassword'), + // 修改 root 密码 + setRootPassword: (password: string): Promise> => + request.post('/plugins/mysql/rootPassword', { password }), + // 数据库列表 + databases: (page: number, limit: number): Promise> => + request.get('/plugins/mysql/databases', { params: { page, limit } }), + // 创建数据库 + addDatabase: (database: any): Promise> => + request.post('/plugins/mysql/databases', database), + // 删除数据库 + deleteDatabase: (database: string): Promise> => + request.delete('/plugins/mysql/databases', { params: { database } }), + // 备份列表 + backups: (page: number, limit: number): Promise> => + request.get('/plugins/mysql/backups', { params: { page, limit } }), + // 创建备份 + createBackup: (database: string): Promise> => + request.post('/plugins/mysql/backups', { database }), + // 上传备份 + uploadBackup: (backup: any): Promise> => + request.put('/plugins/mysql/backups', backup), + // 删除备份 + deleteBackup: (name: string): Promise> => + request.delete('/plugins/mysql/backups', { params: { name } }), + // 还原备份 + restoreBackup: (backup: string, database: string): Promise> => + request.post('/plugins/mysql/backups/restore', { backup, database }), + // 用户列表 + users: (page: number, limit: number): Promise> => + request.get('/plugins/mysql/users', { params: { page, limit } }), + // 创建用户 + addUser: (user: any): Promise> => request.post('/plugins/mysql/users', user), + // 删除用户 + deleteUser: (user: string): Promise> => + request.delete('/plugins/mysql/users', { params: { user } }), + // 设置用户密码 + setUserPassword: (user: string, password: string): Promise> => + request.post('/plugins/mysql/users/password', { user, password }), + // 设置用户权限 + setUserPrivileges: (user: string, database: string): Promise> => + request.post('/plugins/mysql/users/privileges', { user, database }) +} diff --git a/web/src/api/plugins/openresty/index.ts b/web/src/api/plugins/openresty/index.ts new file mode 100644 index 00000000..5915a04a --- /dev/null +++ b/web/src/api/plugins/openresty/index.ts @@ -0,0 +1,16 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/openresty/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/openresty/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/openresty/config', { config }), + // 获取错误日志 + errorLog: (): Promise> => request.get('/plugins/openresty/errorLog'), + // 清空错误日志 + clearErrorLog: (): Promise> => request.post('/plugins/openresty/clearErrorLog') +} diff --git a/web/src/api/plugins/php/index.ts b/web/src/api/plugins/php/index.ts new file mode 100644 index 00000000..c56578e0 --- /dev/null +++ b/web/src/api/plugins/php/index.ts @@ -0,0 +1,41 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (version: number): Promise> => + request.get(`/plugins/php/${version}/load`), + // 获取配置 + config: (version: number): Promise> => + request.get(`/plugins/php/${version}/config`), + // 保存配置 + saveConfig: (version: number, config: string): Promise> => + request.post(`/plugins/php/${version}/config`, { config }), + // 获取FPM配置 + fpmConfig: (version: number): Promise> => + request.get(`/plugins/php/${version}/fpmConfig`), + // 保存FPM配置 + saveFPMConfig: (version: number, config: string): Promise> => + request.post(`/plugins/php/${version}/fpmConfig`, { config }), + // 获取错误日志 + errorLog: (version: number): Promise> => + request.get(`/plugins/php/${version}/errorLog`), + // 清空错误日志 + clearErrorLog: (version: number): Promise> => + request.post(`/plugins/php/${version}/clearErrorLog`), + // 获取慢日志 + slowLog: (version: number): Promise> => + request.get(`/plugins/php/${version}/slowLog`), + // 清空慢日志 + clearSlowLog: (version: number): Promise> => + request.post(`/plugins/php/${version}/clearSlowLog`), + // 拓展列表 + extensions: (version: number): Promise> => + request.get(`/plugins/php/${version}/extensions`), + // 安装拓展 + installExtension: (version: number, slug: string): Promise> => + request.post(`/plugins/php/${version}/extensions`, { slug }), + // 卸载拓展 + uninstallExtension: (version: number, slug: string): Promise> => + request.delete(`/plugins/php/${version}/extensions`, { params: { slug } }) +} diff --git a/web/src/api/plugins/phpmyadmin/index.ts b/web/src/api/plugins/phpmyadmin/index.ts new file mode 100644 index 00000000..5df266d3 --- /dev/null +++ b/web/src/api/plugins/phpmyadmin/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取信息 + info: (): Promise> => request.get('/plugins/phpmyadmin/info'), + // 设置端口 + port: (port: number): Promise> => + request.post('/plugins/phpmyadmin/port', { port }), + // 获取配置 + getConfig: (): Promise> => request.get('/plugins/phpmyadmin/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/phpmyadmin/config', { config }) +} diff --git a/web/src/api/plugins/podman/index.ts b/web/src/api/plugins/podman/index.ts new file mode 100644 index 00000000..931958bb --- /dev/null +++ b/web/src/api/plugins/podman/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取注册表配置 + registryConfig: (): Promise> => request.get('/plugins/podman/registryConfig'), + // 保存注册表配置 + saveRegistryConfig: (config: string): Promise> => + request.post('/plugins/podman/registryConfig', { config }), + // 获取存储配置 + storageConfig: (): Promise> => request.get('/plugins/podman/storageConfig'), + // 保存存储配置 + saveStorageConfig: (config: string): Promise> => + request.post('/plugins/podman/storageConfig', { config }) +} diff --git a/web/src/api/plugins/postgresql/index.ts b/web/src/api/plugins/postgresql/index.ts new file mode 100644 index 00000000..686ecf39 --- /dev/null +++ b/web/src/api/plugins/postgresql/index.ts @@ -0,0 +1,57 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/postgresql/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/postgresql/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/postgresql/config', { config }), + // 获取用户配置 + userConfig: (): Promise> => request.get('/plugins/postgresql/userConfig'), + // 保存配置 + saveUserConfig: (config: string): Promise> => + request.post('/plugins/postgresql/userConfig', { config }), + // 获取日志 + log: (): Promise> => request.get('/plugins/postgresql/log'), + // 清空错误日志 + clearLog: (): Promise> => request.post('/plugins/postgresql/clearLog'), + // 数据库列表 + databases: (page: number, limit: number): Promise> => + request.get('/plugins/postgresql/databases', { params: { page, limit } }), + // 创建数据库 + addDatabase: (database: any): Promise> => + request.post('/plugins/postgresql/databases', database), + // 删除数据库 + deleteDatabase: (database: string): Promise> => + request.delete('/plugins/postgresql/databases', { params: { database } }), + // 备份列表 + backups: (page: number, limit: number): Promise> => + request.get('/plugins/postgresql/backups', { params: { page, limit } }), + // 创建备份 + createBackup: (database: string): Promise> => + request.post('/plugins/postgresql/backups', { database }), + // 上传备份 + uploadBackup: (backup: any): Promise> => + request.put('/plugins/postgresql/backups', backup), + // 删除备份 + deleteBackup: (name: string): Promise> => + request.delete('/plugins/postgresql/backups', { params: { name } }), + // 还原备份 + restoreBackup: (backup: string, database: string): Promise> => + request.post('/plugins/postgresql/backups/restore', { backup, database }), + // 角色列表 + roles: (page: number, limit: number): Promise> => + request.get('/plugins/postgresql/roles', { params: { page, limit } }), + // 创建角色 + addRole: (user: any): Promise> => + request.post('/plugins/postgresql/roles', user), + // 删除角色 + deleteRole: (user: string): Promise> => + request.delete('/plugins/postgresql/roles', { params: { user } }), + // 设置角色密码 + setRolePassword: (user: string, password: string): Promise> => + request.post('/plugins/postgresql/roles/password', { user, password }) +} diff --git a/web/src/api/plugins/pureftpd/index.ts b/web/src/api/plugins/pureftpd/index.ts new file mode 100644 index 00000000..79bad008 --- /dev/null +++ b/web/src/api/plugins/pureftpd/index.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 列表 + list: (page: number, limit: number): Promise> => + request.get('/plugins/pureftpd/list', { params: { page, limit } }), + // 添加 + add: (username: string, password: string, path: string): Promise> => + request.post('/plugins/pureftpd/add', { username, password, path }), + // 删除 + delete: (username: string): Promise> => + request.delete('/plugins/pureftpd/delete', { params: { username } }), + // 修改密码 + changePassword: (username: string, password: string): Promise> => + request.post('/plugins/pureftpd/changePassword', { username, password }), + // 获取端口 + port: (): Promise> => request.get('/plugins/pureftpd/port'), + // 修改端口 + setPort: (port: number): Promise> => + request.post('/plugins/pureftpd/port', { port }) +} diff --git a/web/src/api/plugins/redis/index.ts b/web/src/api/plugins/redis/index.ts new file mode 100644 index 00000000..8e3d6324 --- /dev/null +++ b/web/src/api/plugins/redis/index.ts @@ -0,0 +1,12 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/redis/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/redis/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/redis/config', { config }) +} diff --git a/web/src/api/plugins/rsync/index.ts b/web/src/api/plugins/rsync/index.ts new file mode 100644 index 00000000..22708131 --- /dev/null +++ b/web/src/api/plugins/rsync/index.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取配置 + config: (): Promise> => request.get('/plugins/rsync/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/rsync/config', { config }), + // 模块列表 + modules: (page: number, limit: number): Promise> => + request.get('/plugins/rsync/modules', { params: { page, limit } }), + // 添加模块 + addModule: (module: any): Promise> => + request.post('/plugins/rsync/modules', module), + // 删除模块 + deleteModule: (name: string): Promise> => + request.delete('/plugins/rsync/modules/' + name), + // 更新模块 + updateModule: (name: string, module: any): Promise> => + request.post('/plugins/rsync/modules/' + name, module) +} diff --git a/web/src/api/plugins/s3fs/index.ts b/web/src/api/plugins/s3fs/index.ts new file mode 100644 index 00000000..c8c912e8 --- /dev/null +++ b/web/src/api/plugins/s3fs/index.ts @@ -0,0 +1,12 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 列表 + list: (page: number, limit: number): Promise> => + request.get('/plugins/s3fs/list', { params: { page, limit } }), + // 添加 + add: (data: any): Promise> => request.post('/plugins/s3fs/add', data), + // 删除 + delete: (id: number): Promise> => request.post('/plugins/s3fs/delete', { id }) +} diff --git a/web/src/api/plugins/supervisor/index.ts b/web/src/api/plugins/supervisor/index.ts new file mode 100644 index 00000000..29da895b --- /dev/null +++ b/web/src/api/plugins/supervisor/index.ts @@ -0,0 +1,48 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 服务名称 + service: (): Promise> => request.get('/plugins/supervisor/service'), + // 负载状态 + load: (): Promise> => request.get('/plugins/supervisor/load'), + // 获取错误日志 + log: (): Promise> => request.get('/plugins/supervisor/log'), + // 清空错误日志 + clearLog: (): Promise> => request.post('/plugins/supervisor/clearLog'), + // 获取配置 + config: (): Promise> => request.get('/plugins/supervisor/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/supervisor/config', { config }), + // 进程列表 + processes: (page: number, limit: number): Promise> => + request.get('/plugins/supervisor/processes', { params: { page, limit } }), + // 进程启动 + startProcess: (process: string): Promise> => + request.post('/plugins/supervisor/startProcess', { process }), + // 进程停止 + stopProcess: (process: string): Promise> => + request.post('/plugins/supervisor/stopProcess', { process }), + // 进程重启 + restartProcess: (process: string): Promise> => + request.post('/plugins/supervisor/restartProcess', { process }), + // 进程日志 + processLog: (process: string): Promise> => + request.get('/plugins/supervisor/processLog', { params: { process } }), + // 清空进程日志 + clearProcessLog: (process: string): Promise> => + request.post('/plugins/supervisor/clearProcessLog', { process }), + // 进程配置 + processConfig: (process: string): Promise> => + request.get('/plugins/supervisor/processConfig', { params: { process } }), + // 保存进程配置 + saveProcessConfig: (process: string, config: string): Promise> => + request.post('/plugins/supervisor/processConfig', { process, config }), + // 添加进程 + addProcess: (process: any): Promise> => + request.post('/plugins/supervisor/addProcess', process), + // 删除进程 + deleteProcess: (process: string): Promise> => + request.post('/plugins/supervisor/deleteProcess', { process }) +} diff --git a/web/src/api/plugins/toolbox/index.ts b/web/src/api/plugins/toolbox/index.ts new file mode 100644 index 00000000..51939554 --- /dev/null +++ b/web/src/api/plugins/toolbox/index.ts @@ -0,0 +1,28 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // DNS + dns: (): Promise> => request.get('/plugins/toolbox/dns'), + // 设置 DNS + setDns: (dns1: string, dns2: string): Promise> => + request.post('/plugins/toolbox/dns', { dns1, dns2 }), + // SWAP + swap: (): Promise> => request.get('/plugins/toolbox/swap'), + // 设置 SWAP + setSwap: (size: number): Promise> => + request.post('/plugins/toolbox/swap', { size }), + // 时区 + timezone: (): Promise> => request.get('/plugins/toolbox/timezone'), + // 设置时区 + setTimezone: (timezone: string): Promise> => + request.post('/plugins/toolbox/timezone', { timezone }), + // Hosts + hosts: (): Promise> => request.get('/plugins/toolbox/hosts'), + // 设置 Hosts + setHosts: (hosts: string): Promise> => + request.post('/plugins/toolbox/hosts', { hosts }), + // 设置 Root 密码 + setRootPassword: (password: string): Promise> => + request.post('/plugins/toolbox/rootPassword', { password }) +} diff --git a/web/src/assets/images/404.webp b/web/src/assets/images/404.webp new file mode 100644 index 0000000000000000000000000000000000000000..3482bf96530efee4fbb405eea59b32a875c0e575 GIT binary patch literal 14344 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 Tprf~=qKP8a;Q#&`1^B-JkclP* literal 0 HcmV?d00001 diff --git a/web/src/assets/images/login_banner.png b/web/src/assets/images/login_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..cd29ced45ccb69b52bc92972d3fce388f8ffe31e GIT binary patch literal 415646 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 k{^~ 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%0D(r2@Xdls_08OEf0Z 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

4$g&Z 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

F&L1zivHQHm}P5!)NIX&xmV8!dE3ThHV4xI z7}o&iz>yN1I1{=**V)lguFnSbsK>1hP1DfU)QWDWf1Z}OY2!=v1O%t|l!7?qBiX|*AE@EnW z2E)Tc7#teF`1k}C=F=#aiXWrePdMk29iy4UqQxoKnnKxcPnNpPVadho%w z?A-F?cb-Hf9>te(U&h{YNQ_Jj1dkj(y6ODI^N-EX%|A*?ZHdQY(fDqqe3PsG%&tZCE3d6&r7#$hG z!qyR(3=(3K8QPXM|`#ptXS`;`a4*Gk$(c8Nbubg}ZvkQct1`Q0>6&Gr?=A7SZoHKfjpStZ>9D*d`VTR6R2otFy zz;s7|y_4KT-1HyXL;}*xG?|B!9h<5JY zU2bpt?#R^C3x&UO?G2kjtDb%$LQK(oI6mogY&809rI*Ln|E@F6S2-8#3GLuF4y zfV6EeLx2kg&ZLJVIhax)2xx|@29}7&(ALp|XJ0yx(b+uqt&c)y3Y^xow|L5#jNY2U zBZRB1G%K>uTtF$nU?w733A;9Q;KV>0rXqlI96Ns=BUA6huI>hx^Qmbdu6aOu8IlXN z48X|ynWYpM7<>w_{{QT~XP9Kybsf6)IrrYmxjIzm?&-a(K37Nfv`hili7sQWOK2kO#oz(9_-1Ip^-mRX3b--j92$ zx~DON90ow`@56_gn(nGwH=KRe-fOSLb$soKqxjn=pG0$a2P%_ONXHZ4u0iB$W5;p9 z2qA%_JJ$iFU`d6wLK*9Y5^U)}N`R9F$q1Y&1ybu*?cZB!tOku}#A^e578aMCy?ggg z51t!5TPc-paD+4<6`7`q@zGJ_H?oK|r9Imlc%`5=VKd z-O87HJ~xlY+iWs|Ai(GIV z%wOLhc!fvDaWFGGgUPW;2;m?S4WqNI9nomi9YcBHZ&(Kq$}74`AyKJRu(Y^2*=!zX&Ys2e%ru~Ya=C(Hp@2d@k9;AIN~HqRvLI|nNu?y9Y~rlM z8JqX}gQt_J)MGu}-OudWy?ZJaj~0&)42iIzU)H?sQn83kdmBxSPU&aPoQXaC^xi`22>; ze0weYqdhSosQ@ViY|BQzP{88S0>;M1FgQ4bk+BiX&(4Z$Hs_dCt1P9HCC(NM-8T^k z_|Jz!k%?3?J)LYy&ShF!bImO+m8N9UYHIFs7M5nzwara9J#bc4d;3tWRw1OoJ@0tu zn>uGS0O1=Mjt$$inOU{{jw1|$P#Om!VGW5OK_>~W6l_S8$&VxRAsiWB!NJ?#jeGCC z7tvr43JhRC*vA0kNJL_YCfjgoViwt|g+!1*!tF3hwP}HO@g>n)U`cFN;E3Rm@H%}u z1drqVY7n3P;XDRstGN2w>+qYO`Z)IV_dyC9Tyux-U5YFB!JGF$8=*MB-rp z4n`Iw3RVnFyy96&@XcdX%9BOETti(1NIxf~g9tFCgkmPH>nP*#w8Gg{S0G?!c?n;6 z_F1%c-HQ%h^J1hr&7n3!>D1M86i9J5X+c0}8gg0$KRW&_zVLTnKyP~nejhM%zJm6q zCU4I{7u)B(3cF5EG^b|PssO3n`>i4smevYb%NAV^!kV>-RPY&k*01YRZK*`Lu@E$( z5wA_Owlv9uhYqd(_(wlJx4OEzB!nI3#55@+=I0kMKR=H|It3w>H+M#t1OqRH_W2w- zu!AYt6v46Fg-nu^XJdw%4@C#*ElTU zojWtG6oSz(_Fi=*=9gwsE|#F{8VZF1{`61(6rG)&xap>w z;5g26jO4dlM!eXLZ;durbq>Y**o07F2$zfe-uyMW8f%P16z z*t>5p`uqDmg>VKX)hlNC)Q{b}e&tFTgTq6Zo1X)qkVquZyJH7}{@|te4_oIPYA#lu zD2(F>R4XQymzQw%)EOK(augF&ld4oHlax}~wu9BxRmvCh&=`Y|3c_*V*iuPp!*Lv! zzyp$u;4q>h<9tmwjLC2~biApl<#1nj@7Y~@cCH+_>dNvr|NgreTUl2>`ts*4+qLAx z$>UgFUSZ2C^Tvr2r<)#s{D~W9rl%gVOzWVg8JU*m=D-zK?q@sucavGJU~Fs@GqW>T zU0Ff7R7N-&MW(F{J9hM=si_%;;RmbRbZomMuLuiz1qZ3%*bWM%B9>Q{F)}iOvu96Z zcz6VJv$Har&pKwsEGwzjxW=c$q0mq$5*~=f6Jx1#VzxEiy4u{@UTSOYFxxwug=tmg zu_MP|OQ!zhPyQHx`mg^Dq~DMG-hJO&CSNoF;Tsszag=R4%yFE6f-n?QOw$;J{R}}v zg2U~wIWRD_VdBSURuReU#d{xo0Ikg_KnO?9#lDP}Irg4B1ma+m>;fJO(IF37a_*}4cImh!YW1H1bNdVUr`kN#U?eE0U<4bUa z1Fq}%?z2a60|g`U;8}5%>mqe z!xi}9_a25O1=?C#K=P$XZok|G*b}_*b`Sb@ZDte?#Mwfi zC6mG4EB52Wkz?+BHe;BYn!y{j=!Svzjt+Ep zcOl?6o?8J|TO?g;FQ}TOmVzT3R7xc*E-m5onKO9u@KYEb8kV_S4vr%nxBe-}tX7$6 zR>@UZ@H9)HgqQFIz)Z%d$T)52hHoYkikwTNlP6nSTL$`f^w0Hnb{7KSfIU4utAhRj z0)~EBt{)Ho=wT$1NeCfWu~77%I(0g8^yrb>XJ%#|G%MAEy5UPVHKp{ud-kx_=2k2$ zE@EME5xGJZmTf@*&;vfSx3^>8-o5DQ>P93Ifu?ECxis`jpHJmdtA!&_EmyIgUBmeJ z1WuhgiPL9KtC{I(mEFh+)3nM`i4D%Q$zV8iHWrJVOs7&qEiJ9{%`Gk2rlxeYIo)J; z?e3G;?7K?-=-Weh^3<1c{Madc`}h9|KK$Vi`z@C%8i4Q(j8HVf7=tF22rI|{ zNO4^wgbhMKGohG*gpOr1fhUIYSS(RNS*hXMm<9Kti9e{%$FTgEo+ zJfH{+1WB$LJ4puSNF>+>?%x~4_^ia?Q3XLE%&Zph>F*szTXPq#Xl{aG=D|q16d@*I z2?rYUgDCs{**$&g z%&F7GO8FWoM2k{DIbXum1^WxzxL(FP~}2I7g=LmAc*Pd8LVeFpcD`Y1YsBkl%o&~2C#egZtUE-6RA`R zKA#_)Gtf(ffR`%0yM<^8X-im+iCi|1`RREK3=QDqiQ_mwGNP7OmPN%htI7p}Q{iCb zOgtVx-qO-K(3Z)}wRg5>qv1#;9*c|i){JWD?NHrqo%rsJn0?!5aT z{=r9n9^pU`(o?*wR{*yds?$Z%Sxu_CR4N0)fh`0q+kx`A;xlg*h)~OW^<)Zi1!1Ix z&ZZ>ZbH{$HnbR1VDM5g6_}B^j$4U{u_2Gwbb#EI&ehrpYg@giWemKyeX$CA?Vx>^T zQ%6tX3qLxHvC%%P}B@ptZ z0_Im%QMRm_OFf7HMoeiwZEff7U2Bb1pb?FDZKJcZQ|{iqdp(s%3~#KjubaYYQUJ3Z z8?)0hSX)^`Pk$1&Bi-(PZI=``y#8VJTV07d> z`ucj&+};KurE8S_DkM1~f)o-$3dEvuTzB0;-Xo5W22k&|$1uCDo40vMl)gL1Z%RV4OiJ2~OPWlD;y| zih_hx61Ht&dS(hoj~>C&;v6_o;mzTyZcARG}PkdGLFK#~YW&iRV2>)B8^ zv=I)4XQR=`WGb0nZfQ#A+B&+5@mR!JFXUw^l~UjR_IGgTnyc`o&tIO;#OYI~@c;eE z|HQ*T`YAhqemH#g%$fe7!E^U5EH2#V*v?)8#SEWMkH_PX!olR^By1@W2!;@kN1<_! zY<2^NVW6w43w!qLL2F9}fj|(P^Op_=FBHn(at&RYvSZsQ6^mG2TE_YF=W+7naSWUt z!2HsZD3yzr zg;^~3Ea8EBZ++XWgAG9V21dZ|Q<}yVF(M@uBdL_Z6@~o_22*ep!e(s+b5rijN zvLM^=rPC(HmkMZ3r|_vy{1W;)o1p}OkxLU)Tju`An|bhX0GM=Z%K`#I3FHe!tgWqW zDw!(fQXoma=$*&~-ObA;V=d9xH?o}l2)A60B7@nL^$X0>x z4GiJjpM3@o+;ux{-oFp+u?S*@4**cIs#wge;LPMC9yxvvXGcbn4(0KS?~dU9y(MU| zWjr=UI67X1&Ehz8MGq1_3&IxQjGo_M{uKg2@@yuQA}C@I;8-h^u(rMd=?Hgr1QH}Y zUk%b1t~+?ma$^-}L?d3~JQMJ%rslNO(bY9MF)=YGrRrnkZe+6?>sXkdM|a;2k8R?P zA8g;OkpkZCz5Gp@quf$7^$N4z`4P5_bSjP8Z@&`*XU<_`V*~kI4#$rl!(GV)G|lIV zv_W2=p3N>|J8=Qq}gc06(r&b?A^5oeO*29`}}Tk!z)-TZ=>K@(Lo9amhGTgE+d=W!2IF@ zCMU*mZg2?a&yQefeqI&|1;=*mA_L9%LxE$>&CO57;;|`VJIoOx2*BW+gBYrsPcP`2 zp7;5JrF1M-O(o;ySUgdRrxWJl+_KaS13`a4{g?m#|It7FC;t>(-Cg+I-~HXo@?QVx zBahW+-pVBWTeZ?k_`>u3q(z9*F9DrD*@ zoE|FU)cFNSDe&O?-izCAy|rGQ0o!6;PutDOFWgBe>G~0F6t3)QMl#xmz2g-e8=Au8!a5e`2k=*)AI1;k3AClsNQNR% zQld~QVrhK^8)goPWDxgU-Hx00@5YsF8hmmUb0r@hIW>co)g1QrUyZAF?f{1Zk?vYw zyk;eTRj>Ef44bRA#O!hws~dSwiB;82Of*d^8Ge2I-kWd8HdcW~G~%@fGDAZ{qNlrO z@`(T0!E(8L4Kea536zQ@OixYWs)N_T=MTY=3Y=Z&@v6`OgBPxL7un8knT?Z5;fgD- z#K9X5;U_=(F#uq8W(w1j6WFt7KWyRI1#Ky6)|TcAx5est3b)Ul0hCH%=bpVN6mvLv z@+5?`!HJO1=kdAEeGZ0U;A0>CD59}gecq2T=7Kr({2jX*CA0so7}j=Sbn8n9t+v*)uqN_$e$eErE!kDcy|jo*o2zK3J9o z1qH@6cYvQ<4?5d%RVS;J3bMH@R+d*VJv)uD@i9z{Pheqj5&3M^u}sSnQsf!q)4@RC znU=Q9qdR*0PIYv2t}%_IQi?OinZb2%4UR7mbbMjI6HP`PzpbmF&rqY2lQIwp;A?;L zRr>fRe?#5(o(FL2?YF519=QLqy}zG$>KUZd2|WJTWBRGnr_zU?I(*I4!be+MTOaT3 z?K#!g)3dm1*RG0yv>*BDqj=9d-yww%_=TVQ`M3Sr)&PWWU?hSerRkbbic2V_m}1Zv zVCVqT7|K=$Yf#b# zh}DPLFQ{z7RfyaKZ?(kFyuOx%QXGpp1CO6GF}IQNIsiUYp5Kn;_~XR*Wvgtr!&ZB*P&A#yp@a zL8L%JKr?5sTVFx{{sivqYsbKH69$(Yj4nA?%T_R3Dq%8Lf=(RafR48Q9$eoY!htRa zS2h>WET^Fyi4sNe*uVxJ8Fu{$@3{H^b~QDDIX0LIxT<#Ky!x9XwW)mSQiNR@>7pqx zxx9gmQl(yqa2+%R0)8#u(vqA7wir(~q7jXF&7!TXMYOlIucuPUscde&D1}NYrO2vQ zusA=Dav_gkAXJ|NuF=2i%#TZS2uZz$#DiBE6u;7+tBYsVz@OTDy#}s?l5lK+a4d?u z@3{vqHL!yr^kWqkg#pM#VVAN$zH5RFFb9Y0l{^MC$IZ2hw?Vq@VR4}+8h#t4R{Bbi8| zHIsqg?}tlJVVffqj9zquP(gXbA$MQl2nWSN5yQj7c;t~saDMnasui~*3I?pMu3_-} zc_des;Sc!1H68GL=uESUO1Xl3p@8iA29{Tru&^+X`GrNSuC1zkt{^STbR5U209esA zJ`xH?o^EYzeQL+f{^9Pfp4Ik_Hfw2VQSI#6N$I8}(y3;QO--n9(2wcyX~+Vp$<3*rat=1ZrACF#~#0IpOc^b_{V6@WN7&88U5JN<1NRIAH8L6Zti`i zY29GxMvETM{HAGg+jgL79Kk>s>82)RIy;a`r(py_kV2qRsiIP;ARG#zx2Ff~ZS4q! zLM~ObK34S7pGBo4lmu+ypi-$|b#)C>lan}o@&rzvI;AEiCsi(&b8N>dLD8}=;2(&^ z<4<;Vbv?6Z&z`Xz{k_@t_GT-S=~4gh|MTzEqmTR)fAS}P(nt^uKzJFPd|yBrhG7w- zG872~r4%Vr3=rU0ED#1~%ZMgBaqBJDBO3GpaG*37Y%%aHo^%W#La|atp^!t6Gx)iH z6sD{G`2tIBZQ%teaPqloTU3EC!Zm$4FMl7fLZG(2nQMNX8rRmY+`D(W zAt=*`M!Y_;u~9&CbCZ>6Ya1V*n8*T72N7{cDlD(8V0~>JsdN*F5m4(G)n>r!c13hi zgYxYr;cw=6F#;=>qR-a4ZVC#fWn#z9e%$enyYTI=e;t--VsU8^1LscT;Pp3xkq$|0 zlN4IZm97?<+u7QLY0tY^c9l-O*?b`o425y<&<(In6BDB&;GDp99qZW*eEQRW3Dd0N z;~)PxQpuDnB1eD_0>m#izrP9mwst`;CbmdXR`EU}(nXJ@itF&2^=E_dc~PSFmVdaRGzp z265{6Ni{Sygt>)zQLa?VfXr%K8xM!W$C{d&pX}eUV_^60y^C$_nR2ySlHKhQxt=Q^ z7KQdKjlbBNq~=#w*_u;8R69s6HU;=0&0)-eTtO1OBigGrTlu#A`=0ItH=?i0Ep@1*{cpeig zKrk4?y|>?iD>_>MrwjtYQ4UD)*H9tQt5lO)JvW8K?CKgOmX}}(v8^ku1Sd9i{dLz& z{LjV`(1=F7K7}bFl}N_yrskG0-SCY%w!M=$3n(I5UR%NZ{2coF`@pg_d*RG| z&3EuplT}+gbX)%{q=c{(Lg5H*xZy^?aWFc59-5{@Hw+YuMSS54pGUb=#3w%S3AA@~ z)h#f#3ddZyKX~D+d@-fgJ$rT|6b>O6^wm{avBfT8>)rLd#!X&vLO24&Vi9Abqxk6~ zkKoy7p25oU3T)d}uDFg;8|zsVi$$86)uCxRG_FJEoIs?66cECOZ3|^Pf`sEp;Rs;~ z+a{$y=<4o9OKS$9V9*sk#7kEPRtk_x zy55l0DmFG;yPVUfPpZ==PhfmTr8U$MHS8cTeoyv!@VG zrYs#oe(V#!hcAErf8n}AH#U+&0}x(b(d>(>NIY!oK5i0&gn;5wLDvj8wuzC2Dhe!y zYp%N%iAWfL0%N+{8I)efsYcaP0!lHgtZiUpV*@ciA>YYTuog22Ey z!{YK9j-NdPKp?F9(3Lh2)EzKOcwHvK+o5?x%=P4xN`a9ANb{3uY$j+% zU|fen2vSSp+2sy=_TeF%o^)+L-gE6P{NjzfkziFg!hLVOMpb=9yEnCMcetfoUSkXc z5Kt&NHbz%hu#n5S*AnD{QKg`3S~(Gk4&8d=^>d9Cpb?FD{UVu6IZf%*N-B{UpRbh5 zO2W?oD&>nCTyPN(s%`|iWY`SU2{^T=)F zaqjFHG^Lvn^oL@1!C$2f$CNuwh821dSp7?gj?>o1A+u5CnsGW zVy?k7ZG8P3-@y9%20rzhzlGhqcDw60qYK`9b1v}3gTbxk<+kTZUv7eNe=w%Rg7_}m4Yh=Dv{sfd9{f-ykyAVz1im|rfUx3?GfyyH$pBSAQ}1!C|h zqn>0nT`E~fZ^)b=q=MrpOiWE-d}16>0;yOCZOsITvR9o{UWN6o?N?qiG_DteYszQd zGFYQo)=9LU96Xo?Pyj-U_(h1*Er<($^)x)uq7B3waZ6o#)%$ zB~wb_$^%#8=9_N94}b750ELN(33PXlWAEMr9&<+489Xl)q;BsJZUu9;5Qry|xapQ# z03u9JPI|zP!7{7(;lmGOVQ~qc`sAl@+pV`EU>Fddx8HV4isu8x+X1PY#{o^(VE7ET z2u-ZsdEUI0w&jsp0of}+D5)SE2Nm1I#_BqThtK2iQ%~aPu_J1JZb4Kll?s8@44<*2 zX>3DC2_dB4c0>S>x+KL(5eF2LQb||SfD27iHj!55gbiKOmi+#}d?*^8ibW!msd#EW z)6urx-Q8VGr_e4^y5a9=YH1F2_H^;qObh(J0IZ6Icsh+_vxaCS27f3BuIXMgTR}MrR;7wc zsRWI2w6(ONwYdd>KoE>ESC!WD3xtwjF&x#qA_QBdP?~XpL*hnC-MG= zex{KK8i4RJPDC6k8jXrbz+YwxLO?1&LMj6rMGd)}g`QnK=*YCdU>u?b1U;zW0tx0l zSPE6!!Q9*uO63v~Q4@wP!J*)gXLmrtd+69jk_!Qn!F@Ir1hX(}hH-pk8Mf)*=9_QC z6?^+UbtncV0Wbzh9@xByxIj&uptcw&DNwa+965CgYinyD=AbJr(H>;bl<=gmIfQ2r z^j7W?k^-0n1i2M?sR+cp*-I$5i>nlvRDcqIlJNNgkQBjcsR`d7(eTwLH!!{G15*k& zb|mlz58aD9JDQ52%n&x>xd5?teriC23X2ctE7Why`@g+y~JZoT6!9Q8eo(euMlLO}E8M2|f4EdH2Uu{FBrrsAaZ`gcmY9)qP)35$!17(6$K zr=NZn=LXKIrPT$eTD3|FnD_gAr<1AV(O4up=Qx5@tJQF|S_#?02{?|U2`RZ!l9M9I z7?ZlDIl51`d`2Me^ZQl;e*aQ57F$as;`yegR52QfR=MtTdV0ELG#XQX^_O2nBp60( zYs*{aejPk>4(p4nC@YKRrl*Y~M~^lgJ$Celsqyg#%jME7et)>NrM1o9wWAN|mNW$d z0e}cT<*K+cu0hvyD9-C_GZGS(ZKF~t!!k`I<1u71t%${9&@=`|3fQ&-+cHr#El4Tg z^BV{SgCN}pVtiXySV0H}6|;=&dKMEC6F7PDBu<|^g~^F&k(nsz=C4n5P{ z+#Bi(VyT3g$$7x3qBZ4*&TJ^hU4UJH zaWPZ+&8i&~gOUmkSs;yLa@ofC%o>tSy}0G(8<9%H0LSqvI-Bq1iVs}c+fdf3j*MYF zo5M5D9D#5Y!ajzb9bv?R78oR)dbRbH`4qi*b`oCI)I(OP#t|g|!huu-kPpx_I9d$r zB7}3}1w3*591f2Zu~^m-)q}YHz;68Z2i}ca_U#6@svt&?(gm3C{3Ku9Q%RdnQeIaO z%Jp|DnKp)|<}tgnR_|^~uS3b9tWYR;{)+uq3^$(sMl|B}jjpaPJpAyF>}Vn~?F;zE zMN#Y|KnH;JwKc3Rt)RWD8xqnj48BdwCSIrm_(DIAZCyfa>)<)IgU-%QJox?x@t2?e zOH?WqEG{i#qeLjo~55cx7 z(71*`fMaTE3jg)L{1*%j594P){IfW4-~bH6fNk4#aJ%U<`26Q(tGD5EglD$gPZ#{j z>l!KP(L)^@)k+oFwRKEPO<~~d8Js$G8e?MUv-jKLMoj`OKS@P z{s1i7hFvUTBkNj?<#Smu#?ao@hW7S$=v)IaR%akc%^+V)FbX zf|@{El0gT9bO_Kcw1K!Vp?O_L$)FT>JqwKrq^2-9C$W@gxbCKYTz&Nw(8zIH{fxn3 z9HbOTdXz_czT{c^Ps91Kahw?#1elIw(80b=j-X}(jstM+jjYJG{8mJP0L4AKcDFKI zQ~f4La1!8#9~Qz`DG8ifFmZU)#*^nwENlb-rJ*Gi!TWDGfREjGC-%1lz$@$EOoLF; z!~WhimuhybbiM5Ie>5@gg8CM03v+~Z}JI{12U3g9;L3R4kRSy1M34yNLje)$I^2281rw6~ps&4=+=? z$95q1d;qH!Tf1i4ff8M0G{YUqAY6aL4LEV?Bp!Y25pcyYaP}E%nx=VX z@A85Xk4xrN+6GRytH4U-T|x#+I`9WVxZ&oT5s$}l_i^Y#YHqXycxjZ&$E zjg1Y=&&^?EbQI?X&tY_AOsy;}%TlRqNhxxSv&ld(a3q;ZJ>JpLajL7kW2v*Pvl0#k zRlZQ5ifOU3T~!rFkSv>E!Xl>YD&P;Qa3m}v;V`11pb92ZV$ZG~96NR#kfco0bwM%= z19#v3)|)e%otnbg^FuT|InKsM#skkhd${}D(7-#FmzM9h9s58y98I=&bm`qay%bBP zVfYL%#$Ejh?_x5K+F9S%d1Ee)lrSv|rfEVq45XUUh(sf>90&Q89Oh>iF+Mqt`MEh* z)hgQC+Yyf?AuqV6Jz!~D$mesIo1Mqt&>&8oIHiV$2F3Eyl3A`)vjj|rgMnkoRO)bB zTgR#1-ro6Ldv}z0(UcE=_XqONzxw6ILf-&{Z%$kgkH?&FIGh86Ba|X30aFoHN(9nF z$h5V9bH2HN5`$6%#yt6EDWHUe;yRXB*D*CWgVtC8nXqu@rWtePtG87yFRa^nDxf65 zn1X~K*=hj8GgWB14+jtILvLp%gjC?nqkGim7TJa1V0*$)O1N-=I4s-3Bac6UwbeCf zFtDS|huy6X7?Y6DK!RQ|aCsAzr9n(VfWZy|c3=7K^MrVpRH@S|{ z`3)?s+pvWX9kD15U9lJU-gE=*zG^2jkszpAswZZ?W$uGs_08CJe@M?`5rirTqZ@e) z%`KrS6r^&gujDpErOz;yuGq8ZXfhHO4aqK4bwdeob%k+E@JqO5P+YI3Lcro3w3(gA>Y#|Yf#cBHzSqH+koU&DzZc}}g zw#@!N_nwea)(om3Azf=L;lMI2l*%QnudQKvW(MP96Br*G!{pSYT3T6BgDrsk%nGac>cI@>#!Tbo;}`C>u(e1=+DSyoNSG_h2-I#tY3G-TudyzfJ5 zXlwvuljre?U;lLgz-K=FzwykmlMvQTFsmjw)A6AX{p?%zxu2SuhEFp{BhHTLnne4GmF0E(^pQs(g@jM%*wr0FbJ&D%YJa?!C&(^OLC7}FMfhHp3$jT~ue zPMyeP+QvINdp4R=P1VWi32BvzDwRmUXZT>*wkobHAS#41Obg%q&R1Yd2bN{xfBxy8 zLf3Wt`o}+xfBWzL9o~K4d*0Ib-4DL|L!?_8j&I}HtSSp}16GXyS z0W$>{aaa(WVqRO!;-6Fb^yrHMe$3|#tQOYLzoQ@f_w5Fwnt1@Ld+5DTE+XZ66p{o? zPI%<_5e!Z&LvkOw5;kt?20|!63h8~iQ`aJXt8Y9UWr5&8=qWsMx`e-dRv?$RK&3Jq zC`i%}^cy(X)r_6pUASV$4qUlwH#(DHB!UE8BM7?;0S4lJ7X*^+c7)G0rHPT|>E4Kr^+#lfQK9d5m6AY4XihjKj7r;A_cPs97f{ zA;B2o*4u8w*;A)+^7sjGM`7UP3AD9lkZx^*?Euno*H}fKjfA@R@xPSMSEMemxtGd8 zds_za*gJ8qX#j&~22jYa!f|YmkwjQuU%~gk|Mz(2nWwO8*B%@^co0`zbrt%0dl8Gr z;P(eyZ#f2c225?Ll}nEXDjWxv<-oB_R4Nse%4Otod91IlVQFO7-PXe;9NW&Kb20U2U|0(bFHndrF5#vURqj|V`JlL@2-CQ z@<%`RlFrM&_`^SZOF!%1|Nal~u6y5!C!Rb^gF|PGk-_1nb3^A2jf{`oU(T0q)41M| zN~QhXJ9hAnt}X;ZA%HO`a%YGyeB9MVYX3FiApzSoQ7)BWn>K_aurN1=Y&M6PnHj9D zuAy2fAs7y#r>6^h_w7YzXB$GnFaQo=OH?ZrEG;c!Xm|uCPMlB!XV1#Hh1qJMRLT?7 zWWXOhnoOskXm4*j)zQ{5pJ~gK6UinqH!}mD=Eu+f;?Fl0_XZ$*^G8?DUIA#{}DB5Y<{?7Z+9n=wxg%MB0(UOXwH5x)H5I~qSXi`CQ z%Fsm>O4!ht4#}9?vBm47%35cT)RjghgaXQnFgP=Z@%aVVj^ioddL0D?ME|DTM^K@%-%X%mjtQdx0 z>Kd0DYio$bqWG8p`rkAj?k(}TFMb~P-FGj(|GkIV;^MsT?7-Q!b3;S7&P>ldU{a}AizcaBVbk-N86BI#sgvg*EDOyE z!VUcV@1ger@Id9_V@HE!*6)5Ez zsCeH&F$Jf$v+GzQ9vjQFG|7qad;<(Nq7iRE{KC)wJQfz0s43M{4uwLC;LLLOeF~Lo z6}jv>q_AC08AdNj?fK18z`K+<&ZR)^^8v^g11@VhlavPz6>#nKH{k5x5PtN7AAp3y z%)}HXhexn;-vL-c)T#el1>0V%>T7L7FSZ_AEjJKgISz<1I8s8`ZvJj=Z$q*q&nvPPbghH`^iHS)lsi1KVT{oZ`I=IH5GXqRx;G96jy#lv{ zlmd6)CJx?_1| z<=wVzUmXbgn=&o!THlUcNM|xIe8z>U0JZu7XbZE5Hg6>5iWy2`P*Oq)8`WwNn#4zRqmj``UI^rnMoO$DK_;Uz49kvF3L zLds=s_Cm!JSl%eYAIhL-N1rz!F1P(8z35GVB8DfPd>S(|GtkJy-mVm`-4TY8Yp}hy z;@+8z(q*WLEJHZhS zG~JIKyLO|irvvlz^O&3%$L!23^7#TR%Xa4*iQLtl0Is{+MqPib0 zDG5akAotiAF$V;bzzW3KnyzcBx~|RYx-k(B1t(&W=wvLOm`|n>>*;i=?DGe$a3~}= z=Mo_Nli&NtjR$#K{NvyKN034yoleuh(2zDYH5nT?b9T?z#Q43reEv?QuqzY^B|6$V zwH-TlQZm^LZWyj>R9z)zQxl;6>|an5qWU(V<{aq=foipaO1TQlvcNb;&>ui75p!+K zEDM=TJ9h8djZ7wkfIk2sZRD~!OifSYvm>6?%8yiL27PCe`Kb}m)pKNJu zf3~ZueRk)Lodv@%#Q*%W|E`{X^4Z44ZUDl!E;I-RLn0gwmzg9*^|=iN&2NBd9Eu!B z}2@D)l6H6#$OW3hDjaa~fk`4gws+Pe9z#w0s##rP2=PNeW%L1WT z6kR<%^(rR-Na=XVhF=;ss7-9bG!64BtN7_7PoP{XAr%2`-ycD{HVaD()MGArh90}Nt2GbOIB2bEbnM=f^O+x(1`(;IOslEu zzC~TvmZTJ20C5F{^|f`FW)-1Oa0?sYA{Ga4As~1u5aCifdXm5cCflhJR3FeBAIGGof&3Arc8ML?V&(STt5hrjk`XWY`t5#EGYm ztL~m2^&7wO8;u9}BJm%;`R{T5?}pKmX06z){Sx+Bq8 zsJpL+_x1H78c%?e?y3pbXvu6V@KaEp%-_Xo^vdfLN-5z82bD?{QYrZT0i>H!XliMN zp=l@<@(4%5*wNdA?(R-RBVpK%KrXw1@v$)+KY0|Vj-QqjQ`1hakjp8lru>1xvrWy( zM|!$?PIq;8&8L#-su0py-&j|=rZ+Bj0}#G-BSVDGf8#qc6b|PBAsACIm=2vdbjCaj z0SQM5IFciuE1^&*L)RFDW23FD8Kz}pXmA8!u5e|Cfq-T~*vg|EN=UDz$SzXhttSn} zz!U=mL&XZCP-ST9ilV2h-Rs=des^=;Z)->?0qNQdINr*tURuJl=SPvNnjUC% z4T=?%(ltHX-O=&z2j2I3wXA{Y$j1Ac$bGHqK(sVgaIBfEiO zv4D6yUbhxdFJei(7+8EMYtoCQalT4`_<3oc7Xytiw5C6QR`LKf6qIyx9eKI(*4u8y z`N1LlYI4@!j+Hr7|Mwz`6Hsf2R5gmSqE%QPW`1cU@J0^yO!tvIJ@GLbmb+12xKG#nW={Ju4Rz+Vgo{S}Qf#|VaHKA%^Q{{4?t zZfQduKKV5M<-ht@jfeLl@r5sZ0rPBzZo2zc_Shqj`{!n7+Quiw4$jZdy{l5G+`=`j zEgnz$yZichM|Te*(Ks06+wLQEVHfZPtzxJ?*5g<d4H zKleolMY!S6L45VAe~n-Gg`dTa9sL+SKZDpt&iDcq{_cJq1xW+N~uKDQk+OoCp=u#uHCIi&8z8`=beo9qnrsvVoVEZ62GrP~=Mf=~)d2!IiM!3cc8 z2%1`2ARGs_U4gJ2l*?sQ&5~OTmNjCmOAqDDfMGCbh5<;9<)uZO8$O5Z>MA+5t!J~@ zWHMDy-QC^U{+<1+KHX|W@`49)%BHk*p_*%;nOq8RExj2e(C{l^|8k15oDZ$Bv#vDl70SO7N2LTYyPn^T&zVuaGd+-L_bo*U6 zae4@A)|c>!PyPmG7RJ!u%+L}kLJHgC?~`W?erd^Yrcj?nlTZNTSSw2i!O+#-hLGQY zg4?Cs6qNA*qEw#2I=KQP%uROEvRy#x_hEQ^22UM629aODod?3Wv)4vIZwuE_N8Xa!bD)+45K8u>797-`sW)I^wR8?6@d#4m8wL^vf*bMm~_trfkT3M-&xha z6?;|fBVNi^qx$z#o`M`H0h?(UT`A((!6~d4DiBhFnC4N1K$@arIvRWUmw({{gYu2N zIE`pTBep~$ky7zwyrk>;npDarMhZ$%sZv3{kcW~2iW?0v;8z23(S?~!L4iEK!AiA? zw$2W`=l$=)U;Nqsf@xK-u)Kt`XV2i^jW>gF4#)HS+XRL3HBppSN^F8e1(z^L1SJ#{ z99PYnb1=@KQ2^Wk0+A>fBQWVw2wgg=3o1Qck>{z%;PVGjwQY=87Ui-V!9t;U<;29q zyaKIMt!km8z02vb&4!t}-obaZwhnM&d7-}rjtCi3E4_q+?g^ACRqGjp@x28U%? zyjraq6XO%Hv9Zw|voo`|<#L7FgpgPI1L3CDOslWEX9uNP+u#p|whax{ipumd?-zG6 zgcA^sMG*{zkVwR#5kn!Dg=tzyHaDX+lYyb@SXo)Y(9j@`9yx-+;UQ;bWu;=7)n(4K zfk-6wSX-v$$^QQSv7P<9a(i~|w0(iJ8W}l(-}#6CuyNrVfbi`ThT#|CPdMgP>fjC3SG?6dl5D$c0NnrvGKm82OpC3Uw zs^gA>{fKBeXpkVTLpjp(RebI&$J?W5%uudaFl`6^00)#?L6!7hnr)`X2&NQp!X!I=pAk-k-kXwp((I?qDMtars0z7FW?ow8}Z( z0FmVlkdsxlQOcJfYzb%=c8?nI>IdU4FueBQL0o_GMm+lHkD;W4fq`>qY41dLZ_k!I zUF#6OMr%E7S@mn9BwK(i)%O#qkDy4eFs!5}BI5yIg)N0<>G~!cfgrkjdSRB!7_}U| zR4%mU3x(UJrzdlo#>)Z2f4-x)qY(0k?9YGx^NpM1BC)Z)fkYw>;n=jiw8YPkj`(J# zXPf3`X0DiDn7^}FD&E3~_eP?TSZgNZ@9ynGJlzcL^KV)QdskGw1m)UG-!lZm7eG3# z!5;{~&<$9X3d-d&q>@M^5(tJuSY2Di`O#6FIDSkG505zO>+5C9u@`lXpG_nZk9M?m z9PaM!813HCSM-O1VsU9nCDR&y{ue&dxabW)`1T6FKd1~}xJXQshF>8W=7bIcVdF<_o!BAxgB{sp8Qjngq7uA8r z7Enjhl?IW3Bn8DJ94Wv!AsLUm^L<+sN^3%=uE>o82?lTji|gz7+*iH=DFuG*Lm$MW zPd|O%R+ZcV1F50~-YsWm_Q@*1?ztNCmE_{@pIf%5PAUjVXd6f+DX{ z3<(Rs_{ut-7#PJ;vFLUM8H17nL{K_s#bhe}lVAVXFQ3g98#WV-XvF0diD*>nnr0EB ztj2gz2$5D2OgawA#S$FH@rr=8m5rXau=utr=DozIdkU}a3r3ey<5JSWVL^Odyxrnl9f`}lbgM6U`(ljiuE@S!8Wf0>47#3z{u|Fe` zjQJsKv02sgI(pBoC~l`@0>nK^A_0N`u4zc8($D#6S9Q$=akg;h$kx|Z@t^Y$)4JV2pVNOT;heLcJU~_QnUjsN>uE z4T&)b?!!v9f|4nr=>~k914>*d`TRPMaz>%RboEaa7DTE6cJt`9e zsdzXvaMO*~|9vnRSdT?wjR(CEjkt{B#PMU`oS8b;mNmv#rEoeyN)wKQLcRdAY9bu= zD2i_}AHp{>J8TEdjF{U&BcQLZ4-Y)>9{km3{tCr>39BnhICJ_WuDkIjXq8A0JTfHR)kj4>7CeYWv6V+;&EiKFiZPVVlzP|p> z$+3xwVfgKMBr);f4}Z9F?Dz?J=-{>L$kF3(>-*7CDV#xDMwddb3L1wm^7AG z*5WfW(|rpI3x~4V+-;6+UE%ZjGO2Vb*wNL^+B!PO9}0uG21dNDQoCJ9$TK8+*}Dc} z1g`6zaFT*;3lu9QluAW}`~l>$S*)*bV03&8bF*_IUno?Rk_&-A;AkqH{z+d~&ygKF zdS<)3`^u~9D~bM$01#KVAaBp7?H+z4&i)L+<)V>QoO*WXSlSV}h-zdY-Y`pu%3=U)}@HzP{YF4sd zO;pP5QmSoT$cvt=tzNs^*0H*Q@9T=CUH}Yk`Ddv-NKxKEIdn2&FMkgxtLjIKdhSN$rEGm_n4cI(DL9UeQlWrip$NNbV({D$@}&Yc zHnO5xsg{M3GyXu}Xj4nm5BvN3kM#BKn(yuDt<20%h|?!d(qI1NXBrDe0}#HgL#=P* zlxbQ)&cWhgLewYF6jLaTuVFn~f+Dx0rgP?k3FSTLNPPb2iELx|`Wlynze*QQ{T zwz#$9biuk`uzl=Hy-asG~zM@DMU&)bmjN^ zOs4UQA~;G4C4?Z$vS3-JYaPVc3s>E?y!LXz;9I%7_`D$5vw&dC?G%Qi5#0Ok`!F>% ziSdz9SY{O`j~_)ckwhYugyWgjOVCR;tbZlf;KF~S=f1YN(E!_$uWIv#@GJ_HboUsH zYiMihfK@SJnwFN!Zp2L6yn1nI$zr-*)(yY8d&kbjJMVml{OCXUHT92v?;pLbp3ndN z7k`0RJdR?iM7#IxXNya7`s~bXbarmOYhhvOs{BUwhN^8{OT@aP(O9%KlhHamx>>v_ z4c+f^y9O7Y`b}f33tHCf1&RdEGqYM#yscq=DPdPkl=69$3I${x8_R%lq>#dqWkodO z4+ajmwq|~`tH1yFzJ2@WyZibojD++XerTH3s3bK2;oCc&dGdSc-g_roUS4X~Ikc9h zAh@a^5n&)z#p>#Yo0vI6G!k-Uos^eg2w;puBZ-&~FsSOyE4&m^30=S|ClI)%fiVWn zsNEjYGo2;xGZ`3Zm|0rH7ykOo7#SUd9tfgRvGBkD;xADsu7hz7uIspJZwGca3+So> zVgw;w>Wk}A=K;bRVUf1*+iO+Ei+Jq4xvS8w39uw!qiiErbX8UpkqCT-4xwxhAiut{ zGBpT(!QW_;%nRj`OH2t2&n@He<7cp5s<@P2x2*{Vk~-6u4qSEMo4@#xhXxx@cOx2c zdBy|pc@O^Lv!7PJfX~zzuhb+a6&zHn6_}L@q>zBN)j##c<&oE9)qEX&3SaGGxs>G`*81l@51Ni=3!elR#%pA_RMMAe9JB1x(=ttsM&TLZ#<}ezQMFjM4~)O zie~8O=;}t*GBG+bq-8hO6Vo5L7{l>>X{+53( zzx;#0#{9rM7!gS&Nh!(Cj|}_gW@nq`=H~mCS5|H)7E0F(A$R$FMoT=N2)4F&Xszw- zh{a;i3?GOwc({9;kj%>|64b8VR%+>X%>+;9K{%*XODN=vs8lPkZA(c7L5xZo*Qdha z;L~lH%n$l^?LNMH=bq*6?oKnEj>!4BWqjh-Khao}8-VcgjdUu7)wQ+P{BwTk*Zz=x z{L`Omvoqt}LBD47rZwnd4NWnLM2N7lJP+G4Vdxs#J2N0db7_{0prr76dlJg7fM9re z-QCRB28o&4LJNG(6E8jB<=Ipyf!}N()PXbMn;01f-O(!0_nf&*Dcvegcwd z02G9Du(Y-c;tE6#IztuQzB7V!Fbh?!xT>V2Kum}5C?)Q?;(iaN04W$0aaUZ05r|}6 z277x#rOOx-UhH@Q;dNU9oB1)DV^}WR?#)%Xirb~I@fe@;?zlX%~e;wW$%H{|J4_e*b_!^K2EW4jLj`A z8gp}Vk=dE)j)nP!D>v41H&m?3HH=tKC=`mO(rI6NM<;J;%^(yBgK_Te;iNm7Lf(i6 z6khpe!lRxN(H2gTtj`)TDd0F&6bgBi$^{7ND5W5jM3ESq34}sVb#!!mfA_APC-&~& zyRxIN->eiYxxSvqgYSD^V*zLY!Z$8H{pnA`A>h|P_A3B@|NZBGfmkwza;1t$JOSz0 z_~1i7gFpN8KSMB*g46?ugb0yj5{vVT_@#&5k5Byie?%;?ihuvdfAoruB^uT-Iyuok zF*bB-Z1>pcgBJ9WF^c2i$713w}9UW~5g#sv+On@;cA~5o(qf%Z_EtE=K zK<#)j9ktq;;rDZwuBhDE2J&VY;fcx!A+VY);rrkJAvQMhV16CaQzK=X1|@9}6xz~3 z^t6V-)r#v^!!<|%QV8$EwkoA5s2UZv21=v?W6#}#yi#h+>kba<6?U~*abp@5)`}<; zO%V4Z9*aWf?qdGN09{q5iBipCLP|(RkW#{t3V927`rIgpnP>d zxyxZ4yZYq~cw@yyZn3`r(3?Dza zZ`a=Z)d#K;rv}d8s{L2KrJlpT`Px^}luBb`HH*?Yn{=Pg7@ZgkOifIs=jP{jEH5t| z%H{Ld3rFqdn${eRM8Yktt-Pb7lj5l+_ya*OjYDqf(zC7o+E?LzsCBSw6jFDez&tjS zgyRTgH*#27T0)^%f)r8^v9hkSnP4dRL|1$J_x9}Eb)tXIt`)yOU?r0gHMy~X_dIZa zVCVE)`;^mfES7GY;=2CIct z{LF(7Qk)!>N+wbCN6^#6aMj*x@x9Y0@#U|62mk61zKT8j*YHF|{M?py`4UH`y*t`3ShpyiDY$BDk z``cR^t7{_~aT$kU_#`84DTNA@5=s&Yso)3))oK;OmQa3=D0WGu>epHE@^V?*w}7yh zlM`xo4vM+I&paPsS8t8rI1(N0op|uQ@55(5`&n#cvshVM#fg*0aOa*SsJlw%Eng1b?l zHqk@dL89A?fRM=N^O&BV#?sOvY}*nFRN;(fBa!eEon1ZO*|l@$$({ZEYi;fA*5u@* z%4Q3A&w~vx*Z_pDQ#^C>ILtrEPaQw`P;*4ZuHBP@K{-I!kPbsn zYXpO57qOaMM@wfn+FDxB-`k6!kqK~*Ag-i@rb$EtIv{1;H;NIf&wag6lGyYZRDdH8 z^J_444oe7tlcyB6X*XfoHXeWcFcue=!3`b46R=TGE?q$>Bmx}!I$av1aNI4 zG4A;rYJdWSv|ZrD85H^8z<^W|fP{uBI4m&Q=AZK>iLL6ciljTl%?+&OBo&^UK z@QTN<0RYaRpkN7s>u;-pUWcC+J^D*5&ngfr?jb=+0fMVbo2b&?5@?7wS(ni@sMTP{fr?U z3WbABEv;I6XBSJiv>+G^LFby+rK{aNv{~Ta`c%@T%Qo~PFRfCWAaHX55l}*)m@8mv zauRd1vnUlx(h;&sjLn9_kw?3`yT846@7`1U_U&0u=}k6qratu>pKL4w4M6xh0l{aV zg<}}->x@(|Ks~n-i4s_DW|f|rTHuwk$x4L+w;V^SR93Wdxr&NeVHMM4mQ#hWZDInX zINa`=(iGV8o!5K-kC7p4OqHfRY@{Q-o9m z$SponbZMb8kE5x;B_ZV}XdEcpj$28R?jWLqg5*FpU%>I>18^(>_!XolHbWpV1u&+t zt1XVJ+W@XAu%QDI;6#v80|SGuY$!<};?Oh!rMX~et-`QcRS-fT6;d$R!U32oEGd=r zX8B&n^YuJ==>q>k1pqZed-ANa6qpBMxUQqQwH-T63$|m?#@edivBi${^|gBkht8Ry zNWhLqA|qYhz2?(TKdo-O@y!Pc|MkE6SGexF>ky7c;n)@%9vS3wGjpNo*_q_@%yjSa z%JNnDT<#jvwD&=&4xgb%qp^t5+S<-C9i50LQ_%eZ5N8nX+4Q=CvR<;`bt@_5julB& ze?;p~uflVI<3+FE<{*jNUSJ-uS5QKtoX=xwVgl2XQz#bmQVLOJjL(L{p+`HryZ>(g z6$egSapk^jG#arta#{Iv_k6gq@HPP9>%CqRTg;a=b2+Ekw&|-BE55R2`l}VwZ&s{e zxoY~Vm8xE~gi$rCTBTBw^=q=bNS!&O?7qzFndkPXIcu5p_fH#yfU!Jtu& z$75!5Dq$yLk?QQ+Y&DTdInh`|`WZ(aP?Iy$>iDxy)1%KEQ?W=KiyL|0@BZGuxaP5^ z4*%Bb;>d$H?uiE8byWZ%UWP6GuBQtRz#ojFqs@o&<5RfRcF-IO;MUu2LI1b@4(HAe zfpi}jlgOkZ=twG%U4jO;b5pAq(Kf|Vm!`RO31IaS5K{_FNkoGTVckTjRDe{|-SR{@NyF|3!|bKjnfARZ0_o{6NvqHqlW*h|ul$9cf zlrn@AWLq|*BU~kD+EO&!zH)4R{Q`qq+BCnu4A+FM$F+6#?zK2pMe4a{?h?wQ3b^1L z5)m?)4Bqpe2e7=bfc2GiF%VOEwMgS5!SC*IGbNb}zax@gK1cSlpXe?$QJ$6(byzbze z>Y1}`n{-{r(%dYco}Tv4OwT0dX6L%*78kDE*jT@|TB%+kr0V3H#{+?&Kb=nT_Kr?! zZq6VW3W4bcz<+{B2V4kn<$p9U+K*B zG$tn}RUx;b9N|Z+6b_U_qeZEbR%eBvqD+t<((XaK_3G5+g+ z`QNZD#;i&{XqwePK40*c%xciK>|mv82P{(>jxGFE$I?vUXqFI$Ed+CvBnhbtN9vC4 zFlk%Nu^r|Jn_HGN9NY0h2&Sas4kQyol9ZC5;Ak2z1pEV|Lre6AAbDtN8b6&v17lwzLC56ivCpK!MnQA8JPju zWk3r-I2`L6RjkfWU}K%*+>?h9Z|=sPOfv$pIBvc5R!odd!L$V!DeUa+Ku8}0swOyG z1y9au_6@A=?}x43K-v_Xsi}i;QV_y{&k%?NfP5|sp%e%T93BN#0V5;hSXo_%0t2h5 zlzP=-0-?V>gll$$!R#q8*UBDLr!sIbO~;BUF)%rglS6ZuT$A8FA9{D}#P04e`hq#M z1UC@YtKdpO3I*l?*NY|W=NqKISdxD}Ah`X#UMeAKa{;Eh#e`DK1kO-9fparO zm_i3<0z29>2pYb6LYGQGBUY#X;x!V|sf~Ur+MED!JIex!0j9$-wYGupJ$npC$EHxU zJ>aigJwi>_Hrl&7zy0p}@B7xT{oDs~Z#J=?Ml|B>9KmQz5cR; z93kqgfz1u0y4WgGTcj@j`ehzUU$?-7^wlw34YiK86?G8CzeaO%a(bmLXCKR4Zl7P0wI* zd|c(S>qB!=XpJyL!HL)zw!Y+q-AqMm!#IX66=TEFQ;kPqTF}?(-Q^0Ja9b3}xyDms|)%erdoGHn%LrEZZj2wwP^Jd9_l}s=f-h%?fuc zi&srkV_f)^rUf8{Ur6P*gbbOM9h9~ea%?MZnw7RnslYdu71>fUM=8q}^cPae_-ZPd znolHBW34Ue1;a4Z=uy954~*7W`@zKFUEU3p#8N z91s|UGGOazlz9p^e5eW?WmESOKS9%~fKi3y-S8(Z6e{JqW|eXr5D5sJbQKo>x)b1j7Z#Zpb6LQ zi6N7&fMgb2R6xoBIh*Y(QG;1t`IAwdd;>|cPMfG1+}Akuf^Bl>6%lH^Qd7f+GN-1h~3T2uN-D9`bU!4qFaVO1D=fJTK&6IF6p3yHTiC zF*7y6ta2q}Rm}r4Q&ZMqjg@qt@5k3%bM-3c+_@a9566xkLB*`%nnSlBpI^c8XO8k* zE~iaSj)$jbrZcm1vj^5z)~+j;O9vds>2;(?F~);|U|4TzYDQ;A2eq`eA`l9JYlbI2 zL>KN?>T063ksx}RMB%L+>}~Hs)EWkS@7&F z6bL%k!OR@4+SiFIcMz0aaMejQ4PYS@sv$gnY7&p0If~)MDnhY1?zrs++`2D;zGw+S zxdh!_g~k-11cclI-fClv-onH@31k8Q#_H2=?x4O`&QhD&XV_BPRvxFK=FxMpjY?f2 z`&A4f1QZc$#V|gb!&v=d{#hP&@IjEY5MvmWp>#d*r{ZIYMAOO(-gNZ`;le1(hTszt11gWAOx9S~HYsYezbn zgy9Q-h}DbJ+aEmI_E5h3OH7+4_S-AY9$lQg>t#6>mKPT=Ha3d&)fL!|V-aIZ!GQm8 zPfz!^_U*ai_@15p8|k2K2YgI1VtC&}4>lIt1|WQczL2J4bojKo@s``f`pPm{Qi*E0 zijm<#Ffd$y+fAyF&9h1=OYwL@k>5ZzD^W2k2t>k^tvKL*ftd0UQ;aEHC_mRpV~p9F zkCiJnDcb=j;*OF;N@+?c-KtcLf?4*NjtFW*nT55Dcb^*``pERmY&f4UUkyi*#>nuI zpmZ!s{oOHy^>q|j2o)B@=TDgU)+0qkR?&`jB!A5NJWtAuFs%D1t>@yJ^yVy`P6aj-@hL>-*`Q4yx}@@cC>?t zfpgsjYD@rRz_x&^n;iV|EgU=4Dr~!qav+3tmd3FuKfd{N38x3khzI<*{i+n+do9DB z=6M)20pSpY6wo*T$si>M^OWsKI)HL#>s)U|4U#0lBuKe_iIQAiJ5$^PgjBC*31zzp zMlpb-Yjm$%dybl+a*Z-u=ihr)5pKvWQsJHgsP}KOKKr1yRXuAX-nE}oAg+P4AgUsQ zr>6t*wz^_>``3ix{lhFp?w% zM@R?=K)N