Compare commits

...

682 Commits

Author SHA1 Message Date
耗子 da44f1a8fe feat: 发布v2.3.19 2024-11-12 02:50:24 +08:00
耗子 0317aa1401 feat: redis添加清空 2024-11-12 02:46:54 +08:00
renovate[bot] 90fc48f478 chore(deps): Update dependency vite to v5.4.11 (#354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 14:21:13 +00:00
renovate[bot] a8178cbcd6 chore(deps): Update all dependencies (#353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 15:12:12 +08:00
耗子 91ecd04c27 feat: 优化登录中间件使用白名单 2024-11-11 14:28:00 +08:00
耗子 6743ac9621 fix: 防止首页报错 2024-11-11 14:19:36 +08:00
耗子 5606d980be fix: cli下修改端口后自动放行 2024-11-11 14:13:18 +08:00
renovate[bot] 34300431a4 chore(deps): Lock file maintenance (#352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 08:25:44 +08:00
耗子 6f0f4c69ef feat: 初步完成redis操作方法 2024-11-11 02:22:23 +08:00
耗子 5fba45e3a4 feat: 初步完成redis操作方法 2024-11-11 02:22:17 +08:00
耗子 bb140ade7b feat: 优化app路由加载 2024-11-11 01:07:37 +08:00
耗子 1316c50a57 feat: php支持设置为cli默认版本 2024-11-11 01:04:09 +08:00
耗子 f14b4f5c54 Merge remote-tracking branch 'origin/main' 2024-11-11 00:47:48 +08:00
耗子 b08eb6bf1f feat: 优化目录输入自动清理多余的斜杠和空格 2024-11-11 00:47:09 +08:00
renovate[bot] 7f5869f805 chore(deps): Update dependency @vue/tsconfig to ^0.6.0 (#351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 00:38:26 +08:00
耗子 ecadaa9bbc fix: lint 2024-11-10 18:37:44 +08:00
耗子 60b9350625 feat: 优化删除和写入支持自动去除ia属性 2024-11-10 18:25:01 +08:00
耗子 d64f099f8e feat: 发布v2.3.18 2024-11-10 08:37:38 +08:00
耗子 294a78cd95 feat: 优化删除检查 2024-11-10 03:26:08 +08:00
renovate[bot] 8294a1a74e chore(deps): Update module github.com/urfave/cli/v3 to v3.0.0-alpha9.3 (#349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 02:28:09 +08:00
renovate[bot] c403efd762 chore(deps): Update dependency @iconify/json to v2.2.270 (#348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 18:02:04 +08:00
耗子 cf8dfca4c0 Merge remote-tracking branch 'origin/main' 2024-11-09 18:00:15 +08:00
耗子 e552caa38f feat: 修改ip获取地址 2024-11-09 18:00:09 +08:00
renovate[bot] ddff13990a chore(deps): Update dependency marked to v15 (#347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 06:58:36 +00:00
renovate[bot] 27f79ea7a3 chore(deps): Update dependency pinia-plugin-persistedstate to v4.1.3 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 01:32:12 +00:00
耗子 04d570197a feat: 添加redis驱动 2024-11-09 02:41:47 +08:00
renovate[bot] 8e74428023 chore(deps): Update all dependencies (#345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 01:10:12 +08:00
renovate[bot] f4c09324b7 chore(deps): Update dependency md-editor-v3 to v4.21.3 (#344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 11:08:41 +00:00
renovate[bot] ee6a12fac2 chore(deps): Update module golang.org/x/crypto to v0.29.0 (#343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 03:10:34 +00:00
renovate[bot] ba429068ad chore(deps): Update dependency marked to v14.1.4 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 21:48:01 +00:00
renovate[bot] 711ccb4504 chore(deps): Update dependency @iconify/json to v2.2.269 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 19:09:13 +08:00
耗子 7d6e078e20 chore: 添加新合作 2024-11-07 16:58:49 +08:00
耗子 d3a7c9e29c feat: 数据库密码可以为空(本地认证) 2024-11-07 02:37:29 +08:00
耗子 10d22bfbc0 feat: 初始化数据库服务 2024-11-07 02:27:17 +08:00
耗子 724d05d980 feat: 优化数据库模型 2024-11-07 02:13:24 +08:00
耗子 690e7f0391 feat: 重命名安全为数据库 2024-11-07 01:12:38 +08:00
耗子 a28fc592d1 feat: 优化数据库模型 2024-11-07 00:52:12 +08:00
耗子 ff7e76ac2b feat: 初始化数据库条目模型 2024-11-07 00:49:11 +08:00
耗子 f2eb3a180c feat: 对部分密码进行加密 2024-11-07 00:25:09 +08:00
renovate[bot] 020a4ad870 chore(deps): Update all dependencies (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 22:16:57 +08:00
renovate[bot] b5e7cfc069 chore(deps): Update all dependencies (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 21:31:06 +08:00
renovate[bot] 52f8a6f045 chore(deps): Update dependency @types/node to v22.9.0 (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 05:13:50 +00:00
耗子 3831d47a23 feat(文件): 路径切换后清空已选择 2024-11-05 02:57:30 +08:00
耗子 3a579f2346 feat: 运行等待任务前标记先前运行中的任务为失败 2024-11-04 17:59:13 +08:00
耗子 e85e8dffb3 fix: lint 2024-11-04 17:48:46 +08:00
耗子 e18d40b0ab feat: 防止任务重复提交 2024-11-04 17:47:39 +08:00
renovate[bot] c308b8e42c chore(deps): Update all dependencies (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:40:20 +08:00
renovate[bot] cafded526f chore(deps): Lock file maintenance (#335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 04:24:39 +00:00
renovate[bot] 99aa6c99d8 chore(deps): Update dependency pinia to v2.2.6 (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 01:11:37 +00:00
耗子 e0daee4766 feat: 移动部分方法到utils 2024-11-04 03:04:34 +08:00
耗子 bf8c832b32 feat: 去掉重复的方法 2024-11-04 02:33:15 +08:00
耗子 2d35cbc176 fix: lint 2024-11-03 16:46:05 +08:00
renovate[bot] 6b51f58856 chore(deps): Update dependency @types/node to v22.8.7 (#330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 07:25:33 +00:00
renovate[bot] f58c680ced chore(deps): Update dependency @iconify/json to v2.2.267 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 19:49:40 +00:00
耗子 9c6d0b5837 feat: php拓展安装添加确认弹窗 2024-11-02 21:28:29 +08:00
耗子 d069cbec75 feat: 负载条不允许超过100% 2024-11-02 21:22:35 +08:00
耗子 9b252aa8f6 docs: 更新文档 2024-11-02 20:02:43 +08:00
耗子 a52d06e3cb feat: 添加memcached php拓展 2024-11-02 19:26:38 +08:00
耗子 c25c78bfef feat: 发布v2.3.17 2024-11-02 18:32:54 +08:00
耗子 567f3ab693 fix: lint 2024-11-02 18:18:29 +08:00
耗子 aa162e12db feat: 支持memcached 2024-11-02 17:48:52 +08:00
renovate[bot] 7169b90c1d chore(deps): Update dependency sass to v1.80.6 (#328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 03:07:42 +00:00
耗子 5e1001f10f docs: update README 2024-11-02 03:36:33 +08:00
耗子 8e26589c80 feat: 提升最低TLS到1.3 2024-11-02 03:34:21 +08:00
耗子 27ec65650d feat: 替换go默认的logger 2024-11-02 01:12:17 +08:00
耗子 e2345a8285 workflow: 合并codecov与test 2024-11-02 00:43:01 +08:00
耗子 4ebe20d7f8 workflow: issue lock修改为手动触发 2024-11-02 00:37:02 +08:00
renovate[bot] 8f5872b284 chore(deps): Update dependency alova to v3.2.0 (#327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 00:34:24 +08:00
耗子 75c6522453 fix: lint 2024-11-01 23:07:51 +08:00
耗子 ff34cc72cc fix: lint 2024-11-01 21:54:06 +08:00
耗子 e2fb1080da Merge remote-tracking branch 'origin/main' 2024-11-01 21:42:27 +08:00
耗子 b96c6ad899 feat: php支持74和80 2024-11-01 21:42:20 +08:00
renovate[bot] c1a4fb0904 chore(deps): Update all dependencies (#326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 09:46:35 +08:00
renovate[bot] 5c50da27c5 chore(deps): Update dependency @vue-js-cron/naive-ui to v2.0.7 (#325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 18:48:45 +00:00
耗子 336a9bc675 fix: 文件管理bug 2024-10-31 21:49:58 +08:00
耗子 ea3bf37687 fix: 7z list 2024-10-31 18:38:03 +08:00
耗子 79e09f9cb7 chore: debug 2024-10-31 18:32:08 +08:00
耗子 d73c988073 chore: debug 2024-10-31 18:32:01 +08:00
耗子 9641ae7f34 chore: debug 2024-10-31 18:31:56 +08:00
耗子 fd2c89a480 chore: debug 2024-10-31 18:27:01 +08:00
耗子 526f192200 feat(文件): 解压时只设置压缩包内文件的权限 2024-10-31 18:20:54 +08:00
renovate[bot] 6423953aeb chore(deps): Update dependency @types/node to v22.8.5 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 17:07:19 +08:00
耗子 1091ec8a23 Merge remote-tracking branch 'origin/main' 2024-10-31 12:35:38 +08:00
耗子 2cbc641b2d feat(文件): 优化解压,保存时的权限设置 2024-10-31 12:35:31 +08:00
renovate[bot] f4d23211e3 chore(deps): Update dependency @vueuse/core to v11.2.0 (#323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 01:16:52 +00:00
耗子 1b6086bb0b docs: 更新bug模板 2024-10-31 04:11:13 +08:00
耗子 f399e9fcec fix: 应用更新按钮错位 2024-10-31 04:11:03 +08:00
renovate[bot] 17cf1ada0e chore(deps): Update all dependencies (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 03:43:36 +08:00
耗子 85bd71aefe feat: 发布v2.3.16 2024-10-31 03:34:02 +08:00
耗子 f01afacf34 feat: 优化docker兼容 2024-10-31 03:28:41 +08:00
耗子 52e8830605 feat: docker 管理器 2024-10-31 01:23:46 +08:00
耗子 e90c6f4f7b fix: 计划任务修改后未刷新 2024-10-30 20:38:35 +08:00
renovate[bot] eb94ef89b3 chore(deps): Update dependency sass to v1.80.5 (#318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 01:42:39 +00:00
renovate[bot] adc6dccaae chore(deps): Update all dependencies (#317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 18:30:35 +00:00
renovate[bot] 855b2e87f9 chore(deps): Update dependency @types/node to v22 (#316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 11:26:03 +08:00
renovate[bot] 3c601b808f chore(deps): Update dependency @types/node to v20.17.2 (#315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 02:17:04 +00:00
renovate[bot] 8652501e95 chore(deps): Lock file maintenance (#313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 00:22:35 +08:00
renovate[bot] 43b67f67be chore(deps): Update dependency md-editor-v3 to v4.21.2 (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 00:20:13 +08:00
renovate[bot] 4fff0b84d8 chore(deps): migrate config renovate.json (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 00:03:12 +08:00
renovate[bot] fd73982f86 chore(deps): Update dependency cronstrue to v2.51.0 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 23:52:24 +08:00
renovate[bot] 10191ae5d6 chore(deps): Update dependency eslint-plugin-vue to v9.30.0 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 23:52:14 +08:00
耗子 b162f71d47 chore: update renovate config 2024-10-28 23:56:07 +08:00
耗子 a9f34a5783 feat: 实验性支持alova 2024-10-28 23:44:48 +08:00
耗子 1e4285b516 fix: mysql数据库为空时无法备份 2024-10-28 21:10:49 +08:00
耗子 44c370a119 feat(#305): 使用slog记录所有日志 2024-10-28 20:18:25 +08:00
renovate[bot] a1c0b4a399 chore(deps): Update dependency @iconify/json to v2.2.265 (#308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 15:18:03 +08:00
renovate[bot] 805f6c0002 chore(deps): Lock file maintenance (#307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 05:10:06 +00:00
renovate[bot] 5066446053 chore(deps): Lock file maintenance (#306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 03:09:55 +00:00
耗子 0c31641ebd fix: lint 2024-10-28 03:36:35 +08:00
耗子 343d451752 feat: 所有shell操作全部使用shell包 2024-10-28 03:35:20 +08:00
耗子 12ecf0707e feat: 规范压缩解压 2024-10-28 03:21:22 +08:00
耗子 899ce1fa18 feat: 优化压缩解压错误输出 2024-10-28 02:56:53 +08:00
耗子 9ef56f6ede feat: 适配面板备份 2024-10-28 02:43:21 +08:00
耗子 4b3ebc082c feat: 合并gz和tgz 2024-10-28 02:05:36 +08:00
耗子 fb3c8ccc85 fix: test 2024-10-28 02:01:34 +08:00
耗子 f620a8737e fix: test 2024-10-28 01:55:30 +08:00
耗子 d0d5b237ef fix: test 2024-10-28 01:52:37 +08:00
耗子 11f5adc5a4 fix: test 2024-10-28 01:44:31 +08:00
耗子 6927a0256c fix: test 2024-10-28 01:42:21 +08:00
耗子 dbc78343dc fix: test 2024-10-28 01:39:58 +08:00
耗子 d8347310ea fix: test 2024-10-28 01:36:27 +08:00
耗子 8d6b3fe95e fix: test 2024-10-28 01:28:28 +08:00
耗子 036e01795e fix: test 2024-10-28 01:25:22 +08:00
耗子 2d7faaa653 fix: test 2024-10-28 01:22:56 +08:00
耗子 5921a88c75 fix: test 2024-10-28 01:20:41 +08:00
耗子 d6f4b8736b fix: 尝试修复压缩 2024-10-28 01:20:03 +08:00
耗子 227f10751c feat: 避免压缩包多一层目录 2024-10-28 01:02:04 +08:00
耗子 54cc556d17 feat: 重构压缩解压 2024-10-28 00:10:24 +08:00
耗子 0d8a26ccb6 fix: lint 2024-10-27 23:07:06 +08:00
耗子 eb18b1080a refactor: 使用api获取容器信息 2024-10-27 22:52:42 +08:00
renovate[bot] ffe7b95b9c chore(deps): Update module github.com/bddjr/hlfhr to v1.1.3 (#303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-27 19:24:58 +08:00
耗子 ca097cac1e feat: 移除docker包 2024-10-27 19:23:22 +08:00
耗子 f3a578380a feat: 容器网络使用cli实现 2024-10-27 18:51:53 +08:00
耗子 beb271b3b9 feat: 容器镜像使用cli实现 2024-10-27 18:12:21 +08:00
耗子 310316c8e4 feat: 优化容器结构体 2024-10-27 17:45:36 +08:00
耗子 7521b0f788 feat: 使用cli实现容器管理 2024-10-27 17:33:34 +08:00
耗子 2783e2b28e feat: 删除容器一批无用的接口 2024-10-27 16:35:34 +08:00
耗子 cf26063287 feat: 优化去掉go-yaml依赖 2024-10-27 16:23:52 +08:00
耗子 2a61f8939f fix: PHP关闭状态下获取负载报错 2024-10-27 04:37:38 +08:00
耗子 513a298541 feat: 发布v2.3.15 2024-10-27 04:19:48 +08:00
renovate[bot] 1cbc53b311 chore(deps): Update dependency vue-tsc to v2.1.8 (#302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-27 04:18:37 +08:00
耗子 aed35990bc feat: 支持签发自签名证书 2024-10-27 04:17:58 +08:00
耗子 9179543b7f feat: 支持重签面板HTTPS证书 2024-10-27 04:09:33 +08:00
耗子 f40ad8bdf9 feat: 手动签发证书 2024-10-27 03:17:21 +08:00
耗子 56ae7ebfa0 feat: 支持上传本地证书 2024-10-27 02:35:04 +08:00
耗子 cf5f7bd866 feat: 证书支持同时部署多个网站 2024-10-27 01:46:58 +08:00
耗子 bd6496c1cb feat: 网站支持选择证书填入 2024-10-27 01:22:47 +08:00
耗子 3c75c1c49d feat: 文件名支持缩略显示 2024-10-27 01:08:43 +08:00
耗子 2b0f8a57bb fix: lint 2024-10-27 01:00:13 +08:00
耗子 658b6a79eb feat: 文件操作支持覆盖 2024-10-27 01:00:02 +08:00
耗子 42dd00c5ea feat: 标记的文件可以取消 2024-10-27 00:17:32 +08:00
耗子 8fa5cec568 Merge remote-tracking branch 'origin/main' 2024-10-27 00:02:35 +08:00
耗子 2af4b9569d feat: 不允许把目录复制到原目录中 2024-10-27 00:02:29 +08:00
renovate[bot] 408b9ce19b chore(deps): Update dependency @iconify/json to v2.2.264 (#300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 23:30:44 +08:00
耗子 b2845fba15 feat: http自动跳转https 2024-10-26 21:26:14 +08:00
耗子 ba819a5a9c fix: 修复监控单位错误 2024-10-26 20:07:33 +08:00
耗子 bf1359e3d6 fix: 禁用resty的日志输出 2024-10-26 16:33:53 +08:00
耗子 2970aaaa47 feat: 一键签发证书支持自动同步域名 2024-10-26 13:13:29 +08:00
耗子 a65bcf582a feat: PHP网站默认开启防跨站 2024-10-26 05:06:10 +08:00
耗子 145387b3df fix: Alt-Svc头删除不正确 2024-10-26 04:59:28 +08:00
耗子 f7c586e05b feat: 发布v2.3.14 2024-10-26 04:46:16 +08:00
耗子 96c78b6be9 fix: lint 2024-10-26 04:44:17 +08:00
耗子 46ae37a4a1 feat: 优化自动签发证书 2024-10-26 04:39:22 +08:00
耗子 d56b41bfef feat: 为quic自动添加alt-svc 2024-10-26 03:32:33 +08:00
耗子 68beffff22 Merge remote-tracking branch 'origin/main' 2024-10-26 02:53:21 +08:00
耗子 a774253ec3 feat: 优化证书列表账号显示 2024-10-26 02:53:15 +08:00
耗子 9dfc1d3058 Revert "feat: 尝试改善日志性能"
This reverts commit 5ac43df821.
2024-10-26 02:33:18 +08:00
耗子 049f708cae feat: 优化实时日志为500行 2024-10-26 02:32:57 +08:00
renovate[bot] b749a8599f chore(deps): Update dependency @types/node to v20.17.1 (#299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 02:30:07 +08:00
耗子 95cf05879d feat: 支持一键签发证书 2024-10-26 02:28:52 +08:00
耗子 9ac5ae4968 feat: nginx解析器支持设置alt-svc 2024-10-26 01:37:51 +08:00
耗子 2491f34b3f fix: lint 2024-10-26 01:23:25 +08:00
耗子 302d0f1bec fix: 防火墙关闭状态下ping状态报错 2024-10-26 01:23:12 +08:00
耗子 5ac43df821 feat: 尝试改善日志性能 2024-10-25 22:31:38 +08:00
耗子 8a1e7ef88b Merge remote-tracking branch 'origin/main' 2024-10-25 21:29:09 +08:00
耗子 b0a4df7ca6 fix: 创建证书账号/dns后刷新异步数据 2024-10-25 21:29:03 +08:00
renovate[bot] 70f41b87ea chore(deps): Update dependency pinia-plugin-persistedstate to v4.1.2 (#297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 18:16:20 +08:00
耗子 ea96634318 fix: lint 2024-10-25 17:21:27 +08:00
耗子 cfff459673 feat: 移除color包 2024-10-25 17:06:04 +08:00
耗子 bba50692b3 fix: test 2024-10-25 16:53:08 +08:00
耗子 595cb03c67 workflow: 去掉warp actions,太慢了 2024-10-25 16:49:32 +08:00
耗子 fa5817febd fix: lint 2024-10-25 16:36:12 +08:00
耗子 9a9fdbdca6 workflow: 支持ipv6网络 2024-10-25 16:32:47 +08:00
耗子 2225e59ca6 fix: 修改权限不使用sudo 2024-10-25 02:43:33 +08:00
耗子 5dce30523b feat: 支持同时输出ipv4 v6地址 2024-10-25 02:41:58 +08:00
耗子 965c39ee08 feat: 创建网站不允许使用443端口 2024-10-25 02:06:27 +08:00
耗子 3507517d5b feat: 优化密码显示,可点击查看 2024-10-24 22:08:30 +08:00
renovate[bot] 17cce7ec2a chore(deps): Update dependency @types/node to v20.17.0 (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 09:43:58 +00:00
耗子 a2b30b07bb fix: 首页mysql数据库数量不显示 2024-10-24 14:03:32 +08:00
耗子 ffb7bfda51 Merge remote-tracking branch 'origin/main' 2024-10-24 13:41:16 +08:00
耗子 d36dd8fded feat: 终端使用可变字体 2024-10-24 13:41:10 +08:00
renovate[bot] a5f69db353 chore(deps): Update dependency sass to v1.80.4 (#295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 04:10:09 +00:00
renovate[bot] 4606c63c81 chore(deps): Update dependency npm-run-all2 to v7.0.1 (#294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 00:34:58 +00:00
耗子 247000718d feat: 优化默认主题配色 2024-10-24 03:31:53 +08:00
耗子 aedde0d433 feat: 发布v2.3.13 2024-10-24 03:08:23 +08:00
耗子 0b61f2f67c fix: 容器管理多一个空元素 2024-10-24 03:04:55 +08:00
耗子 b166022cea feat: 优化监控开关样式 2024-10-24 02:51:53 +08:00
耗子 fb88060824 feat: 大部分视图支持消息总线 2024-10-24 02:24:07 +08:00
耗子 f760dbaf80 refactor: 消息总线 2024-10-24 01:59:52 +08:00
耗子 6050bf611b feat: 重构证书组件 2024-10-24 01:48:57 +08:00
耗子 0b425f09ee feat: 移除思源黑体,减小打包大小 2024-10-24 00:38:01 +08:00
耗子 f18c39ff60 feat: 缩小面板侧边栏宽度 2024-10-24 00:28:45 +08:00
耗子 9db30ac11b fix: 加回旧版的arg检查 2024-10-23 22:51:18 +08:00
耗子 3c8f606d45 feat: 添加一层root用户检查 2024-10-23 22:38:30 +08:00
renovate[bot] 59c08f0850 chore(deps): Update dependency @vue/eslint-config-prettier to v10.1.0 (#292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:54:38 +08:00
renovate[bot] 29a7aa4ca5 chore(deps): Update dependency @types/node to v20.16.15 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:54:27 +08:00
renovate[bot] 8eb75a921e chore(deps): Update dependency @iconify/json to v2.2.263 (#291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 11:47:40 +00:00
耗子 86fb1c392e fix: 优化实时日志modal重载 2024-10-23 15:47:53 +08:00
耗子 df4e47cc7b fix: supervisor不支持重载 2024-10-23 15:35:20 +08:00
耗子 6101bedee3 fix: 优化网站列表获取 2024-10-23 15:26:23 +08:00
耗子 817e86466c fix: 网站列表HTTPS显示不正确 2024-10-23 15:21:52 +08:00
耗子 710601600a Merge remote-tracking branch 'origin/main' 2024-10-23 15:09:24 +08:00
耗子 63a934510e fix: 统一重定向状态码为308 2024-10-23 15:09:17 +08:00
renovate[bot] f3aac307d0 chore(deps): Update dependency vite to v5.4.10 (#290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 13:11:00 +08:00
耗子 e32c887bd7 feat: 终端固定背景颜色防止闪烁 2024-10-23 04:30:14 +08:00
耗子 bc37511b15 fix: 实时日志调整为1000行 2024-10-23 04:24:00 +08:00
耗子 72652eb814 fix: remove debug 2024-10-23 04:18:31 +08:00
耗子 6e3db107ec fix: go embed error 2024-10-23 04:06:04 +08:00
耗子 81843470b7 feat: 发布v2.3.11 2024-10-23 03:13:44 +08:00
renovate[bot] a539532ae2 chore(deps): Update unocss monorepo to v0.63.6 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 03:12:40 +08:00
耗子 fc1584e7da fix: 罕见情况下防火墙规则错误 2024-10-23 03:05:40 +08:00
耗子 3c72a66c1f feat: ssh多机管理 2024-10-23 02:39:43 +08:00
renovate[bot] 346ec7ab1f chore(deps): Update dependency @types/node to v20.16.14 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 07:07:18 +00:00
renovate[bot] 7850b74966 chore(deps): Update dependency npm-run-all2 to v7 (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 10:17:28 +08:00
耗子 f2d2bad52f feat: 初始化ssh模型 2024-10-22 02:59:53 +08:00
耗子 0786451c79 docs: 添加微信群 2024-10-22 02:52:56 +08:00
renovate[bot] 0291ae7637 chore(deps): Lock file maintenance (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:50 +08:00
renovate[bot] de76376e97 chore(deps): Update dependency md-editor-v3 to v4.21.1 (#282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:26 +08:00
耗子 de21179d61 fix: lint 2024-10-21 11:56:53 +08:00
renovate[bot] b81f3de01d chore(deps): Lock file maintenance (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 11:55:54 +08:00
renovate[bot] be3bae7d57 chore(deps): Update module github.com/klauspost/compress to v1.17.11 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:39:07 +08:00
renovate[bot] ffb4e21f48 chore(deps): Update dependency eslint-plugin-vue to v9.29.1 (#279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:58 +08:00
renovate[bot] 39624d8e1c chore(deps): Update dependency @iconify/json to v2.2.262 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:48 +08:00
耗子 1378413e54 fix: lint 2024-10-21 01:35:32 +08:00
耗子 730aea037c feat: 应用支持实时日志 2024-10-21 01:33:13 +08:00
耗子 5ae6135429 fix: 实时日志优化 2024-10-21 01:16:09 +08:00
耗子 84703391a3 feat: 支持实时日志流 2024-10-21 01:08:10 +08:00
耗子 354705ec5b fix: cron添加脚本不保存 2024-10-21 01:07:37 +08:00
耗子 f182eab2dc fix: test 2024-10-20 22:29:25 +08:00
耗子 2828795fb0 fix: test 2024-10-20 22:21:31 +08:00
耗子 e6aad21631 fix: lint 2024-10-20 22:15:23 +08:00
耗子 ff239c467a refactor: 面板websocket 2024-10-20 22:14:11 +08:00
耗子 892f91be3e feat: 开发服务器代理支持多目标 2024-10-20 19:55:14 +08:00
耗子 d51d11f460 feat: 文件远程下载 2024-10-20 19:20:39 +08:00
耗子 f9233bd36b feat: 文件搜索 2024-10-20 18:51:05 +08:00
耗子 a2ebc070ae feat: 文件管理优化 2024-10-20 17:28:02 +08:00
耗子 480a6a829d feat: 终端支持字体缩放 2024-10-20 05:14:48 +08:00
耗子 47b92a8b2a refactor: 重写ssh 2024-10-20 04:38:27 +08:00
耗子 1a7f679fca feat: 发布v2.3.10 2024-10-20 01:47:00 +08:00
耗子 8b6b94ea1f feat: 跑分插件优化 2024-10-20 01:46:25 +08:00
耗子 0d9c8b3a2d feat: 跑分插件 2024-10-20 01:34:22 +08:00
耗子 2be97a4543 feat: 优化颜色选择器 2024-10-19 21:05:25 +08:00
renovate[bot] 7b6b86f0f6 chore(deps): Update dependency @types/node to v20.16.13 (#277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 21:04:55 +08:00
耗子 6f82242d92 feat: 颜色选择器 2024-10-19 21:01:46 +08:00
耗子 7f301cec58 feat: 组件支持固定 2024-10-19 20:47:33 +08:00
耗子 c75cfe9eb6 feat: 前端路由固定 2024-10-19 20:18:23 +08:00
耗子 b94664ed87 Merge remote-tracking branch 'origin/main' 2024-10-19 18:01:26 +08:00
耗子 93fc5e08f6 feat: 前端优化 2024-10-19 18:01:18 +08:00
renovate[bot] 9bcfd4cd73 chore(deps): Update dependency sass to v1.80.3 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 15:35:39 +08:00
耗子 2d10110eef fix: test 2024-10-19 15:01:44 +08:00
耗子 78e31fdc40 chore: 添加aff 2024-10-19 14:41:47 +08:00
耗子 d2b1394950 feat(HTTPS): 证书支持华为云 2024-10-19 05:14:00 +08:00
耗子 389b0a005b feat(HTTPS): 优化证书申请 2024-10-19 03:03:38 +08:00
renovate[bot] 0b1554c2c6 chore(deps): Update dependency npm-run-all2 to v6.2.4 (#274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 17:17:39 +08:00
renovate[bot] 3b580e2827 chore(deps): Update dependency sass to v1.80.2 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 08:48:22 +08:00
耗子 595f5ba2d9 fix: tests 2024-10-18 02:28:08 +08:00
耗子 78543dac67 feat: 发布v2.3.9 2024-10-18 02:11:58 +08:00
耗子 e9cbc8e945 refactor: 重构防火墙 2024-10-18 02:08:55 +08:00
耗子 905e206f9c fix: test 2024-10-17 23:27:11 +08:00
耗子 b5b1992e3b fix: build 2024-10-17 23:05:38 +08:00
耗子 a7fe0c87a5 feat: 首页资源总览优化 2024-10-17 22:46:49 +08:00
耗子 5dcbac1c93 feat(工具箱): 支持设置主机名和时间 2024-10-17 22:17:20 +08:00
耗子 67a6d5cea8 feat: 添加应用图标 2024-10-17 21:22:26 +08:00
耗子 f6fec06e82 feat: 添加应用图标 2024-10-17 21:20:26 +08:00
耗子 2d74e8966e fix: 优化表格样式 2024-10-17 20:30:56 +08:00
耗子 065877547e fix: 固定按钮防止错位 2024-10-17 20:17:06 +08:00
耗子 12b6a1af35 feat: 优化首页布局 2024-10-17 18:55:08 +08:00
耗子 b36f7f3085 feat: 添加密码复杂度验证 2024-10-17 18:40:27 +08:00
耗子 2112e86329 fix: 修复报错Slug不存在 2024-10-17 18:02:48 +08:00
耗子 f2c3569447 fix: 修改部分端口报错 2024-10-17 17:54:33 +08:00
耗子 1a9fe40b80 feat: cli支持更新应用 2024-10-17 17:32:38 +08:00
耗子 df021819b4 feat: 大幅优化移动端体验 2024-10-17 17:18:54 +08:00
耗子 57749f03b9 fix: 防火墙创建规则后未关闭窗口 2024-10-17 16:30:06 +08:00
耗子 d97697efde feat: 发布v2.3.8 2024-10-17 16:25:14 +08:00
耗子 bc28719b10 fix: 修改端口报错 2024-10-17 16:22:01 +08:00
耗子 db79d17919 feat: 支持openEuler 2024-10-17 16:19:22 +08:00
耗子 4e6654af33 fix: 华为欧拉检查 2024-10-17 13:51:41 +08:00
耗子 4015c7e1bc Merge remote-tracking branch 'origin/main' 2024-10-17 13:20:30 +08:00
耗子 d30874a5d4 fix: opencloudos检查 2024-10-17 13:20:24 +08:00
renovate[bot] f4a97c1740 chore(deps): Update dependency sass to v1.80.1 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 12:49:54 +08:00
耗子 7cb497929b fix: cli下安装应用 2024-10-17 12:39:12 +08:00
耗子 8cb74159f2 feat: 发布v2.3.7 2024-10-17 12:28:37 +08:00
renovate[bot] 3b75f7d2ac chore(deps): Update dependency sass to v1.80.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 12:15:24 +08:00
耗子 87c65176aa Merge remote-tracking branch 'origin/main' 2024-10-17 12:09:06 +08:00
耗子 3b3cb1ff24 fix: CLI下不运行队列分发 2024-10-17 12:09:00 +08:00
renovate[bot] a701ebcad6 chore(deps): Update dependency @types/node to v20.16.12 (#270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 03:18:31 +00:00
renovate[bot] 3d88983feb chore(deps): Update dependency @iconify/json to v2.2.261 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 01:37:10 +00:00
耗子 39a9bad2e1 fix: 去掉翻译 2024-10-17 04:45:30 +08:00
耗子 7b0e98bf02 feat: 防火墙完善 2024-10-17 04:44:58 +08:00
耗子 324c61625d feat: 防火墙后端升级 2024-10-17 02:05:27 +08:00
耗子 b50a3f4b4e fix: 首页监控默认10条 2024-10-17 01:13:28 +08:00
耗子 3de3b40d88 feat: 首页全新设计 2024-10-17 00:50:43 +08:00
耗子 722c4f3812 feat: 首页全新设计 2024-10-16 22:54:57 +08:00
耗子 9cf3a0e2b6 fix: 防火墙删不掉 2024-10-16 21:51:37 +08:00
耗子 c80c22c133 refactor: 仪表盘实时数据接口 2024-10-16 17:04:34 +08:00
耗子 5acf1e6eb7 feat: 修改面板端口自动放行 2024-10-16 15:22:47 +08:00
耗子 cdeaf9f48f feat: 完善验证器 2024-10-16 04:09:59 +08:00
耗子 afebeb7c00 feat: 发布v2.3.6 2024-10-16 02:33:43 +08:00
耗子 0d73f2919d workflow: update auto-close 2024-10-16 02:22:12 +08:00
耗子 070ad97e95 workflow: 添加回修订的auto-close 2024-10-16 01:52:52 +08:00
耗子 b7eba92b2b Revert "workflow: 暂时移除auto-close威力太大了"
This reverts commit 2c339b9923.
2024-10-16 01:47:42 +08:00
耗子 2c339b9923 workflow: 暂时移除auto-close威力太大了 2024-10-16 01:28:16 +08:00
耗子 8d0ea50d4f workflow: 添加自动关闭issue 2024-10-16 01:19:44 +08:00
耗子 d6a1c13328 workflow: 添加自动关闭issue 2024-10-16 01:02:37 +08:00
耗子 789e6e2043 docs: 优化描述 2024-10-15 23:55:38 +08:00
耗子 d692b9ac13 docs: 添加自动挂载脚本说明 2024-10-15 23:55:15 +08:00
耗子 eb362e42e3 fix: #260 2024-10-15 18:07:31 +08:00
耗子 3ca106ec87 refactor: 重构证书目录为cert 2024-10-15 18:05:14 +08:00
耗子 377baa0783 fix: build 2024-10-15 17:52:34 +08:00
耗子 117ccabbb5 feat: 优化默认分页条数 2024-10-15 17:51:25 +08:00
耗子 b718c11f3a feat: 离线模式 2024-10-15 17:44:41 +08:00
耗子 41d3d3fb97 feat: 重启后自动调度等待中的任务 2024-10-15 17:18:28 +08:00
耗子 31ad944da8 Merge remote-tracking branch 'origin/main' 2024-10-15 16:53:52 +08:00
耗子 a25de6e4db feat: 仅在cli中打印信息 2024-10-15 16:53:42 +08:00
renovate[bot] 3af6d4d47d chore(deps): Update dependency marked to v14.1.3 (#259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 16:01:11 +08:00
耗子 59a2fef9e8 chore: 优化描述 2024-10-15 11:35:41 +08:00
耗子 3d22a1bc03 chore: 优化描述 2024-10-15 10:11:14 +08:00
耗子 e64c27a821 chore: 添加新赞助商 2024-10-15 10:09:22 +08:00
耗子 f2bdba10bb chore: 添加新赞助商 2024-10-15 10:08:12 +08:00
耗子 e18d07e694 chore: 添加新赞助商 2024-10-15 10:06:05 +08:00
耗子 50ba0ce38d feat: 优化修复流程 2024-10-15 03:46:56 +08:00
耗子 8b2c7ea778 feat: 优化备份信息终端输出 2024-10-15 03:38:52 +08:00
耗子 e640a3cedd feat: 添加更新日期 2024-10-15 03:26:31 +08:00
耗子 31ab4f0fb6 feat: 发布v2.3.5 2024-10-15 03:24:27 +08:00
耗子 8dee17b539 Merge remote-tracking branch 'origin/main' 2024-10-15 03:18:15 +08:00
renovate[bot] af3172739e chore(deps): Update dependency vite to v5.4.9 (#258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 03:16:41 +08:00
耗子 182d9b1c8c feat: 添加面板修复命令 2024-10-15 03:14:38 +08:00
耗子 d03facb556 feat: 优化升级流程 2024-10-15 02:23:24 +08:00
耗子 577fb370a7 feat: 发布v2.3.4 2024-10-15 00:44:24 +08:00
耗子 7ef8fc6787 fix: lint 2024-10-15 00:36:41 +08:00
耗子 e5ba1375b1 feat: 网站管理优化 2024-10-15 00:29:12 +08:00
耗子 e8a01e2d04 refactor: 网站nginx配置解析生成 2024-10-14 21:34:24 +08:00
耗子 bdee27541c feat: nginx解析器完成 2024-10-14 19:07:48 +08:00
renovate[bot] f807da175d chore(deps): Lock file maintenance (#256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:13:19 +08:00
renovate[bot] ae29541d02 chore(deps): Update dependency @iconify/json to v2.2.260 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:12:49 +08:00
renovate[bot] 0f844ac3ea chore(deps): Update module github.com/urfave/cli/v3 to v3.0.0-alpha9.1 (#255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:12:35 +08:00
耗子 744d0b9617 fix: 跨文件系统文件移动优化 2024-10-14 13:03:16 +08:00
耗子 1bea44146f feat: 初步实现nginx配置解析器 2024-10-14 03:32:51 +08:00
耗子 a7a68f6b34 feat: 添加请求验证标签 2024-10-13 23:43:59 +08:00
耗子 80bfe50335 feat: 发布v2.3.3 2024-10-13 22:48:37 +08:00
耗子 f235492f8b feat: 数据备份前端 2024-10-13 22:44:42 +08:00
renovate[bot] e1bcabec5d chore(deps): Update module github.com/go-rat/chix to v1.1.4 (#252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 13:19:04 +00:00
耗子 8a03006772 fix: lint 2024-10-13 17:58:00 +08:00
耗子 e497f5f7d7 fix: build 2024-10-13 17:46:03 +08:00
耗子 5668bc1afd refactor: 备份重构 2024-10-13 17:43:36 +08:00
耗子 bf9c0bc12e feat: 首页优化 2024-10-13 13:59:56 +08:00
耗子 7677c3b50e fix: 部分接口修改 2024-10-13 13:48:18 +08:00
耗子 82a74a538b fix(toolbox): swap setting 2024-10-13 12:19:51 +08:00
耗子 d84e3f75e3 docs: update system table 2024-10-13 04:22:43 +08:00
耗子 ce155af56b docs: update system table 2024-10-13 04:22:01 +08:00
耗子 ebc5fb9796 fix: php拓展安装 2024-10-13 04:01:35 +08:00
耗子 5e237a6bce docs: update system table 2024-10-13 03:35:54 +08:00
耗子 567663a6f5 docs: update system table 2024-10-13 03:13:51 +08:00
耗子 38935607d6 fix: restart panel after update 2024-10-13 01:41:21 +08:00
耗子 0af6b22862 fix: restart panel after update 2024-10-13 01:37:43 +08:00
耗子 787bb80355 fix: version bug 2024-10-13 01:18:23 +08:00
耗子 1785b8ee1a docs: 更新文档 2024-10-13 01:08:05 +08:00
耗子 a223280172 fix: build 2024-10-13 00:49:34 +08:00
耗子 e1cceca340 fix: ssh session 2024-10-13 00:46:06 +08:00
耗子 b9d5aa5fb1 fix: panel version 2024-10-13 00:35:56 +08:00
耗子 3fae54bf58 feat: update acmez 2024-10-13 00:32:46 +08:00
耗子 1b2f9ea1d2 fix: cert eab save 2024-10-13 00:07:02 +08:00
耗子 fb94c015b2 fix: cert query 2024-10-12 23:59:02 +08:00
耗子 1ae8011660 feat: 添加GoogleCN证书 2024-10-12 23:49:20 +08:00
耗子 2978df4981 fix: lint 2024-10-12 22:08:58 +08:00
耗子 99cd473680 feat: 同步修改 2024-10-12 22:05:58 +08:00
耗子 d18bf93571 fix: 修复php拓展安装 2024-10-12 19:24:46 +08:00
耗子 86e4cff989 refactor: 重命名openresty为nginx 2024-10-12 19:13:33 +08:00
耗子 6072332e1a fix: lint 2024-10-12 18:44:16 +08:00
耗子 8721cff4df fix: lint 2024-10-12 18:42:01 +08:00
耗子 cab338c4c4 fix: lint 2024-10-12 18:38:11 +08:00
耗子 de7dd8834f fix: lint 2024-10-12 18:32:13 +08:00
耗子 1d9e2761f3 fix: lint 2024-10-12 18:28:39 +08:00
耗子 6102742377 fix: lint 2024-10-12 18:25:10 +08:00
耗子 641747230b fix: lint 2024-10-12 18:19:27 +08:00
耗子 3c1c4e92b9 feat: 将面板配置固定保存 2024-10-12 18:11:34 +08:00
耗子 6ec5ebb18d fix: lint 2024-10-12 17:35:37 +08:00
耗子 198bd39664 feat: 完善升级 2024-10-12 17:29:38 +08:00
耗子 b561e241a5 Merge remote-tracking branch 'origin/main' 2024-10-12 16:58:22 +08:00
耗子 ff3b755faa feat: 补全更多cli命令 2024-10-12 12:59:58 +08:00
renovate[bot] 7e7b180370 chore(deps): Update dependency @iconify/json to v2.2.259 (#246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 09:05:45 +08:00
耗子 497e357d65 feat: 初步完成升级方法 2024-10-12 04:31:23 +08:00
耗子 09698f7cd3 feat: 安全入口及应用安装检查中间件 2024-10-12 03:40:20 +08:00
耗子 119183971d feat: 支持且默认HTTPS 2024-10-12 02:52:23 +08:00
耗子 96d7e14631 feat: 添加计划任务 2024-10-12 02:25:41 +08:00
耗子 0c4d97a512 feat: 优化队列实现 2024-10-12 00:58:55 +08:00
耗子 3c39027120 docs: 更新安全说明 2024-10-12 00:20:08 +08:00
renovate[bot] 8f6b2647a3 chore(deps): Update dependency vue to v3.5.12 (#245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 23:44:28 +08:00
耗子 ecbf8dd51a Merge remote-tracking branch 'origin/main' 2024-10-11 22:11:01 +08:00
耗子 8a8e6302da fix: setting cli 2024-10-11 22:10:55 +08:00
renovate[bot] 2e45b6c5ab chore(deps): Update dependency eslint-plugin-vue to v9.29.0 (#242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 09:36:33 +00:00
renovate[bot] 962098b1f7 chore(deps): Update dependency sass to v1.79.5 (#241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 04:09:58 +00:00
耗子 7817756569 fix: translation 2024-10-11 03:31:03 +08:00
耗子 6738f3e83d fix: remove database app manage page 2024-10-11 03:29:45 +08:00
耗子 07bb0961da fix: website save 2024-10-11 02:56:28 +08:00
耗子 ac6c57b394 fix: ports 2024-10-11 02:41:42 +08:00
耗子 5082a4f9f9 feat: 全局移除waf 2024-10-11 02:34:18 +08:00
耗子 cfc1d46381 fix: git 2024-10-11 02:03:10 +08:00
耗子 3157011cc0 fix: openresty 2024-10-11 01:54:14 +08:00
耗子 48b06b25da feat: 补全部分面板命令 2024-10-11 01:39:47 +08:00
耗子 f04ae48347 workflow: fix web name 2024-10-11 01:03:06 +08:00
耗子 87d26fbb23 workflow: fix download frontend 2024-10-11 00:58:21 +08:00
耗子 6cfb7f4e0b workflow: fix release 2024-10-11 00:10:55 +08:00
耗子 bee61facad docs: 更新文档 2024-10-11 00:00:30 +08:00
耗子 342bf62ef6 feat: hack files language 2024-10-10 23:47:37 +08:00
耗子 9cd80f8d1b fix: panel path 2024-10-10 21:50:50 +08:00
耗子 f9b53175a1 docs: update README 2024-10-10 21:34:07 +08:00
耗子 6704612482 fix: ci 2024-10-10 21:21:00 +08:00
耗子 d31f40f2dc feat: 优化配置加载 2024-10-10 21:14:54 +08:00
耗子 e02de8ba05 fix: 前端编译 2024-10-10 02:46:15 +08:00
耗子 95f0e0d536 refactor: 应用渠道机制 2024-10-10 02:26:32 +08:00
耗子 7e18d75203 chore: remove eslint-plugin-import 2024-10-10 00:24:36 +08:00
耗子 3de66d884c fix: lint 2024-10-10 00:21:33 +08:00
耗子 a911002953 fix: 修复npm报错 2024-10-10 00:17:04 +08:00
耗子 c5d0b31251 feat: 添加部分路由 2024-10-09 23:41:26 +08:00
耗子 f77e4d17fc feat: 应用商店版本机制重构 2024-10-09 22:22:10 +08:00
renovate[bot] 6892cff96f chore(deps): Update dependency vite-plugin-static-copy to v2 (#237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 15:28:31 +08:00
renovate[bot] 4337bc2718 chore(deps): Update dependency typescript to v5.6.3 (#236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 03:16:05 +00:00
renovate[bot] 82145021b7 chore(deps): Update dependency @iconify/json to v2.2.258 (#235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 18:01:20 +00:00
renovate[bot] d0d98f273c chore(deps): Update unocss monorepo to v0.63.4 (#234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 14:19:06 +00:00
renovate[bot] 7ff18ae0a9 chore(deps): Update dependency @types/node to v20.16.11 (#233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 04:20:13 +00:00
renovate[bot] 7a5d2352fa chore(deps): Update dependency @iconify/json to v2.2.257 (#232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-07 10:14:25 +00:00
renovate[bot] dc0314fc28 chore(deps): Lock file maintenance (#231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-07 11:41:04 +08:00
renovate[bot] 9ca2445d4a chore(deps): Update dependency vue-i18n to v10.0.4 (#230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 13:41:41 +00:00
耗子 ba42e33b72 fix: eslint报错 2024-10-06 11:53:43 +08:00
renovate[bot] 06c0c08ead chore(deps): Update dependency @vue/eslint-config-typescript to v14 (#229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 10:57:31 +08:00
renovate[bot] dc96f9d4ac chore(deps): Update dependency @vue/eslint-config-prettier to v10 (#228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 21:33:52 +00:00
renovate[bot] e7a54cbe85 chore(deps): Update module golang.org/x/net to v0.30.0 (#227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 01:14:08 +00:00
renovate[bot] 39f03106df chore(deps): Update module golang.org/x/crypto to v0.28.0 (#226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 21:10:42 +00:00
renovate[bot] 24ec5d0bb3 chore(deps): Update unocss monorepo to v0.63.3 (#225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 10:30:42 +00:00
renovate[bot] 22cd9f8ce1 chore(deps): Update module github.com/knadh/koanf/providers/file to v1.1.2 (#224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 01:18:53 +00:00
renovate[bot] c5efc2fb33 chore(deps): Update dependency vue to v3.5.11 (#223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-03 22:29:25 +00:00
renovate[bot] 24b633d24c chore(deps): Update dependency eslint-plugin-import to v2.31.0 (#222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-03 15:08:40 +00:00
renovate[bot] 6f994e2282 chore(deps): Update dependency @iconify/json to v2.2.256 (#221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-03 12:43:52 +00:00
renovate[bot] 946a8198a3 chore(deps): Update module github.com/mholt/acmez/v2 to v2.0.3 (#220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-03 00:25:19 +00:00
renovate[bot] 39ff8c9040 chore(deps): Update module github.com/knadh/koanf/providers/file to v1.1.1 (#219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-02 21:28:21 +00:00
renovate[bot] 65ebecafdb chore(deps): Update unocss monorepo to v0.63.2 (#218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-02 07:06:34 +00:00
耗子 af5e79ce37 fix: lint 2024-10-02 04:55:14 +08:00
耗子 a37393b688 feat: 日志记录器优化 2024-10-02 02:18:36 +08:00
耗子 c18a96e439 feat: 设置完善及日志记录器 2024-10-02 01:49:07 +08:00
renovate[bot] 1208144605 chore(deps): Update dependency pinia to v2.2.4 (#217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 22:00:40 +08:00
renovate[bot] 07c1c110d9 chore(deps): Update dependency @iconify/json to v2.2.255 (#216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 13:25:04 +00:00
renovate[bot] dd66274997 chore(deps): Update dependency pinia to v2.2.3 (#215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 18:04:52 +00:00
耗子 dc0174c64e feat: 设置页面初步完善及前端import lint 2024-09-30 19:00:26 +08:00
耗子 67055a5aac Merge remote-tracking branch 'origin/main' 2024-09-30 17:52:36 +08:00
耗子 54470f87b0 feat: 命令行获取信息和初始化 2024-09-30 17:52:23 +08:00
renovate[bot] 41a61a2c15 chore(deps): Lock file maintenance (#214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 03:51:45 +00:00
renovate[bot] 17390a026f chore(deps): Update unocss monorepo to v0.63.1 (#213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-29 07:43:57 +00:00
耗子 3fce818d13 feat: 全局移除carbon 2024-09-29 03:31:27 +08:00
耗子 169c0ff18e fix: go.sum 2024-09-29 03:08:52 +08:00
耗子 6a30d56d7d feat: 全局移除swagger 2024-09-29 03:08:26 +08:00
耗子 4ad7ec58be feat: 本地加载monaco编辑器以及去掉无用的插件 2024-09-29 01:56:50 +08:00
耗子 d4feba17d0 feat: 去掉nginx高亮,优化登录界面,解决时区和cookie问题 2024-09-29 01:12:43 +08:00
耗子 e5f36df4bf feat: 前端重构1 2024-09-28 22:37:51 +08:00
renovate[bot] e1b53e88ec chore(deps): Update dependency @iconify/json to v2.2.254 (#211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 17:35:14 +08:00
renovate[bot] d12aabd4d3 chore(deps): Update unocss monorepo to ^0.63.0 (#212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 17:35:02 +08:00
renovate[bot] 54c893d6e6 chore(deps): Update dependency sass to v1.79.4 (#210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 17:22:19 +08:00
耗子 4e01168f3c fix: lint 2024-09-28 11:52:52 +08:00
耗子 5bb43dbf0f fix: build 2024-09-28 11:24:19 +08:00
耗子 dcb0d667ea fix: build 2024-09-28 11:10:42 +08:00
耗子 af8ef1abc8 feat: 实现应用expr依赖判断 2024-09-28 11:07:25 +08:00
耗子 2fc19dd97e fix: lint 2024-09-28 10:16:02 +08:00
耗子 b0e577b42a fix: lint 2024-09-28 10:07:45 +08:00
耗子 051b6b9227 Merge remote-tracking branch 'origin/main' 2024-09-28 02:43:27 +08:00
耗子 92ac563d75 feat: 提交剩余的插件 2024-09-28 02:43:00 +08:00
renovate[bot] b7b775971e chore(deps): Update dependency @types/node to v20.16.10 (#209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-27 18:04:22 +00:00
renovate[bot] 32266ca7c8 chore(deps): Update dependency vue to v3.5.10 (#207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 01:15:22 +08:00
耗子 73387d114e fix: lint 2024-09-27 22:55:12 +08:00
耗子 3271557561 fix: percona插件加载 2024-09-27 22:53:06 +08:00
耗子 9a295241c3 feat: 提交一批插件 2024-09-27 22:51:54 +08:00
耗子 a00b44c485 Merge remote-tracking branch 'origin/main' 2024-09-27 17:48:04 +08:00
耗子 93b72ad45f feat: 优化防火墙 2024-09-27 17:47:58 +08:00
renovate[bot] a86b35f587 chore(deps): Update module github.com/go-resty/resty/v2 to v2.15.3 (#206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 21:25:38 +00:00
耗子 4054c788f3 feat: 初始化cli路由 2024-09-27 03:08:56 +08:00
renovate[bot] 881eb7bfb1 chore(deps): Update dependency @types/node to v20.16.9 (#202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-27 00:31:18 +08:00
renovate[bot] fcc35c19ee chore(deps): Update dependency vue to v3.5.9 (#205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 22:50:39 +08:00
renovate[bot] 7c3b5c02ba chore(deps): Update dependency @iconify/json to v2.2.253 (#204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 14:33:58 +08:00
renovate[bot] bb70b25565 chore(deps): Update dependency naive-ui to v2.40.1 (#203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 06:11:06 +00:00
renovate[bot] bc2e07bcd8 chore(deps): Update dependency naive-ui to v2.40.0 (#201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 00:21:25 +00:00
renovate[bot] aa74f30893 chore(deps): Update dependency @types/node to v20.16.8 (#200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 22:25:02 +00:00
renovate[bot] 27687d7dbf chore(deps): Update dependency vite to v5.4.8 (#199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 15:33:20 +08:00
renovate[bot] f6f21d7b70 chore(deps): Update dependency @types/node to v20.16.7 (#197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 12:10:38 +08:00
renovate[bot] 0815507aa8 chore(deps): Update dependency @types/node to v20.16.6 (#195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-24 02:01:10 +00:00
renovate[bot] fb40b140c9 chore(deps): Update dependency @iconify/json to v2.2.252 (#194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 10:55:19 +00:00
renovate[bot] eedf07ec2a chore(deps): Lock file maintenance (#193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 07:13:05 +00:00
renovate[bot] 5c214625c3 chore(deps): Update module github.com/go-gormigrate/gormigrate/v2 to v2.1.3 (#192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 03:55:27 +00:00
renovate[bot] 0402805f24 chore(deps): Update dependency vue-i18n to v10.0.3 (#191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 22:57:44 +08:00
renovate[bot] 40715dd8a3 chore(deps): Update dependency vue to v3.5.8 (#190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 15:00:25 +08:00
renovate[bot] 505e6d4794 chore(deps): Update module github.com/go-resty/resty/v2 to v2.15.2 (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 04:54:48 +00:00
renovate[bot] 04f3925e6d chore(deps): Update dependency vue-i18n to v10.0.2 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 22:09:36 +00:00
renovate[bot] c77fa3f0ea chore(deps): Update dependency sass to v1.79.3 (#187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 04:17:14 +00:00
renovate[bot] 51ede84e0a chore(deps): Update dependency @iconify/json to v2.2.251 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 01:36:53 +00:00
renovate[bot] 053ef46f95 chore(deps): Update module github.com/docker/docker to v27.3.1+incompatible (#186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 21:52:17 +00:00
耗子 11d58d838f feat: cli 路由支持 2024-09-21 03:12:55 +08:00
renovate[bot] 13b15ccb32 chore(deps): Update dependency vue to v3.5.7 (#184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 02:59:00 +08:00
renovate[bot] 255acb392c chore(deps): Update dependency vite to v5.4.7 (#183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 23:46:17 +08:00
renovate[bot] 6c16901dbc chore(deps): Update dependency sass to v1.79.2 (#182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 03:02:30 +00:00
renovate[bot] 284a4c67d4 chore(deps): Update module github.com/docker/docker to v27.3.0+incompatible (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 00:48:24 +00:00
renovate[bot] 484ad397e7 chore(deps): Update module github.com/go-resty/resty/v2 to v2.15.1 (#181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 11:03:12 +00:00
耗子 407203dcfa feat: cli 路由支持 2024-09-19 01:37:39 +08:00
耗子 dded5e26aa feat: 支持远程插件 (#180)
* feat: api基本完成

* feat: 阶段性提交

* feat: 初步支持远程插件

* feat: 支持远程插件

* fix: tests
2024-09-19 01:17:29 +08:00
耗子 d029e6c58e Merge remote-tracking branch 'origin/main' 2024-09-18 19:13:23 +08:00
耗子 c1c0cbbac5 refactor: rename plugin to apps 2024-09-18 19:13:17 +08:00
耗子 0fffe3a644 refactor: rename app to web 2024-09-18 19:09:33 +08:00
耗子 2e26af435a refactor: rename app to panel 2024-09-18 19:08:45 +08:00
耗子 ef2ecfcc80 feat: api初步支持 2024-09-18 19:02:49 +08:00
renovate[bot] 6fe4d82619 chore(deps): Update dependency @iconify/json to v2.2.250 (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 10:30:10 +00:00
renovate[bot] 29562974ce chore(deps): Update dependency @vitejs/plugin-vue to v5.1.4 (#177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 07:11:40 +00:00
renovate[bot] b7f169634b chore(deps): Update dependency sass to v1.79.1 (#178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 12:11:45 +08:00
renovate[bot] 87e3011f18 chore(deps): Update dependency sass to v1.79.0 (#176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 03:50:25 +00:00
renovate[bot] cc714a5007 chore(deps): Update module github.com/go-rat/gormstore to v1.0.6 (#175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 21:31:33 +00:00
耗子 615744e0cf feat: 测试插件 2024-09-18 02:25:24 +08:00
耗子 3193a15131 workflow: 防止混淆 2024-09-18 01:47:58 +08:00
耗子 194287554e refactor: migrate to chi framework (#165)
* 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>
2024-09-18 01:43:14 +08:00
耗子 97b1d1a4bb feat: 更新脚本 2024-09-06 02:19:21 +08:00
耗子 b150f61862 feat: 更新说明 2024-09-02 23:10:51 +08:00
耗子 b82bc21d1b feat: 更新说明 2024-08-25 02:12:25 +08:00
renovate[bot] 6e9a4963af chore(deps): Update golang Docker tag to v1.23 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 00:52:12 +00:00
renovate[bot] b4a3708fa0 chore(deps): Update module github.com/docker/docker to v27.1.2+incompatible (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 19:50:06 +00:00
renovate[bot] 9bca8173f2 chore(deps): Update module github.com/mholt/acmez/v2 to v2.0.2 (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-09 00:29:56 +00:00
renovate[bot] 6520aba081 chore(deps): Update module golang.org/x/net to v0.28.0 (#150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 04:42:41 +00:00
renovate[bot] 2903d00550 chore(deps): Update module golang.org/x/crypto to v0.26.0 (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 02:29:21 +00:00
renovate[bot] a53ba36db8 chore(deps): Update module github.com/spf13/cast to v1.7.0 (#148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-06 21:06:13 +00:00
renovate[bot] e9f2854d59 chore(deps): Update module github.com/go-resty/resty/v2 to v2.14.0 (#147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 10:53:32 +00:00
耗子 43cd3bf38d fix: ioncube安装 2024-08-05 13:03:52 +08:00
耗子 325a218e20 refactor: 插件 2024-07-29 03:25:53 +08:00
耗子 847e55bc8b refactor: 插件 2024-07-29 01:21:50 +08:00
耗子 1ebddd289c refactor: 移动http方法到pkg 2024-07-28 20:58:33 +08:00
耗子 e3f8fe8fe9 feat: 发布v2.2.27 2024-07-28 16:46:58 +08:00
耗子 b9996ba41e feat: 更新依赖 2024-07-28 16:29:34 +08:00
耗子 bb1cd025f1 fix: 更新爱发电域名 2024-07-28 16:19:49 +08:00
耗子 a3059eb1cb fix: ufw添加带ip的规则后报错 2024-07-28 16:06:06 +08:00
耗子 192a1044ed Merge remote-tracking branch 'origin/main' 2024-07-28 15:54:05 +08:00
耗子 bbc6e3c7cc fix: 修复debian下cron不生效 2024-07-28 15:53:57 +08:00
renovate[bot] 0aaea53af6 chore(deps): Update module github.com/docker/docker to v27.1.1+incompatible (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 03:02:23 +00:00
renovate[bot] 2ad9f48a5b chore(deps): Update module github.com/docker/docker to v27.1.0+incompatible (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 18:01:46 +00:00
耗子 0e715a7576 refactor: 规范go module版本号 2024-07-13 15:19:42 +08:00
耗子 724610a45d feat: 发布v2.2.25 2024-07-13 04:17:45 +08:00
耗子 f65cf9e38e feat: 优化设置项 2024-07-13 03:40:07 +08:00
耗子 4a8ebfbdfa feat: 发布v2.2.24 2024-07-13 03:37:18 +08:00
耗子 fee0fcb998 fix: 等待中的任务显示为运行中 2024-07-13 03:25:55 +08:00
耗子 1e081fcdf2 feat(网站): 支持设置QUIC和OCSP 2024-07-13 03:17:21 +08:00
耗子 6c6fb09270 feat: 重定向至登录页面 2024-07-13 01:54:42 +08:00
耗子 51b47818c0 feat: 发布v2.2.23 2024-07-13 01:48:18 +08:00
耗子 5b393819ba fix: optimize ci 2024-07-13 01:24:57 +08:00
耗子 f807d0cd1a fix: embed目录错误 2024-07-13 01:20:41 +08:00
耗子 0d6f0f317d fix: embed目录错误 2024-07-13 01:18:03 +08:00
耗子 6bad7f4b5b refactor: 重构入口和前端加载逻辑 2024-07-13 01:17:24 +08:00
耗子 7a7134b26a chore: 更新框架 2024-07-12 22:55:11 +08:00
耗子 28cf7098ff feat: 发布v2.2.22 2024-07-12 18:20:56 +08:00
耗子 b56979147e feat: 发布v2.2.21 2024-07-12 16:51:39 +08:00
耗子 2ec0d9e844 Merge remote-tracking branch 'origin/main' 2024-07-12 16:32:54 +08:00
耗子 66641a626e feat: 更新框架 2024-07-12 16:32:47 +08:00
renovate[bot] 95bfd6843b chore(deps): Update module gorm.io/gorm to v1.25.11 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-11 15:46:32 +00:00
耗子 7e28a34b7f refactor: 重构filter标签 2024-07-11 16:54:34 +08:00
耗子 b2152edeeb feat: session支持刷新 2024-07-11 14:52:16 +08:00
耗子 00815979d2 feat: 发布v2.2.20 2024-07-11 02:59:33 +08:00
耗子 c4e01b37b7 refactor: 使用session鉴权 2024-07-11 02:51:49 +08:00
耗子 7907345521 feat: 更新依赖 2024-07-11 01:08:37 +08:00
耗子 8f32d6cdb0 feat: 优化版本号处理 2024-07-08 04:56:00 +08:00
耗子 c3582903bf fix: CVE-2024-0406 2024-07-08 04:35:52 +08:00
renovate[bot] 7d25d2f5aa chore(deps): Update module golang.org/x/net to v0.27.0 2024-07-05 20:03:32 +00:00
renovate[bot] 46fc7b1c6a chore(deps): Update module golang.org/x/crypto to v0.25.0 2024-07-05 15:11:03 +00:00
耗子 34ac62d1ae fix: lint 2024-07-03 02:03:58 +08:00
耗子 30e78ccf46 feat: 发布v2.2.19 2024-07-03 02:02:38 +08:00
耗子 22f4cc7fcb feat: 支持腾讯云 2024-07-03 01:45:25 +08:00
耗子 1771ae682d Merge remote-tracking branch 'origin/main' 2024-07-02 21:07:19 +08:00
耗子 d1beb60ffd fix: service文件错误 2024-07-02 21:06:57 +08:00
renovate[bot] cba5f86667 chore(deps): Update module github.com/docker/docker to v27.0.3+incompatible 2024-07-02 10:03:44 +00:00
耗子 e1d9832ce0 fix: lint 2024-07-01 15:49:24 +08:00
耗子 6278535902 fix: lint 2024-06-28 23:40:59 +08:00
耗子 32fb87243b feat: 部分软件增加安装校验 2024-06-28 16:41:57 +08:00
耗子 5f85e7fbe0 feat: 添加PostgreSQL的操作方法 2024-06-28 04:01:46 +08:00
耗子 c36104a075 feat: 去掉mysql命令的路径 2024-06-28 03:25:59 +08:00
耗子 583f5d13c7 feat: 优化安装脚本 2024-06-28 03:14:26 +08:00
耗子 b453738daf fix: docker的弃用警告 2024-06-28 03:05:46 +08:00
耗子 98431fb4b2 feat: 发布v2.2.18 2024-06-28 02:53:45 +08:00
耗子 4d76e9286d feat: 删除网站支持删除数据库 2024-06-28 02:52:31 +08:00
耗子 c70758339f fix: build 2024-06-28 01:58:43 +08:00
耗子 ff524cdb0b feat: update dependencies 2024-06-28 01:32:32 +08:00
耗子 73c95d880a Merge remote-tracking branch 'refs/remotes/origin/renovate/github.com-docker-docker-27.x' 2024-06-28 01:28:31 +08:00
耗子 8a1d8c0b05 fix: 优化数据库正则 2024-06-28 01:20:37 +08:00
耗子 2251175906 docs: update readme 2024-06-27 19:13:45 +08:00
机器人 69edde98c8 chore(deps): Update module github.com/docker/docker to v27 2024-06-27 04:07:08 +00:00
耗子 29a3db640d fix: 去掉错误的参数 2024-06-25 18:43:55 +08:00
耗子 ee87bf65b4 feat: 调整部分参数 2024-06-25 18:42:00 +08:00
耗子 aa080dab15 feat: 发布v2.2.17 2024-06-25 03:37:37 +08:00
耗子 b4e5498b48 feat: OpenResty支持返回具体错误 2024-06-25 03:35:35 +08:00
耗子 63838bffa1 feat: phpMyAdmin支持编辑配置 2024-06-25 03:07:38 +08:00
耗子 fa41b18ced fix: 优化查询 2024-06-25 02:51:34 +08:00
耗子 31bd15217b feat: 添加日志记录中间件 2024-06-25 02:38:13 +08:00
耗子 e6b6f06706 feat: 尝试优化内存 2024-06-25 02:08:13 +08:00
耗子 1b836297bf feat: 尝试优化内存 2024-06-25 01:52:41 +08:00
耗子 96bea2cf90 fix: 去掉无用的检查 2024-06-25 01:23:01 +08:00
耗子 8aff513284 fix: 禁止80端口使用HTTPS 2024-06-25 01:13:24 +08:00
耗子 368e41a512 feat: 发布v2.2.16 2024-06-25 01:01:07 +08:00
耗子 924fdee7c5 feat: 支持设置HTTPS端口 2024-06-25 01:00:08 +08:00
耗子 438ba1db1e fix: lint 2024-06-24 23:50:53 +08:00
耗子 d4e52a2d87 feat: ZeroSSL支持自动获取EAB 2024-06-24 23:46:13 +08:00
耗子 4432a12c7a feat: 临时性手段阻止任务重复添加 2024-06-24 23:28:35 +08:00
耗子 a75e36e447 feat: 优化任务 2024-06-24 23:10:39 +08:00
耗子 05674ee04e feat: 优化迁移 2024-06-24 22:53:43 +08:00
耗子 09703761eb fix(文件): 权限设置错误 2024-06-24 22:20:41 +08:00
耗子 dfa14c3fd4 refactor: MySQL改密逻辑 2024-06-24 21:52:46 +08:00
耗子 7b80f034ea feat: 安装前先清理文件 2024-06-24 19:46:43 +08:00
耗子 71923093fd feat(文件): 解压不再判断路径存在 2024-06-24 19:11:01 +08:00
耗子 aae3cf0518 fix: 统一格式 2024-06-24 03:46:21 +08:00
耗子 ed22e79552 feat: 防止密码爆破攻击 2024-06-24 03:45:38 +08:00
耗子 15dd5bb718 feat: 发布v2.2.15 2024-06-23 05:09:30 +08:00
耗子 0e7af2914e feat: 发布v2.2.14 2024-06-23 04:52:18 +08:00
耗子 7ec378829b feat: 优化https证书签发 2024-06-23 04:44:14 +08:00
耗子 f2e8ff86c6 feat: 发布v2.2.13 2024-06-23 03:19:39 +08:00
耗子 eed03d644f fix: test 2024-06-23 03:19:13 +08:00
耗子 d9be877710 feat: 发布v2.2.12 2024-06-23 03:03:38 +08:00
耗子 10439d7d97 feat: 优化证书信息获取 2024-06-23 02:42:01 +08:00
耗子 55eaa26970 feat: 添加面板HTTPS接口 2024-06-23 02:35:25 +08:00
耗子 0d769d3400 fix: lint 2024-06-23 01:46:14 +08:00
耗子 7ff084e5a8 refactor: 重构types包 2024-06-23 01:32:45 +08:00
耗子 89e133c626 refactor: 重构str包 2024-06-23 01:12:51 +08:00
耗子 c876f4c484 refactor: 重构os包 2024-06-23 01:08:07 +08:00
耗子 85c38e5bc5 refactor: 重构io包 2024-06-23 00:59:07 +08:00
耗子 6bd07251fc refactor: 重构systemctl包 2024-06-23 00:48:37 +08:00
耗子 11d6b40dce refactor: 重构shell命令执行 2024-06-23 00:44:14 +08:00
耗子 aa9b55d1eb feat: bump v2.2.11 2024-06-19 02:06:58 +08:00
耗子 2e97812776 fix: 修复版本号比较错误 2024-06-19 02:06:39 +08:00
耗子 e0a7ada6d5 feat(监控): 去掉cpu型号信息 2024-06-19 01:58:40 +08:00
耗子 d48963ab50 fix: 由于v2.2.8迁移问题导致更新脚本可能未运行 2024-06-19 01:20:48 +08:00
耗子 59bfed66dc chore: 发布v2.2.10 2024-06-19 01:18:38 +08:00
耗子 5c0bc8a1b7 feat: 安装后删除安装脚本和checksum 2024-06-19 01:15:00 +08:00
耗子 ae9e39d0fe feat: 优化分页函数 2024-06-19 00:47:57 +08:00
耗子 49b72aa656 feat: 一些细节优化 2024-06-18 13:19:28 +08:00
耗子 e0f642cbff feat: 一些细节优化 2024-06-18 12:58:06 +08:00
耗子 eb2c6cab81 feat: 优化防火墙启停流程 2024-06-18 10:09:53 +08:00
耗子 0a3d3828c8 feat: 优化验证器使用 2024-06-18 03:41:34 +08:00
耗子 46b431de30 fix: 一些小问题 2024-06-18 03:30:59 +08:00
耗子 8a5b46ad7e fix(容器): 分页为空时不返回null 2024-06-18 03:13:39 +08:00
耗子 5bd14946de refactor: 重构postgresql插件 2024-06-18 03:04:37 +08:00
耗子 f6c091c725 feat: 更新文档 2024-06-18 01:35:05 +08:00
耗子 f4dc94ad9a feat: 更新网站文档 2024-06-18 01:32:04 +08:00
耗子 8bcc0c0c05 feat: 更新openresty插件文档 2024-06-18 01:29:44 +08:00
耗子 0be04e1859 feat: 更新php插件文档 2024-06-18 01:26:47 +08:00
耗子 bec9336f2f feat: 重构php插件 2024-06-18 01:20:24 +08:00
耗子 0be4ce9891 revert: 加回must_install中间件 2024-06-18 00:48:48 +08:00
耗子 634a9a3219 feat: 优化编译线程数计算算法 2024-06-18 00:32:47 +08:00
耗子 1f2a32f883 feat: 检查接口返回插件名 2024-06-18 00:28:35 +08:00
耗子 32a56aa2dc feat: 去掉must install中间件和提供新接口检查插件安装 2024-06-17 22:03:38 +08:00
耗子 8185a69354 feat: optimize mysql api 2024-06-17 21:48:28 +08:00
耗子 d41862f63b fix: 自启动podman socket 2024-06-16 04:38:28 +08:00
耗子 20e5122425 fix: lint 2024-06-16 04:20:58 +08:00
耗子 17bb8ad62b fix: debian下缺失podman存储配置 2024-06-16 04:20:39 +08:00
耗子 43624de2e4 feat: 优化自动迁移 2024-06-16 03:55:59 +08:00
耗子 cc6a45b127 feat: 修改模型 2024-06-14 22:52:05 +08:00
耗子 7a4a71f0a0 fix: tests 2024-06-14 21:03:15 +08:00
耗子 27cbb92a06 fix: build 2024-06-14 21:00:12 +08:00
耗子 5ddbdfc5df fix: lint 2024-06-14 19:58:49 +08:00
耗子 b3b1b0971f feat: use new migrate 2024-06-14 19:56:44 +08:00
耗子 9caad365e9 Merge remote-tracking branch 'origin/main' 2024-06-14 19:49:33 +08:00
耗子 5b54232e16 refactor: 移动pureftpd类型定义 2024-06-14 19:49:23 +08:00
机器人 a510bdb05d Merge branch 'renovate/github.com-gorilla-websocket-1.x' into 'main'
chore(deps): Update module github.com/gorilla/websocket to v1.5.3

See merge request opensource/panel!77
2024-06-14 04:12:54 +00:00
机器人 50a8b16832 chore(deps): Update module github.com/gorilla/websocket to v1.5.3 2024-06-14 04:07:55 +00:00
耗子 2f56241da1 refactor: 移动pureftpd类型定义 2024-06-14 11:30:30 +08:00
耗子 8557259b53 feat: 移除部分软件服务接口 2024-06-14 11:28:02 +08:00
耗子 1d7ea0280a feat: 移除部分软件服务接口 2024-06-14 11:10:52 +08:00
耗子 10145da2f9 feat: 移除部分软件服务接口 2024-06-14 10:59:22 +08:00
耗子 6ba7d01875 feat: 添加服务操作接口 2024-06-14 10:33:51 +08:00
耗子 763fa7379d Merge remote-tracking branch 'origin/renovate/goreleaser-goreleaser-action-6.x' 2024-06-14 02:44:56 +08:00
耗子 a36eaf7e9a feat: 自启动podman-restart 2024-06-13 02:38:29 +08:00
耗子 ec10cd30a5 feat: 发布v2.2.8 2024-06-12 01:37:40 +08:00
耗子 847fae5caa feat: 名称统一为耗子面板 2024-06-12 01:36:35 +08:00
耗子 b3b8c22634 feat: 名称统一为耗子面板 2024-06-12 01:32:11 +08:00
耗子 5f504d49ef feat: 创建容器后自动启动 2024-06-12 01:09:09 +08:00
机器人 18de16047d chore(deps): Update goreleaser/goreleaser-action action to v6 2024-06-08 10:05:02 +00:00
815 changed files with 53074 additions and 41987 deletions
+56 -19
View File
@@ -1,32 +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]
bin = "./storage/temp/main.exe --env=panel.conf"
cmd = "go build -o ./storage/temp/main.exe ."
delay = 1000
exclude_dir = ["storage", "database"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
# 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/web"
# 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 = ""
include_dir = []
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
# 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"
send_interrupt = false
# 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
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
# 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]
clean_on_exit = false
# Delete tmp directory on exit
clean_on_exit = true
[screen]
clear_on_rebuild = true
keep_scroll = true
+6
View File
@@ -0,0 +1,6 @@
*.sh linguist-language=Go
*.ts linguist-language=Go
*.js linguist-language=Go
*.css linguist-language=Go
*.scss linguist-language=Go
*.html linguist-language=Go
+1 -1
View File
@@ -1,4 +1,4 @@
# These are supported funding model platforms
open_collective: tnb
custom: ['https://afdian.net/a/TheTNB']
custom: [ 'https://afdian.com/a/TheTNB' ]
+14 -15
View File
@@ -19,15 +19,13 @@ body:
options:
- label: 我已经搜索了现有的 Issues, Discussions 和 Google (I've searched the existing Issues, Discussions and Google)
required: true
- label: 我已经阅读了 README 中除版本记录以外的所有内容 (I've read everything in the README except the version notes)
required: true
- label: 我在提问题之前至少花费了 5 分钟来思考和准备 (I spend at least 5 minutes thinking and preparing before asking a question)
- label: 我已经阅读了文档中除版本记录以外的所有内容 (I've read everything in the documentation except the version notes)
required: true
- label: 我没有改动过面板及系统的任何文件 (I have not changed any files of the panel and system)
required: false
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
required: false
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
required: false
- type: dropdown
id: system
@@ -37,9 +35,10 @@ body:
请提供安装面板的操作系统。
Please provide the operating system on which the panel is installed.
options:
- RockyLinux
- AlmaLinux
- Debian
- Ubuntu
- AlmaLinux
- RockyLinux
- Other
validations:
required: true
@@ -50,17 +49,17 @@ body:
description: |
请提供操作系统的版本号。
Please provide the version number of the operating system.
placeholder: "9.0"
placeholder: "12"
validations:
required: true
- type: input
id: version
attributes:
label: 耗子面板版本 (HaoZi Panel Version)
label: 耗子面板版本 (Rat Panel Version)
description: |
请提供面板的版本号。
Please provide the version number of the panel.
placeholder: "2.0.0"
placeholder: "2.3.0"
validations:
required: true
- type: textarea
@@ -68,7 +67,7 @@ body:
attributes:
label: 描述问题 (Describe The Problem)
description: |
简明概要地描述遇到的问题。
简明概要地描述遇到的问题。
Briefly describe the problem you are having.
validations:
required: true
@@ -91,7 +90,7 @@ body:
attributes:
label: 预期行为 (Expected Behavior)
description: |
简明概要地描述期望发生的事情。
简明概要地描述期望发生的事情。
Briefly describe what you expect to happen.
validations:
required: true
@@ -104,8 +103,8 @@ body:
Please copy and paste any relevant log output.
可以把文件拖入这个区域以添加日志文件。
Files can be dragged into this area to add log files.
面板日志文件可在 `/www/panel/storage/logs` 中找到。
Panel log files can be found in `/www/panel/storage/logs`.
面板日志文件可在安装目录 `panel/storage/logs` 中找到。
Panel log files can be found in the installation directory `panel/storage/logs`.
validations:
required: false
- type: textarea
@@ -113,7 +112,7 @@ body:
attributes:
label: 截图 (Screenshots)
description: |
如果有,添加屏幕截图可帮助更快定位的问题。
如果有,添加屏幕截图可帮助更快定位的问题。
If so, adding screenshots can help locate your issue faster.
可以复制图片后在此区域内粘贴以添加图片。
Pictures can be copied and pasted in this area to add pictures.
@@ -128,7 +127,7 @@ body:
description: |
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
Environment? Browser? Software version? Related configuration? Link? References?
任何能让我们对所遇到的问题有更多了解的东西。
任何能让我们对所遇到的问题有更多了解的东西。
Anything that can give us more insight into the problem you're having.
validations:
required: false
+1 -1
View File
@@ -25,7 +25,7 @@ body:
attributes:
label: 描述功能 (Describe Feature)
description: |
简明概要地描述的新功能,以及它将解决什么问题。
简明概要地描述的新功能,以及它将解决什么问题。
Briefly describe your new feature and what problem it will solve.
validations:
required: true
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+55
View File
@@ -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: 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 web-${{ matrix.goarch }} ./cmd/web
go build -ldflags '-s -w --extldflags "-static"' -o cli-${{ matrix.goarch }} ./cmd/cli
- name: Compress ${{ matrix.goarch }}
run: |
upx --best --lzma web-${{ matrix.goarch }}
upx --best --lzma cli-${{ matrix.goarch }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.goarch }}
path: |
web-${{ matrix.goarch }}
cli-${{ matrix.goarch }}
-32
View File
@@ -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"' -o panel-${{ matrix.goarch }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: panel-${{ matrix.goarch }}
path: panel-${{ matrix.goarch }}
-23
View File
@@ -1,23 +0,0 @@
name: Codecov
on:
push:
branches:
- main
pull_request:
jobs:
codecov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install dependencies
run: go mod tidy
- name: Run tests with coverage
run: go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
token: ${{ secrets.CODECOV }}
+42
View File
@@ -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.example.ts 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
+19 -12
View File
@@ -7,27 +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: |
sudo apt-get install -y curl jq unzip zip
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
rm -rf public
unzip frontend.zip
mv dist public
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: frontend.yml
name: frontend
path: internal/embed/frontend
check_artifacts: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
name: Issue Auto Lock
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
issues:
types: [ opened ]
permissions:
issues: write
contents: read
jobs:
issue-lock:
runs-on: ubuntu-latest
steps:
- name: Set GH_REPO
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
- name: Auto Lock Issue
uses: devhaozi/issue-auto-lock@v1
with:
gh_repo: ${{ github.repository }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
issue_labels: "⭐ No Star"
+3 -6
View File
@@ -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)
@@ -1,25 +0,0 @@
name: Issue Close Question
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
issue-close-require:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: needs more info
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
labels: 'question'
inactive-day: 3
body: |
由于该 Issue 3天未收到回应,现已被自动关闭,若有任何问题,可评论回复。
This issue has been closed automatically because it has not had recent activity for 3 days. If you have any questions, please comment here.
+63 -8
View File
@@ -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
+15 -10
View File
@@ -5,20 +5,25 @@ 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
go run . artisan migrate
cp config.example.yml config.yml
- name: Run tests
run: go test ./...
run: sudo go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
token: ${{ secrets.CODECOV }}
+8 -17
View File
@@ -1,10 +1,6 @@
tmp
/.air.toml
/panel.conf
# Golang #
# `go test -c` 生成的二进制文件
*.test
# go coverage 工具
*.out
*.prof
@@ -14,16 +10,15 @@ _cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
# 编译文件 #
# 编译文件
*.com
*.class
*.dll
*.exe
*.o
*.so
/panel
# 压缩包 #
# 压缩包
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
*.7z
*.dmg
@@ -34,16 +29,17 @@ _cgo_export.*
*.tar
*.zip
# 日志文件和数据库 #
# 日志文件和数据库及配置
*.log
*.sqlite
*.db
config.yml
# 临时文件 #
# 临时文件
tmp/
.tmp/
# 系统生成文件 #
# 系统生成文件
.DS_Store
.DS_Store?
.AppleDouble
@@ -58,7 +54,7 @@ Thumbs.db
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# IDE 和编辑器 #
# IDE 和编辑器
.idea/
/go_build_*
out/
@@ -67,8 +63,3 @@ out/
*.sublime*
__debug_bin
.project
# 前端工具链 #
.sass-cache/*
node_modules/
/public
-92
View File
@@ -1,92 +0,0 @@
image: golang:1.22-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 run . artisan migrate
- 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"' -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
- rm -rf public
- unzip frontend.zip
- mv dist public
artifacts:
name: "frontend"
paths:
- public
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
+23 -29
View File
@@ -1,25 +1,38 @@
version: 2
project_name: panel
builds:
- id: panel
binary: panel
- id: web
main: ./cmd/web
binary: web
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w --extldflags "-static"
- id: cli
main: ./cmd/cli
binary: cli
env:
- CGO_ENABLED=0
- GOPROXY=https://goproxy.cn,direct
goos:
- linux
goarch:
- amd64
- arm64
goamd64:
- v2
ldflags:
- -s -w --extldflags "-static"
upx:
- enabled: true
# Filter by build ID.
ids: [ panel ]
ids:
- web
- cli
# Compress argument.
# Valid options are from '1' (faster) to '9' (better), and 'best'.
compress: best
@@ -29,33 +42,14 @@ upx:
brute: false
archives:
- id: panel-archive
- id: panel
builds:
- panel
- web
- cli
format: zip
wrap_in_directory: false
strip_binary_directory: true
files:
- LICENSE
- docs/*
- public/*
- config.example.yml
- storage/*
- database/*
- lang/*
- scripts/*
- panel-example.conf
gitlab_urls:
api: https://git.haozi.net/api/v4/
download: https://git.haozi.net
# set to true if you use a self-signed certificate
skip_tls_verify: false
# set to true if you want to upload to the Package Registry rather than attachments
# Only works with GitLab 13.5+
#
# Since: v1.3
use_package_registry: true
# Set this if you set GITLAB_TOKEN to the value of CI_JOB_TOKEN.
#
# Since: v1.11
use_job_token: true
-1
View File
@@ -1 +0,0 @@
replace carbon.DateTime string
+2 -2
View File
@@ -1,6 +1,6 @@
## 行为准则
耗子 Linux 面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
耗子面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
- 参与者将容忍反对意见。
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
@@ -9,7 +9,7 @@
## Code of Conduct
The HaoZi Linux Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
The Rat Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
- Participants will be tolerant of opposing views.
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
+97 -44
View File
@@ -2,7 +2,11 @@
[简体中文] | [<a href="README_EN.md">English</a>]
</p>
<h1 align="center">耗子 Linux 面板</h1>
<h1 align="center" style="font-size: 40px">耗子面板</h1>
<p align="center">
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
<p align="center">
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
@@ -12,73 +16,93 @@
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
</p>
耗子 Linux 面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板
耗子面板是新一代全能服务器运维管理面板。简单轻量,高效运维
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.net/a/TheTNB)
QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
## 优势
1. **极低占用:** 在 Debian 12 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
1. **极低占用:** 在 Debian 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
4. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
5. **离线运行:** 面板运行可不依赖任何外部服务,甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1以上无事故。
7. **开源开放:** 面板开源,可以自由修改、审计面板源码,安全性有保障。
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
5. **离线运行:** 面板运行可不依赖任何外部服务,甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
7. **开源开放:** 面板开源,可以自由修改、审计面板源码,安全性有保障。
## UI 截图
![UI 截图](ui.png)
![UI 截图](.github/assets/ui.png)
## 运行环境
耗子 Linux 面板支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护
耗子面板支持 `amd64` | `arm64` 架构下的主流系统,下表中的系统均已测试 LNMP 环境安装
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `AlmaLinux` | `RockyLinux`,维护周期更长也更稳定
优先建议使用标注**推荐**的系统,无特殊情况不建议使用标注**不推荐**的系统
不在下表中的其他 RHEL 9.x 系统,可自行尝试安装,但不保证能够正常运行,且不提供技术支持(理论上不会有大问题)。
不在下表中的其他系统,可自行尝试安装,但不提供技术支持(接受相关 PR 提交)。
| 系统 | 版本 |
|------------|----|
| RHEL | 9 |
| AlmaLinux | 9 |
| RockyLinux | 9 |
| Debian | 12 |
| 系统 | 版本 | 备注 |
|---------------------|-----|-----|
| AlmaLinux | 9 | 推荐 |
| AlmaLinux | 8 | 不推荐 |
| RockyLinux | 9 | 支持 |
| RockyLinux | 8 | 不推荐 |
| CentOS Stream | 9 | 不推荐 |
| CentOS Stream | 8 | 不推荐 |
| Ubuntu | 24 | 推荐 |
| Ubuntu | 22 | 支持 |
| Debian | 12 | 推荐 |
| Debian | 11 | 支持 |
| OpenCloudOS | 9 | 支持 |
| TencentOS Server | 4 | 支持 |
| TencentOS Server | 3.1 | 不推荐 |
| Alibaba Cloud Linux | 3.2 | 不推荐 |
| Anolis | 8 | 不推荐 |
| openEuler | 22 | 不推荐 |
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的稳定性。
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
## 挂载硬盘
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
```shell
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
```
## 安装面板
> **Warning**
> 安装面板前,需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,不建议 0 基础的用户安装和使用耗子 Linux 面板。
> 安装面板前,需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
如果你决定继续,请`root`用户登录服务器,行以下命令安装面板:
`root`用户登录服务器,行以下命令安装面板:
```shell
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
```
## 卸载面板
优先建议备份数据重装系统,这样可以保证系统纯净。
如果无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
如果无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
```shell
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
```
卸载面板前请务必备份好所有数据,提前卸载面板全部插件。卸载后数据将**无法恢复**
卸载面板前请务必备份好所有数据,提前卸载面板全部应用。卸载后数据将**无法恢复**
## 日常维护
使用`panel`命令进行日常维护:
使用`panel-cli`命令进行日常维护:
```shell
panel
panel-cli
```
在 [Wiki](https://github.com/TheTNB/panel/wiki) 中查看更多使用方法和技巧。
在 [文档](https://tom.moe/docs?category=57) 中查看更多使用方法和技巧。
## 问题反馈
@@ -88,21 +112,44 @@ panel
## 赞助商
如果耗子 Linux 面板对有帮助,欢迎[赞助我们](https://afdian.net/a/TheTNB),感谢以下支持者/赞助商的支持:
如果耗子面板对有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
**同时接受云资源赞助,可通过QQ群咨询联系**
### 资金
<a href="https://www.weixiaoduo.com/">
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
</a>
### 服务器
- [微晓朵](https://www.weixiaoduo.com/)
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [盾云CDN](http://cdn.ddunyun.com/)
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
</a>
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
</a>
<a href="https://scdn.ddunyun.com/">
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
</a>
### 合作伙伴
<a href="https://1ms.run">
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
</a>
### 赞助商们
<p align="center">
<a target="_blank" href="https://afdian.net/a/TheTNB">
<a target="_blank" href="https://afdian.com/a/TheTNB">
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
</a>
</p>
@@ -111,20 +158,26 @@ panel
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
<a href="https://github.com/TheTNB/panel/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
</picture>
</a>
</p>
## Star 历史
<a href="https://star-history.com/#TheTNB/panel&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
</picture>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history?repo_id=572922963" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=dark" width="721" height="auto">
<img alt="Star History of TheTNB/panel" src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=light" width="721" height="auto">
</picture>
</a>
</p>
## 免责声明
严禁使用耗子 Linux 面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
严禁使用耗子面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
+93 -40
View File
@@ -2,7 +2,11 @@
[<a href="README.md">简体中文</a>] | [English]
</p>
<h1 align="center">HaoZi Linux Panel</h1>
<h1 align="center" style="font-size: 40px">Rat Panel</h1>
<p align="center">
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
<p align="center">
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
@@ -12,50 +16,70 @@
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
</p>
The HaoZi Linux Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
Rat Panel is a new generation of all-in-one server operation and maintenance management panel. Simple and lightweight, efficient operation and maintenance.
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
## Advantages
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian 12, the memory usage is less than 500 MB, far ahead of other panels using containerization.
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian, the memory usage is less than 500 MB, far ahead of other panels using containerization.
2. **Low destructiveness:** The design concept of the panel is to minimize the additional modifications to the system. Among similar panels, we have made the least modifications to the system.
3. **Follow the times:** All components of the panel are at the forefront of the times, updated quickly, powerful functions, and guaranteed security.
4. **Efficient operation and maintenance:** The panel UI interface is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
4. **Efficient operation and maintenance:** The panel is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
5. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for more than 1 year without any accidents.
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for 2 year without any accidents.
7. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
## UI Screenshots
![UI Screenshots](ui.png)
![UI Screenshots](.github/assets/ui.png)
## Operating Environment
HaoZi Linux Panel only supports the latest version of mainstream systems under the `amd64` | `arm64` architecture. It does not support `Ubuntu` because its releases are too frequent and difficult to maintain.
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture, and the systems in the table below have been tested for LNMP environment installation.
Recommended to use `Debian` for low-configuration machines, as its resource usage is lower than that of the `RHEL` system. For other machines, recommended to use `RockyLinux` | `AlmaLinux`, which has a longer maintenance cycle and is more stable.
It is recommended to use the systems marked **Recommended** first, and not recommended to use the systems marked **Not recommended** without special circumstances.
For other RHEL 9.x systems not in the table, you can try to install it yourself, but normal operation is not guaranteed, and technical support is not provided (theoretically there will be no major question).
For other systems not listed in the table below, you can try to install them by yourself, but there is no technical support will be provided (accept related PR submissions).
| OS | Version |
|------------|---------|
| RHEL | 9 |
| RockyLinux | 9 |
| AlmaLinux | 9 |
| Debian | 12 |
| OS | Version | Note |
|---------------------|---------|-----------------|
| AlmaLinux | 9 | Recommended |
| AlmaLinux | 8 | Not recommended |
| RockyLinux | 9 | Support |
| RockyLinux | 8 | Not recommended |
| CentOS Stream | 9 | Not recommended |
| CentOS Stream | 8 | Not recommended |
| Ubuntu | 24 | Recommended |
| Ubuntu | 22 | Support |
| Debian | 12 | Recommended |
| Debian | 11 | Support |
| OpenCloudOS | 9 | Support |
| TencentOS Server | 4 | Support |
| TencentOS Server | 3.1 | Not recommended |
| Alibaba Cloud Linux | 3.2 | Not recommended |
| Anolis | 8 | Not recommended |
| openEuler | 22 | Not recommended |
As system versions are constantly updated, we may also terminate support for some older systems to ensure the stability of the panel.
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
## Mount Disk
If your server has an unmounted data disk, you can run the following command as the `root` user to automatically mount it before installation. The panel does not support cross-directory migration after installation.
```shell
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
```
## Install Panel
> **Warning**
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. It is not recommended for users with zero basic knowledge to install and use HaoZi Linux Panel.
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
Login to the server as the `root` user and run the following command to install the panel:
```shell
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
```
## Uninstall Panel
@@ -65,20 +89,20 @@ Recommended to back up data and reinstall the system first, so that the system c
If you are unable to reinstall the system, log in to the server as the `root` user and execute the following command to uninstall the panel:
```shell
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
```
Before uninstalling the panel, please be sure to back up all data and uninstall all panel plugins in advance. The data will **not be recoverable** after uninstallation!
## Daily Maintenance
Use `panel` command for daily maintenance:
Use `panel-cli` command for daily maintenance:
```shell
panel
panel-cli
```
See more usage methods and tips in [Wiki](https://github.com/TheTNB/panel/wiki).
See more usage methods and tips in [Document](https://tom.moe/docs?category=57).
## Feedback
@@ -88,21 +112,44 @@ For issues with the panel itself, you can submit feedback on the `Issues` page o
## Sponsor
If the HaoZi Linux Panel is helpful to you, welcome to [sponsor us](https://opencollective.com/tnb), thanks to the following supporters/sponsors:
If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollective.com/tnb), thanks to the following supporters/sponsors:
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
### Financial
<a href="https://www.weixiaoduo.com/">
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
</a>
### Server
- [微晓朵](https://www.weixiaoduo.com/)
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [盾云CDN](http://cdn.ddunyun.com/)
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
</a>
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
</a>
<a href="https://scdn.ddunyun.com/">
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
</a>
### Partners
<a href="https://1ms.run">
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
</a>
### Sponsors
<p align="center">
<a target="_blank" href="https://afdian.net/a/TheTNB">
<a target="_blank" href="https://afdian.com/a/TheTNB">
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
</a>
</p>
@@ -111,20 +158,26 @@ If the HaoZi Linux Panel is helpful to you, welcome to [sponsor us](https://open
This project owes its existence to all those who have contributed. To contribute, please check the contributed code section first.
<a href="https://github.com/TheTNB/panel/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
</picture>
</a>
</p>
## Star History
<a href="https://star-history.com/#TheTNB/panel&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
</picture>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history?repo_id=572922963" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=dark" width="721" height="auto">
<img alt="Star History of TheTNB/panel" src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=light" width="721" height="auto">
</picture>
</a>
</p>
## Disclaimer
It is strictly prohibited to use the HaoZi Linux Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
It is strictly prohibited to use the Rat Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
+11 -11
View File
@@ -1,25 +1,25 @@
## 安全说明
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子 Linux 面板,至今无一例安全事故。
**我们不对面板的安全性做任何保证**,但安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子面板,至今无任何安全事故。
耗子 Linux 面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
如果您在面板中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
- 邮箱:i@haozi.net
- 邮箱:[admin@haozi.net](mailto:admin@haozi.net)
- QQ826896000
致某些 CVE 爱好者:通过面板 `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助
提前感谢您的支持与帮助
致某些安全初学者:通过面板 `session` / `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助。
## Security Policy
Security is one of our biggest concerns. We have widely used HaoZi Linux Panel in production environments for many different applications, and there has been no safety incident so far.
HaoZi Linux Panel adopts various solutions in the industry to ensure the security of the panel as much as possible, but we cannot guarantee the absolute security of the panel, **therefore we do not make any guarantees on the security of the panel**.
**We do not make any guarantees about the security of the panel**, but security is one of our top concerns. We have widely used the Rat panel in the production environment of multiple different applications, and there has been no security incidents to date.
If you find any security issues while using the panel, please do not submit an Issue. You can contact us directly through the following methods:
- Email: i@haozi.net
- Email: [admin@haozi.net](mailto:admin@haozi.net)
- Telegram: @devhaozi
To some CVE enthusiasts: Any operation performed through the panel `access_token` (including but not limited to: obtaining root permissions, reading/writing system sensitive files, executing arbitrary shell commands, etc.) is not considered a security issue, please Dont waste each others time with these types of reports. These low-level reports wont do anything to help your resume.
Thank you in advance for your support and help!
To some security beginners: Any operation performed through the panel `session` / `access_token` (including but not limited to: obtaining root permissions, reading/writing sensitive system files, executing arbitrary shell commands, etc.) is not considered a security issue. Please do not waste each other's time by submitting such reports. Such low-level reports are of no help to your resume.
-82
View File
@@ -1,82 +0,0 @@
package commands
import (
"context"
"crypto/x509"
"encoding/pem"
"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/app/models"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/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
}
block, _ := pem.Decode([]byte(cert.Cert))
if block != nil {
data, err := x509.ParseCertificate(block.Bytes)
if err != nil {
continue
}
// 结束时间大于 7 天的证书不续签
endTime := carbon.FromStdTime(data.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
}
-99
View File
@@ -1,99 +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/app/models"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/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
}
// 将等待中的任务分发
// TODO 有bug,需要设计一个锁机制防止重复分发
//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
for _, cpu := range info.Cpus {
cpu.VendorID = ""
cpu.Family = ""
cpu.Model = ""
cpu.PhysicalID = ""
cpu.ModelName = ""
cpu.Flags = 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
}
-729
View File
@@ -1,729 +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"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/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: "耗子 Linux 面板"}, {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(tools.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的不同
_ = tools.ServiceStop("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 := tools.RandomString(16)
hash, err := facades.Hash().Make(password)
if err != nil {
color.Red().Printfln(translate.Get("commands.panel.getInfo.passwordGenerationFail"))
return nil
}
user.Username = tools.RandomString(8)
user.Password = hash
if user.Email == "" {
user.Email = tools.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 := tools.Exec(`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("http.entrance"))
color.Green().Printfln(translate.Get("commands.panel.getInfo.address") + ": " + protocol + "://" + ip + ":" + port + facades.Config().GetString("http.entrance"))
case "getPort":
port, err := tools.Exec(`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("http.entrance"))
case "deleteEntrance":
oldEntrance, err := tools.Exec(`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 = tools.Exec("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 !tools.Exists(path) {
if err := tools.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 := tools.Exec(`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 = tools.Exec(`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 = tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
return nil
}
if err := tools.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 := tools.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 := tools.Exec(`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 = tools.Exec(`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 = tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
return nil
}
if err := tools.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 := tools.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 := tools.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 !tools.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 := tools.Exec(`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 := tools.Exec(`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 := tools.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(types.Website{
Name: name,
Status: true,
Domains: domains,
Ports: uintPorts,
Path: path,
Php: php,
Ssl: false,
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(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
}
-72
View File
@@ -1,72 +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/pkg/tools"
"github.com/TheTNB/panel/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 := tools.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 := tools.Exec(`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
}
types.Status = types.StatusNormal
return nil
}
-29
View File
@@ -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/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{},
}
}
-77
View File
@@ -1,77 +0,0 @@
package controllers
import (
"os"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
)
type AssetController struct {
setting internal.Setting
}
func NewAssetController() *AssetController {
return &AssetController{
setting: services.NewSettingImpl(),
}
}
func (r *AssetController) Index(ctx http.Context) http.Response {
entrance := facades.Config().GetString("http.entrance")
if entrance == "/" {
entrance = ""
}
// 自动纠正 URL 格式
if ctx.Request().Path() == entrance && ctx.Request().Path() != "/" {
return ctx.Response().Redirect(http.StatusMovedPermanently, ctx.Request().Path()+"/")
}
// 拒绝访问非入口文件
if !strings.HasPrefix(ctx.Request().Path(), entrance) {
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
path := strings.TrimPrefix(ctx.Request().Path(), entrance)
// 设置默认首页
if path == "/" || path == "" {
path = "/index.html"
}
if !tools.Exists("public" + path) {
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
file, err := os.Open("public" + path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
stat, err := file.Stat()
if err != nil {
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if stat.IsDir() {
return Error(ctx, http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
return ctx.Response().Header("Cache-Control", "no-cache").File("public" + path)
}
func (r *AssetController) Favicon(ctx http.Context) http.Response {
return ctx.Response().File("public/favicon.png")
}
func (r *AssetController) Robots(ctx http.Context) http.Response {
return ctx.Response().File("public/robots.txt")
}
func (r *AssetController) NotFound(ctx http.Context) http.Response {
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
-701
View File
@@ -1,701 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
requests "github.com/TheTNB/panel/app/http/requests/cert"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/acme"
)
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 证书管理
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/caProviders [get]
func (r *CertController) CAProviders(ctx http.Context) http.Response {
return 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 证书管理
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/dnsProviders [get]
func (r *CertController) DNSProviders(ctx http.Context) http.Response {
return Success(ctx, []map[string]any{
{
"name": "DNSPod",
"dns": acme.DnsPod,
},
{
"name": "阿里云",
"dns": acme.AliYun,
},
{
"name": "CloudFlare",
"dns": acme.CloudFlare,
},
})
}
// Algorithms
//
// @Summary 获取算法列表
// @Description 获取面板证书管理支持的算法列表
// @Tags 证书管理
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/algorithms [get]
func (r *CertController) Algorithms(ctx http.Context) http.Response {
return 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 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"total": total,
"items": users,
})
}
// UserStore
//
// @Summary 添加 ACME 用户
// @Description 添加 ACME 用户到面板证书管理
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// UserUpdate
//
// @Summary 更新 ACME 用户
// @Description 更新面板证书管理的 ACME 用户
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// UserShow
//
// @Summary 获取 ACME 用户
// @Description 获取面板证书管理的 ACME 用户
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, user)
}
// UserDestroy
//
// @Summary 删除 ACME 用户
// @Description 删除面板证书管理的 ACME 用户
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// DNSList
//
// @Summary 获取 DNS 接口列表
// @Description 获取面板证书管理的 DNS 接口列表
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"total": total,
"items": dns,
})
}
// DNSStore
//
// @Summary 添加 DNS 接口
// @Description 添加 DNS 接口到面板证书管理
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// DNSShow
//
// @Summary 获取 DNS 接口
// @Description 获取面板证书管理的 DNS 接口
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, dns)
}
// DNSUpdate
//
// @Summary 更新 DNS 接口
// @Description 更新面板证书管理的 DNS 接口
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// DNSDestroy
//
// @Summary 删除 DNS 接口
// @Description 删除面板证书管理的 DNS 接口
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// CertList
//
// @Summary 获取证书列表
// @Description 获取面板证书管理的证书列表
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"total": total,
"items": certs,
})
}
// CertStore
//
// @Summary 添加证书
// @Description 添加证书到面板证书管理
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// CertUpdate
//
// @Summary 更新证书
// @Description 更新面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// CertShow
//
// @Summary 获取证书
// @Description 获取面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, cert)
}
// CertDestroy
//
// @Summary 删除证书
// @Description 删除面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// Obtain
//
// @Summary 签发证书
// @Description 签发面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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 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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// Renew
//
// @Summary 续签证书
// @Description 续签面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// ManualDNS
//
// @Summary 获取手动 DNS 记录
// @Description 获取签发证书所需的 DNS 记录
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, resolves)
}
// Deploy
//
// @Summary 部署证书
// @Description 部署面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
File diff suppressed because it is too large Load Diff
-52
View File
@@ -1,52 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// 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: facades.Lang(ctx).Get("messages.mistake") + ": " + message,
})
}
// ErrorSystem 响应系统错误
func ErrorSystem(ctx http.Context) http.Response {
return ctx.Response().Json(http.StatusInternalServerError, &ErrorResponse{
Message: facades.Lang(ctx).Get("errors.internal"),
})
}
// Sanitize 消毒请求参数
func Sanitize(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
}
-310
View File
@@ -1,310 +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/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
)
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 ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"total": total,
"items": crons,
})
}
// Add 添加计划任务
func (r *CronController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(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",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
// 单独验证时间格式
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 Error(ctx, http.StatusUnprocessableEntity, "时间格式错误")
}
shell := 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)
shell = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子 Linux 面板 - 数据备份脚本
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)
shell = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子 Linux 面板 - 日志切割脚本
name=` + website + `
save=` + cast.ToString(save) + `
# 执行切割
panel cutoff ${name} ${save} 2>&1
`
}
shellDir := "/www/server/cron/"
shellLogDir := "/www/server/cron/logs/"
if !tools.Exists(shellDir) {
return Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
}
if !tools.Exists(shellLogDir) {
return Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
}
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
if err = tools.Write(shellDir+shellFile+".sh", shell, 0700); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("dos2unix " + shellDir + shellFile + ".sh"); err != nil {
return 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"
err = facades.Orm().Query().Create(&cron)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
"error": err.Error(),
}).Info("保存计划任务失败")
return ErrorSystem(ctx)
}
if err := r.cron.AddToSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return 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 Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
shell, err := tools.Read(cron.Shell)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, shell)
}
// Update 更新计划任务
func (r *CronController) Update(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
// 单独验证时间格式
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 Error(ctx, http.StatusUnprocessableEntity, "时间格式错误")
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
if !cron.Status {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务已禁用")
}
cron.Time = ctx.Request().Input("time")
cron.Name = ctx.Request().Input("name")
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
"error": err.Error(),
}).Info("更新计划任务失败")
return ErrorSystem(ctx)
}
if err = tools.Write(cron.Shell, ctx.Request().Input("script"), 0644); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("dos2unix " + cron.Shell); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if err := r.cron.DeleteFromSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if cron.Status {
if err := r.cron.AddToSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
}
return 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 Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
if err := r.cron.DeleteFromSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.Remove(cron.Shell); err != nil {
return 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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// Status 更新计划任务状态
func (r *CronController) Status(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"status": "bool",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
cron.Status = ctx.Request().InputBool("status")
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
"error": err.Error(),
}).Info("更新计划任务状态失败")
return ErrorSystem(ctx)
}
if err := r.cron.DeleteFromSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if cron.Status {
if err := r.cron.AddToSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
}
return 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 Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
if !tools.Exists(cron.Log) {
return Error(ctx, http.StatusUnprocessableEntity, "日志文件不存在")
}
log, err := tools.Exec("tail -n 1000 " + cron.Log)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, log)
}
-528
View File
@@ -1,528 +0,0 @@
package controllers
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
requests "github.com/TheTNB/panel/app/http/requests/file"
"github.com/TheTNB/panel/pkg/tools"
)
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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
isDir := ctx.Request().InputBool("dir")
if !isDir {
if out, err := tools.Exec("touch " + request.Path); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if err := tools.Mkdir(request.Path, 0755); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
}
r.setPermission(request.Path, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfo, err := tools.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if fileInfo.IsDir() {
return Error(ctx, http.StatusInternalServerError, "目标路径不是文件")
}
if fileInfo.Size() > 10*1024*1024 {
return Error(ctx, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑")
}
content, err := tools.Read(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, content)
}
// Save
//
// @Summary 保存文件内容
// @Description 保存给定路径的文件内容
// @Tags 文件管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Save true "request"
// @Param content body string true "content"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/save [post]
func (r *FileController) Save(ctx http.Context) http.Response {
var request requests.Save
sanitize := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfo, err := tools.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = tools.Write(request.Path, request.Content, fileInfo.Mode()); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := tools.Remove(request.Path); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
src, err := request.File.Open()
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
defer src.Close()
if tools.Exists(request.Path) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
}
data, err := io.ReadAll(src)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = tools.Write(request.Path, string(data), 0755); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
if tools.Exists(request.Target) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
}
if err := tools.Mv(request.Source, request.Target); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Target, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
if tools.Exists(request.Target) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
}
if err := tools.Cp(request.Source, request.Target); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Source, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
info, err := tools.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if info.IsDir() {
return 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 := Sanitize(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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfo, err := tools.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, http.Json{
"name": fileInfo.Name(),
"size": tools.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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := tools.Chmod(request.Path, os.FileMode(request.Mode)); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.Chown(request.Path, request.Owner, request.Group); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := tools.Archive(request.Paths, request.File); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.File, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := tools.UnArchive(request.File, request.Path); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
paths := make(map[string]os.FileInfo)
err := filepath.Walk(request.Path, func(path string, info os.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 Error(ctx, http.StatusInternalServerError, err.Error())
}
return 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 := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
var paginate commonrequests.Paginate
paginateSanitize := Sanitize(ctx, &paginate)
if paginateSanitize != nil {
return paginateSanitize
}
fileInfoList, err := os.ReadDir(request.Path)
if err != nil {
return 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": tools.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
"owner": tools.GetUser(stat.Uid),
"group": tools.GetGroup(stat.Gid),
"uid": stat.Uid,
"gid": stat.Gid,
"hidden": tools.IsHidden(info.Name()),
"symlink": tools.IsSymlink(info.Mode()),
"link": tools.GetSymlink(filepath.Join(request.Path, info.Name())),
"dir": info.IsDir(),
"modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(),
})
}
start := paginate.Limit * (paginate.Page - 1)
end := paginate.Limit * paginate.Page
if start > len(paths) {
start = len(paths)
}
if end > len(paths) {
end = len(paths)
}
paged := paths[start:end]
if paged == nil {
paged = []any{}
}
return Success(ctx, http.Json{
"total": len(paths),
"items": paged,
})
}
// setPermission
func (r *FileController) setPermission(path string, mode uint, owner, group string) {
_ = tools.Chmod(path, os.FileMode(mode))
_ = tools.Chown(path, owner, group)
}
-341
View File
@@ -1,341 +0,0 @@
package controllers
import (
"database/sql"
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/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 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 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 Success(ctx, pluginsJson)
}
// NowMonitor 获取当前监控信息
func (r *InfoController) NowMonitor(ctx http.Context) http.Response {
return Success(ctx, tools.GetMonitoringInfo())
}
// SystemInfo 获取系统信息
func (r *InfoController) SystemInfo(ctx http.Context) http.Response {
monitorInfo := tools.GetMonitoringInfo()
return 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 := tools.ServiceStatus("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 := tools.ServiceStatus("postgresql")
if status && err == nil {
raw, err := tools.Exec(`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 := tools.Exec("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 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 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 Success(ctx, http.Json{
"php": phpData,
"db": dbData,
})
}
// CheckUpdate 检查面板更新
func (r *InfoController) CheckUpdate(ctx http.Context) http.Response {
version := facades.Config().GetString("panel.version")
remote, err := tools.GetLatestPanelVersion()
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
if tools.VersionCompare(version, remote.Version, ">=") {
return Success(ctx, http.Json{
"update": false,
})
}
return Success(ctx, http.Json{
"update": true,
})
}
// UpdateInfo 获取更新信息
func (r *InfoController) UpdateInfo(ctx http.Context) http.Response {
version := facades.Config().GetString("panel.version")
current, err := tools.GetLatestPanelVersion()
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
if tools.VersionCompare(version, current.Version, ">=") {
return Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本")
}
versions, err := tools.GenerateVersions(version, current.Version)
if err != nil {
return 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 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 Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
}
if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
types.Status = types.StatusFailed
return 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 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 Error(ctx, http.StatusInternalServerError, err.Error())
}
types.Status = types.StatusNormal
tools.RestartPanel()
return 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 Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
}
tools.RestartPanel()
return Success(ctx, nil)
}
-185
View File
@@ -1,185 +0,0 @@
package controllers
import (
"fmt"
"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/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
)
type MonitorController struct {
setting internal.Setting
}
func NewMonitorController() *MonitorController {
return &MonitorController{
setting: services.NewSettingImpl(),
}
}
// Switch 监控开关
func (r *MonitorController) Switch(ctx http.Context) http.Response {
value := ctx.Request().InputBool("monitor")
err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value))
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
"monitor": value,
"error": err.Error(),
}).Info("更新监控开关失败")
return ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// SaveDays 保存监控天数
func (r *MonitorController) SaveDays(ctx http.Context) http.Response {
days := ctx.Request().Input("days")
err := r.setting.Set(models.SettingKeyMonitorDays, days)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
"days": days,
"error": err.Error(),
}).Info("更新监控开关失败")
return ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// SwitchAndDays 监控开关和监控天数
func (r *MonitorController) SwitchAndDays(ctx http.Context) http.Response {
monitor := r.setting.Get(models.SettingKeyMonitor)
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
return Success(ctx, http.Json{
"switch": cast.ToBool(monitor),
"days": cast.ToInt(monitorDays),
})
}
// Clear 清空监控数据
func (r *MonitorController) Clear(ctx http.Context) http.Response {
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
"error": err.Error(),
}).Info("清空监控数据失败")
return ErrorSystem(ctx)
}
return 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)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
"start": startTime.ToDateTimeString(),
"end": endTime.ToDateTimeString(),
"error": err.Error(),
}).Info("获取监控数据失败")
return ErrorSystem(ctx)
}
if len(monitors) == 0 {
return Error(ctx, http.StatusNotFound, "监控数据为空")
}
type load struct {
Load1 []float64 `json:"load1"`
Load5 []float64 `json:"load5"`
Load15 []float64 `json:"load15"`
}
type cpu struct {
Percent []string `json:"percent"`
}
type mem struct {
Total string `json:"total"`
Available []string `json:"available"`
Used []string `json:"used"`
}
type swap struct {
Total string `json:"total"`
Used []string `json:"used"`
Free []string `json:"free"`
}
type network struct {
Sent []string `json:"sent"`
Recv []string `json:"recv"`
Tx []string `json:"tx"`
Rx []string `json:"rx"`
}
type monitorData struct {
Times []string `json:"times"`
Load load `json:"load"`
Cpu cpu `json:"cpu"`
Mem mem `json:"mem"`
Swap swap `json:"swap"`
Net network `json:"net"`
}
var data monitorData
var bytesSent uint64
var bytesRecv uint64
var bytesSent2 uint64
var bytesRecv2 uint64
for _, net := range monitors[0].Info.Net {
if net.Name == "lo" {
continue
}
bytesSent += net.BytesSent
bytesRecv += net.BytesRecv
}
for i, monitor := range monitors {
// 跳过第一条数据,因为第一条数据的流量为 0
if i == 0 {
// MB
data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024)
data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024)
continue
}
for _, net := range monitor.Info.Net {
if net.Name == "lo" {
continue
}
bytesSent2 += net.BytesSent
bytesRecv2 += net.BytesRecv
}
data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString())
data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1)
data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5)
data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15)
data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024)))
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024)))
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024))
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024))
bytesSent = bytesSent2
bytesRecv = bytesRecv2
bytesSent2 = 0
bytesRecv2 = 0
}
return Success(ctx, data)
}
-152
View File
@@ -1,152 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
)
type PluginController struct {
plugin internal.Plugin
task internal.Task
}
func NewPluginController() *PluginController {
return &PluginController{
plugin: services.NewPluginImpl(),
task: services.NewTaskImpl(),
}
}
// List 列出所有插件
func (r *PluginController) List(ctx http.Context) http.Response {
plugins := r.plugin.All()
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
return 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,
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(pluginArr) {
return Success(ctx, http.Json{
"total": 0,
"items": []plugin{},
})
}
if endIndex > len(pluginArr) {
endIndex = len(pluginArr)
}
pagedPlugins := pluginArr[startIndex:endIndex]
return Success(ctx, http.Json{
"total": len(pluginArr),
"items": pagedPlugins,
})
}
// Install 安装插件
func (r *PluginController) Install(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if err := r.plugin.Install(slug); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, "任务已提交")
}
// Uninstall 卸载插件
func (r *PluginController) Uninstall(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if err := r.plugin.Uninstall(slug); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, "任务已提交")
}
// Update 更新插件
func (r *PluginController) Update(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if err := r.plugin.Update(slug); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, "任务已提交")
}
// UpdateShow 更新插件首页显示状态
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 ErrorSystem(ctx)
}
if plugin.ID == 0 {
return 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 ErrorSystem(ctx)
}
return Success(ctx, "操作成功")
}
@@ -1,409 +0,0 @@
package plugins
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
type Fail2banController struct {
website internal.Website
}
func NewFail2banController() *Fail2banController {
return &Fail2banController{
website: services.NewWebsiteImpl(),
}
}
// Status 获取运行状态
func (r *Fail2banController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("fail2ban")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Fail2banController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("fail2ban"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Fail2banController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("fail2ban"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启服务失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Fail2banController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("fail2ban"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动服务失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Fail2banController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("fail2ban"); err != nil {
return nil
}
status, err := tools.ServiceStatus("fail2ban")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}
return controllers.Success(ctx, !status)
}
// List 所有 Fail2ban 规则
func (r *Fail2banController) List(ctx http.Context) http.Response {
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
raw, err := tools.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
if len(jailList) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "Fail2ban 规则为空")
}
var jails []types.Fail2banJail
for i, jail := range jailList {
if i == 0 {
continue
}
jailName := jail[1]
jailRaw := tools.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]),
})
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(jails) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.Fail2banJail{},
})
}
if endIndex > len(jails) {
endIndex = len(jails)
}
pagedJails := jails[startIndex:endIndex]
if pagedJails == nil {
pagedJails = []types.Fail2banJail{}
}
return controllers.Success(ctx, http.Json{
"total": len(jails),
"items": pagedJails,
})
}
// Add 添加 Fail2ban 规则
func (r *Fail2banController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(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",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
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 := tools.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.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 controllers.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 controllers.Error(ctx, http.StatusUnprocessableEntity, "网站不存在")
}
config, err := r.website.GetConfig(website.ID)
if err != nil {
return controllers.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 = tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
var filter string
if jailWebsiteMode == "cc" {
filter = `
[Definition]
failregex = ^<HOST>\s-.*HTTP/.*$
ignoreregex =
`
} else {
filter = `
[Definition]
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
ignoreregex =
`
}
if err = tools.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
case "service":
var logPath string
var filter string
var port string
var err error
switch jailName {
case "ssh":
if tools.IsDebian() {
logPath = "/var/log/auth.log"
} else {
logPath = "/var/log/secure"
}
filter = "sshd"
port, err = tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
case "mysql":
logPath = "/www/server/mysql/mysql-error.log"
filter = "mysqld-auth"
port, err = tools.Exec("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 = tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
default:
return controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
}
if len(port) == 0 || err != nil {
return controllers.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 := tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
}
if _, err := tools.Exec("fail2ban-client reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
return controllers.Success(ctx, nil)
}
// Delete 删除规则
func (r *Fail2banController) Delete(ctx http.Context) http.Response {
jailName := ctx.Request().Input("name")
raw, err := tools.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if !strings.Contains(raw, "["+jailName+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
}
rule := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1)
raw = strings.TrimSpace(raw)
if err := tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
if _, err := tools.Exec("fail2ban-client reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
return controllers.Success(ctx, nil)
}
// BanList 获取封禁列表
func (r *Fail2banController) BanList(ctx http.Context) http.Response {
name := ctx.Request().Input("name")
if len(name) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
}
currentlyBan, err := tools.Exec(`fail2ban-client status ` + name + ` | grep "Currently banned" | awk '{print $4}'`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
}
totalBan, err := tools.Exec(`fail2ban-client status ` + name + ` | grep "Total banned" | awk '{print $4}'`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
}
bannedIp, err := tools.Exec(`fail2ban-client status ` + name + ` | grep "Banned IP list" | awk -F ":" '{print $2}'`)
if err != nil {
return controllers.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 controllers.Success(ctx, http.Json{
"currently_ban": currentlyBan,
"total_ban": totalBan,
"baned_list": list,
})
}
// Unban 解封
func (r *Fail2banController) Unban(ctx http.Context) http.Response {
name := ctx.Request().Input("name")
ip := ctx.Request().Input("ip")
if len(name) == 0 || len(ip) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
}
if _, err := tools.Exec("fail2ban-client set " + name + " unbanip " + ip); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "解封失败")
}
return controllers.Success(ctx, nil)
}
// SetWhiteList 设置白名单
func (r *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
ip := ctx.Request().Input("ip")
if len(ip) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
}
raw, err := tools.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.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 controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
}
if err := tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
if _, err := tools.Exec("fail2ban-client reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
return controllers.Success(ctx, nil)
}
// GetWhiteList 获取白名单
func (r *Fail2banController) GetWhiteList(ctx http.Context) http.Response {
raw, err := tools.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
if reg.MatchString(raw) {
ignoreIp := reg.FindStringSubmatch(raw)[1]
return controllers.Success(ctx, ignoreIp)
} else {
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
}
}
@@ -1,251 +0,0 @@
package plugins
import (
"fmt"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/frp"
"github.com/TheTNB/panel/pkg/tools"
)
type FrpController struct {
}
func NewFrpController() *FrpController {
return &FrpController{}
}
// Status
//
// @Summary 服务状态
// @Description 获取 Frp 服务状态
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/status [get]
func (r *FrpController) Status(ctx http.Context) http.Response {
frps, err := tools.ServiceStatus("frps")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 frps 服务运行状态失败")
}
frpc, err := tools.ServiceStatus("frpc")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 frpc 服务运行状态失败")
}
return controllers.Success(ctx, http.Json{
"frps": frps,
"frpc": frpc,
})
}
// IsEnabled
//
// @Summary 是否启用服务
// @Description 获取是否启用 Frp 服务
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/isEnabled [get]
func (r *FrpController) IsEnabled(ctx http.Context) http.Response {
frps, err := tools.ServiceIsEnabled("frps")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 frps 服务启用状态失败")
}
frpc, err := tools.ServiceIsEnabled("frpc")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 frpc 服务启用状态失败")
}
return controllers.Success(ctx, http.Json{
"frps": frps,
"frpc": frpc,
})
}
// Enable
//
// @Summary 启用服务
// @Description 启用 Frp 服务
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Param data body requests.Service true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/enable [post]
func (r *FrpController) Enable(ctx http.Context) http.Response {
var serviceRequest requests.Service
sanitize := controllers.Sanitize(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
if err := tools.ServiceEnable(serviceRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", serviceRequest.Service))
}
return controllers.Success(ctx, nil)
}
// Disable
//
// @Summary 禁用服务
// @Description 禁用 Frp 服务
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Param data body requests.Service true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/disable [post]
func (r *FrpController) Disable(ctx http.Context) http.Response {
var serviceRequest requests.Service
sanitize := controllers.Sanitize(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
if err := tools.ServiceDisable(serviceRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", serviceRequest.Service))
}
return controllers.Success(ctx, nil)
}
// Restart
//
// @Summary 重启服务
// @Description 重启 Frp 服务
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @param data body requests.Service true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/restart [post]
func (r *FrpController) Restart(ctx http.Context) http.Response {
var serviceRequest requests.Service
sanitize := controllers.Sanitize(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
if err := tools.ServiceRestart(serviceRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", serviceRequest.Service))
}
return controllers.Success(ctx, nil)
}
// Start
//
// @Summary 启动服务
// @Description 启动 Frp 服务
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Param data body requests.Service true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/start [post]
func (r *FrpController) Start(ctx http.Context) http.Response {
var serviceRequest requests.Service
sanitize := controllers.Sanitize(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
if err := tools.ServiceStart(serviceRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", serviceRequest.Service))
}
status, err := tools.ServiceStatus(serviceRequest.Service)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", serviceRequest.Service))
}
return controllers.Success(ctx, status)
}
// Stop
//
// @Summary 停止服务
// @Description 停止 Frp 服务
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Param data body requests.Service true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/stop [post]
func (r *FrpController) Stop(ctx http.Context) http.Response {
var serviceRequest requests.Service
sanitize := controllers.Sanitize(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
if err := tools.ServiceStop(serviceRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", serviceRequest.Service))
}
status, err := tools.ServiceStatus(serviceRequest.Service)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", serviceRequest.Service))
}
return controllers.Success(ctx, !status)
}
// 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 := controllers.Sanitize(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
config, err := tools.Read(fmt.Sprintf("/www/server/frp/%s.toml", serviceRequest.Service))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := tools.Write(fmt.Sprintf("/www/server/frp/%s.toml", updateRequest.Service), updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceRestart(updateRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,193 +0,0 @@
package plugins
import (
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/gitea"
"github.com/TheTNB/panel/pkg/tools"
)
type GiteaController struct {
}
func NewGiteaController() *GiteaController {
return &GiteaController{}
}
// Status
//
// @Summary 服务状态
// @Description 获取 Gitea 服务状态
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/status [get]
func (r *GiteaController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("gitea")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Gitea 服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// IsEnabled
//
// @Summary 是否启用服务
// @Description 获取是否启用 Gitea 服务
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/isEnabled [get]
func (r *GiteaController) IsEnabled(ctx http.Context) http.Response {
enabled, err := tools.ServiceIsEnabled("gitea")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Gitea 服务启用状态失败")
}
return controllers.Success(ctx, enabled)
}
// Enable
//
// @Summary 启用服务
// @Description 启用 Gitea 服务
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/enable [post]
func (r *GiteaController) Enable(ctx http.Context) http.Response {
if err := tools.ServiceEnable("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启用 Gitea 服务失败")
}
return controllers.Success(ctx, nil)
}
// Disable
//
// @Summary 禁用服务
// @Description 禁用 Gitea 服务
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/disable [post]
func (r *GiteaController) Disable(ctx http.Context) http.Response {
if err := tools.ServiceDisable("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "禁用 Gitea 服务失败")
}
return controllers.Success(ctx, nil)
}
// Restart
//
// @Summary 重启服务
// @Description 重启 Gitea 服务
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/restart [post]
func (r *GiteaController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启 Gitea 服务失败")
}
return controllers.Success(ctx, nil)
}
// Start
//
// @Summary 启动服务
// @Description 启动 Gitea 服务
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/start [post]
func (r *GiteaController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动 Gitea 服务失败")
}
status, err := tools.ServiceStatus("gitea")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Gitea 服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// Stop
//
// @Summary 停止服务
// @Description 停止 Gitea 服务
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/stop [post]
func (r *GiteaController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止 Gitea 服务失败")
}
status, err := tools.ServiceStatus("gitea")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Gitea 服务运行状态失败")
}
return controllers.Success(ctx, !status)
}
// 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 := tools.Read("/www/server/gitea/app.ini")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := tools.Write("/www/server/gitea/app.ini", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceRestart("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,668 +0,0 @@
package plugins
import (
"database/sql"
"fmt"
"regexp"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
type MySQLController struct {
setting internal.Setting
backup internal.Backup
}
func NewMySQLController() *MySQLController {
return &MySQLController{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *MySQLController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *MySQLController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载MySQL配置失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *MySQLController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *MySQLController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *MySQLController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *MySQLController) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/mysql/conf/my.cnf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *MySQLController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *MySQLController) Load(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
raw, err := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if err != nil {
return controllers.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"] = tools.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 controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (r *MySQLController) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (r *MySQLController) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/mysql/mysql-error.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SlowLog 获取慢查询日志
func (r *MySQLController) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (r *MySQLController) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/mysql/mysql-slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.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 controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
return controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
rootPassword := ctx.Request().Input("password")
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
}
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
err := r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\""); err != nil {
return nil
}
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return nil
}
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
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 {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
if err := rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databases) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databases) {
endIndex = len(databases)
}
pagedDatabases := databases[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databases),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DeleteDatabase 删除数据库
func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *MySQLController) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.MysqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []types.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *MySQLController) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *MySQLController) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.MysqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *MySQLController) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err := rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userGrants) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userGrants) {
endIndex = len(userGrants)
}
pagedUserGrants := userGrants[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userGrants),
"items": pagedUserGrants,
})
}
// AddUser 添加用户
func (r *MySQLController) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DeleteUser 删除用户
func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SetUserPassword 设置用户密码
func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SetUserPrivileges 设置用户权限
func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -1,193 +0,0 @@
package plugins
import (
"regexp"
"time"
"github.com/go-resty/resty/v2"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/tools"
)
type OpenRestyController struct {
// Dependent services
}
func NewOpenrestyController() *OpenRestyController {
return &OpenRestyController{
// Inject services
}
}
// Status 获取运行状态
func (r *OpenRestyController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("openresty")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *OpenRestyController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动OpenResty
func (r *OpenRestyController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止OpenResty
func (r *OpenRestyController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启OpenResty
func (r *OpenRestyController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *OpenRestyController) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/openresty/conf/nginx.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
}
if err := tools.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
}
return r.Reload(ctx)
}
// ErrorLog 获取错误日志
func (r *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
return controllers.Success(ctx, "")
}
out, err := tools.Exec("tail -n 100 /www/wwwlogs/openresty_error.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, out)
}
// ClearErrorLog 清空错误日志
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/wwwlogs/openresty_error.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// Load 获取负载
func (r *OpenRestyController) 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 controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
}
raw := resp.String()
type nginxStatus struct {
Name string `json:"name"`
Value string `json:"value"`
}
var data []nginxStatus
workers, err := tools.Exec("ps aux | grep nginx | grep 'worker process' | wc -l")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
}
data = append(data, nginxStatus{
Name: "工作进程",
Value: workers,
})
out, err := tools.Exec("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
}
mem := tools.FormatBytes(cast.ToFloat64(out))
data = append(data, nginxStatus{
Name: "内存占用",
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data = append(data, nginxStatus{
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, nginxStatus{
Name: "总连接次数",
Value: match[1],
})
data = append(data, nginxStatus{
Name: "总握手次数",
Value: match[2],
})
data = append(data, nginxStatus{
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, nginxStatus{
Name: "请求数",
Value: match[1],
})
data = append(data, nginxStatus{
Name: "响应数",
Value: match[2],
})
data = append(data, nginxStatus{
Name: "驻留进程",
Value: match[3],
})
}
return controllers.Success(ctx, data)
}
@@ -1,167 +0,0 @@
package plugins
import (
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/goravel/framework/contracts/http"
)
type PHPController struct {
service internal.PHP
}
func NewPHPController(version uint) *PHPController {
return &PHPController{
service: services.NewPHPImpl(version),
}
}
func (r *PHPController) Status(ctx http.Context) http.Response {
status, err := r.service.Status()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, status)
}
func (r *PHPController) Reload(ctx http.Context) http.Response {
if err := r.service.Reload(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Start(ctx http.Context) http.Response {
if err := r.service.Start(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Stop(ctx http.Context) http.Response {
if err := r.service.Stop(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Restart(ctx http.Context) http.Response {
if err := r.service.Restart(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) GetConfig(ctx http.Context) http.Response {
config, err := r.service.GetConfig()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PHPController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := r.service.SaveConfig(config); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response {
config, err := r.service.GetFPMConfig()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := r.service.SaveFPMConfig(config); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Load(ctx http.Context) http.Response {
load, err := r.service.Load()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, load)
}
func (r *PHPController) ErrorLog(ctx http.Context) http.Response {
log, _ := r.service.GetErrorLog()
return controllers.Success(ctx, log)
}
func (r *PHPController) SlowLog(ctx http.Context) http.Response {
log, _ := r.service.GetSlowLog()
return controllers.Success(ctx, log)
}
func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response {
err := r.service.ClearErrorLog()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response {
err := r.service.ClearSlowLog()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) GetExtensionList(ctx http.Context) http.Response {
extensions, err := r.service.GetExtensions()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, extensions)
}
func (r *PHPController) InstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if err := r.service.InstallExtension(slug); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) UninstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if err := r.service.UninstallExtension(slug); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,93 +0,0 @@
package plugins
import (
"os"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/tools"
)
type PhpMyAdminController struct {
}
func NewPhpMyAdminController() *PhpMyAdminController {
return &PhpMyAdminController{}
}
func (r *PhpMyAdminController) Info(ctx http.Context) http.Response {
files, err := os.ReadDir("/www/server/phpmyadmin")
if err != nil {
return controllers.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 controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
}
conf, err := tools.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
if len(match) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 端口")
}
return controllers.Success(ctx, http.Json{
"path": phpmyadmin,
"port": cast.ToInt(match[1]),
})
}
func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
port := ctx.Request().Input("port")
if len(port) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "端口不能为空")
}
conf, err := tools.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+port+";")
if err := tools.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 controllers.ErrorSystem(ctx)
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw allow " + port + "/tcp"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
if err := tools.ServiceReload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
}
return controllers.Success(ctx, nil)
}
@@ -1,239 +0,0 @@
package plugins
import (
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/podman"
"github.com/TheTNB/panel/pkg/tools"
)
type PodmanController struct {
}
func NewPodmanController() *PodmanController {
return &PodmanController{}
}
// Status
//
// @Summary 服务状态
// @Description 获取 Podman 服务状态
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/status [get]
func (r *PodmanController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("podman")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Podman 服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// IsEnabled
//
// @Summary 是否启用服务
// @Description 获取是否启用 Podman 服务
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/isEnabled [get]
func (r *PodmanController) IsEnabled(ctx http.Context) http.Response {
enabled, err := tools.ServiceIsEnabled("podman")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Podman 服务启用状态失败")
}
return controllers.Success(ctx, enabled)
}
// Enable
//
// @Summary 启用服务
// @Description 启用 Podman 服务
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/enable [post]
func (r *PodmanController) Enable(ctx http.Context) http.Response {
if err := tools.ServiceEnable("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启用 Podman 服务失败")
}
return controllers.Success(ctx, nil)
}
// Disable
//
// @Summary 禁用服务
// @Description 禁用 Podman 服务
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/disable [post]
func (r *PodmanController) Disable(ctx http.Context) http.Response {
if err := tools.ServiceDisable("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "禁用 Podman 服务失败")
}
return controllers.Success(ctx, nil)
}
// Restart
//
// @Summary 重启服务
// @Description 重启 Podman 服务
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/restart [post]
func (r *PodmanController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启 Podman 服务失败")
}
return controllers.Success(ctx, nil)
}
// Start
//
// @Summary 启动服务
// @Description 启动 Podman 服务
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/start [post]
func (r *PodmanController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动 Podman 服务失败")
}
status, err := tools.ServiceStatus("podman")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Podman 服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// Stop
//
// @Summary 停止服务
// @Description 停止 Podman 服务
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/stop [post]
func (r *PodmanController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止 Podman 服务失败")
}
status, err := tools.ServiceStatus("podman")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 Podman 服务运行状态失败")
}
return controllers.Success(ctx, !status)
}
// 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 := tools.Read("/etc/containers/registries.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := tools.Write("/etc/containers/registries.conf", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceRestart("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := tools.Read("/etc/containers/storage.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := tools.Write("/etc/containers/storage.conf", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceRestart("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,553 +0,0 @@
package plugins
import (
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
type Postgresql15Controller struct {
setting internal.Setting
backup internal.Backup
}
func NewPostgresql15Controller() *Postgresql15Controller {
return &Postgresql15Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Postgresql15Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Postgresql15Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Postgresql15Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Postgresql15Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Postgresql15Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *Postgresql15Controller) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/postgresql.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// GetUserConfig 获取用户配置
func (r *Postgresql15Controller) GetUserConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/pg_hba.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *Postgresql15Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// SaveUserConfig 保存用户配置
func (r *Postgresql15Controller) SaveUserConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *Postgresql15Controller) Load(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
time, err := tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
}
pid, err := tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
}
process, err := tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
}
connections, err := tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
}
storage, err := tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.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 controllers.Success(ctx, data)
}
// Log 获取日志
func (r *Postgresql15Controller) Log(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *Postgresql15Controller) ClearLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *Postgresql15Controller) DatabaseList(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
type database struct {
Name string `json:"name"`
Owner string `json:"owner"`
Encoding string `json:"encoding"`
}
raw, err := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
databases := strings.Split(raw, "\n")
if len(databases) >= 4 {
databases = databases[3 : len(databases)-1]
} else {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
var databaseList []database
for _, db := range databases {
parts := strings.Split(db, "|")
if len(parts) != 8 || len(strings.TrimSpace(parts[0])) == 0 {
continue
}
databaseList = append(databaseList, database{
Name: strings.TrimSpace(parts[0]),
Owner: strings.TrimSpace(parts[1]),
Encoding: strings.TrimSpace(parts[2]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databaseList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databaseList) {
endIndex = len(databaseList)
}
pagedDatabases := databaseList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databaseList),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *Postgresql15Controller) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteDatabase 删除数据库
func (r *Postgresql15Controller) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.PostgresqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []types.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *Postgresql15Controller) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *Postgresql15Controller) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.PostgresqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *Postgresql15Controller) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *Postgresql15Controller) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *Postgresql15Controller) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Role string `json:"role"`
}
raw, err := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
users := strings.Split(raw, "\n")
if len(users) < 4 {
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
}
users = users[3:]
var userList []user
for _, u := range users {
userInfo := strings.Split(u, "|")
if len(userInfo) != 3 {
continue
}
userList = append(userList, user{
User: strings.TrimSpace(userInfo[0]),
Role: strings.TrimSpace(userInfo[1]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userList) {
endIndex = len(userList)
}
pagedUsers := userList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userList),
"items": pagedUsers,
})
}
// AddUser 添加用户
func (r *Postgresql15Controller) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteUser 删除用户
func (r *Postgresql15Controller) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
if out, err := tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// SetUserPassword 设置用户密码
func (r *Postgresql15Controller) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -1,553 +0,0 @@
package plugins
import (
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
type Postgresql16Controller struct {
setting internal.Setting
backup internal.Backup
}
func NewPostgresql16Controller() *Postgresql16Controller {
return &Postgresql16Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Postgresql16Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Postgresql16Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Postgresql16Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Postgresql16Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Postgresql16Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *Postgresql16Controller) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/postgresql.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// GetUserConfig 获取用户配置
func (r *Postgresql16Controller) GetUserConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/pg_hba.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *Postgresql16Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// SaveUserConfig 保存用户配置
func (r *Postgresql16Controller) SaveUserConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *Postgresql16Controller) Load(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
time, err := tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
}
pid, err := tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
}
process, err := tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
}
connections, err := tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
}
storage, err := tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.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 controllers.Success(ctx, data)
}
// Log 获取日志
func (r *Postgresql16Controller) Log(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *Postgresql16Controller) ClearLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *Postgresql16Controller) DatabaseList(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
type database struct {
Name string `json:"name"`
Owner string `json:"owner"`
Encoding string `json:"encoding"`
}
raw, err := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
databases := strings.Split(raw, "\n")
if len(databases) >= 4 {
databases = databases[3 : len(databases)-1]
} else {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
var databaseList []database
for _, db := range databases {
parts := strings.Split(db, "|")
if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 {
continue
}
databaseList = append(databaseList, database{
Name: strings.TrimSpace(parts[0]),
Owner: strings.TrimSpace(parts[1]),
Encoding: strings.TrimSpace(parts[2]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databaseList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databaseList) {
endIndex = len(databaseList)
}
pagedDatabases := databaseList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databaseList),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *Postgresql16Controller) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteDatabase 删除数据库
func (r *Postgresql16Controller) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.PostgresqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []types.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *Postgresql16Controller) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *Postgresql16Controller) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.PostgresqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *Postgresql16Controller) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *Postgresql16Controller) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *Postgresql16Controller) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Role string `json:"role"`
}
raw, err := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
users := strings.Split(raw, "\n")
if len(users) < 4 {
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
}
users = users[3:]
var userList []user
for _, u := range users {
userInfo := strings.Split(u, "|")
if len(userInfo) != 2 {
continue
}
userList = append(userList, user{
User: strings.TrimSpace(userInfo[0]),
Role: strings.TrimSpace(userInfo[1]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userList) {
endIndex = len(userList)
}
pagedUsers := userList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userList),
"items": pagedUsers,
})
}
// AddUser 添加用户
func (r *Postgresql16Controller) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteUser 删除用户
func (r *Postgresql16Controller) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
if out, err := tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// SetUserPassword 设置用户密码
func (r *Postgresql16Controller) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -1,245 +0,0 @@
package plugins
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/tools"
)
type PureFtpdController struct {
}
type User struct {
Username string `json:"username"`
Path string `json:"path"`
}
func NewPureFtpdController() *PureFtpdController {
return &PureFtpdController{}
}
// Status 获取运行状态
func (r *PureFtpdController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
}
return controllers.Success(ctx, status)
}
// Restart 重启服务
func (r *PureFtpdController) Restart(ctx http.Context) http.Response {
err := tools.ServiceRestart("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PureFtpd失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *PureFtpdController) Start(ctx http.Context) http.Response {
err := tools.ServiceStart("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PureFtpd失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *PureFtpdController) Stop(ctx http.Context) http.Response {
err := tools.ServiceStop("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PureFtpd失败")
}
return controllers.Success(ctx, nil)
}
// List 获取用户列表
func (r *PureFtpdController) List(ctx http.Context) http.Response {
listRaw, err := tools.Exec("pure-pw list")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []User{},
})
}
listArr := strings.Split(listRaw, "\n")
var users []User
for _, v := range listArr {
if len(v) == 0 {
continue
}
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
users = append(users, User{
Username: match[1],
Path: strings.Replace(match[2], "/./", "/", 1),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(users) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []User{},
})
}
if endIndex > len(users) {
endIndex = len(users)
}
pagedUsers := users[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(users),
"items": pagedUsers,
})
}
// Add 添加用户
func (r *PureFtpdController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"username": "required",
"password": "required|min_len:6",
"path": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
username := ctx.Request().Input("username")
password := ctx.Request().Input("password")
path := ctx.Request().Input("path")
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if !tools.Exists(path) {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "目录不存在")
}
if err = tools.Chmod(path, 0755); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "修改目录权限失败")
}
if err = tools.Chown(path, "www", "www"); err != nil {
return nil
}
if out, err := tools.Exec(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("pure-pw mkdb"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// Delete 删除用户
func (r *PureFtpdController) Delete(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"username": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
username := ctx.Request().Input("username")
if out, err := tools.Exec("pure-pw userdel " + username + " -m"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("pure-pw mkdb"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// ChangePassword 修改密码
func (r *PureFtpdController) ChangePassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"username": "required",
"password": "required|min_len:6",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
username := ctx.Request().Input("username")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("pure-pw mkdb"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// GetPort 获取端口
func (r *PureFtpdController) GetPort(ctx http.Context) http.Response {
port, err := tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
}
return controllers.Success(ctx, cast.ToInt(port))
}
// SetPort 设置端口
func (r *PureFtpdController) SetPort(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"port": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
port := ctx.Request().Input("port")
if out, err := tools.Exec(`sed -i "s/Bind.*/Bind 0.0.0.0,` + port + `/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw allow " + port + "/tcp"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
return r.Restart(ctx)
}
@@ -1,124 +0,0 @@
package plugins
import (
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
type RedisController struct {
}
func NewRedisController() *RedisController {
return &RedisController{}
}
// Status 获取运行状态
func (r *RedisController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("redis")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
}
return controllers.Success(ctx, status)
}
// Restart 重启服务
func (r *RedisController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启Redis失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *RedisController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动Redis失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *RedisController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止Redis失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *RedisController) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/redis/redis.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *RedisController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/redis/redis.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *RedisController) Load(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("redis")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "Redis已停止运行")
}
raw, err := tools.Exec("redis-cli info")
if err != nil {
return controllers.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 controllers.Success(ctx, data)
}
@@ -1,390 +0,0 @@
package plugins
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
requests "github.com/TheTNB/panel/app/http/requests/plugins/rsync"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
type RsyncController struct {
}
func NewRsyncController() *RsyncController {
return &RsyncController{}
}
// Status
//
// @Summary 服务状态
// @Description 获取 Rsync 服务状态
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/status [get]
func (r *RsyncController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("rsyncd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// Restart
//
// @Summary 重启服务
// @Description 重启 Rsync 服务
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/restart [post]
func (r *RsyncController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启服务失败")
}
return controllers.Success(ctx, nil)
}
// Start
//
// @Summary 启动服务
// @Description 启动 Rsync 服务
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/start [post]
func (r *RsyncController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动服务失败")
}
return controllers.Success(ctx, nil)
}
// Stop
//
// @Summary 停止服务
// @Description 停止 Rsync 服务
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/stop [post]
func (r *RsyncController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("rsyncd"); err != nil {
return nil
}
status, err := tools.ServiceStatus("rsyncd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}
return controllers.Success(ctx, !status)
}
// 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 {
var paginateRequest commonrequests.Paginate
sanitize := controllers.Sanitize(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.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 = tools.Exec("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败")
}
case "hosts allow":
currentModule.HostsAllow = value
}
}
}
}
if currentModule != nil {
modules = append(modules, *currentModule)
}
startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
endIndex := paginateRequest.Page * paginateRequest.Limit
if startIndex > len(modules) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.RsyncModule{},
})
}
if endIndex > len(modules) {
endIndex = len(modules)
}
pagedModules := modules[startIndex:endIndex]
if pagedModules == nil {
pagedModules = []types.RsyncModule{}
}
return controllers.Success(ctx, http.Json{
"total": len(modules),
"items": pagedModules,
})
}
// 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 := controllers.Sanitize(ctx, &createRequest)
if sanitize != nil {
return sanitize
}
config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if strings.Contains(config, "["+createRequest.Name+"]") {
return controllers.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 := tools.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("echo '" + createRequest.AuthUser + ":" + createRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err := tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 controllers.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空")
}
config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if !strings.Contains(config, "["+name+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在")
}
module := tools.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 := tools.Exec("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
if err = tools.Write("/etc/rsyncd.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if !strings.Contains(config, "["+updateRequest.Name+"]") {
return controllers.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 := tools.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 := tools.Exec("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
if err = tools.Write("/etc/rsyncd.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("echo '" + updateRequest.AuthUser + ":" + updateRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err = tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := tools.Write("/etc/rsyncd.conf", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,202 +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/app/http/controllers"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/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 {
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
var s3fsList []types.S3fsMount
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(s3fsList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.S3fsMount{},
})
}
if endIndex > len(s3fsList) {
endIndex = len(s3fsList)
}
pagedS3fsList := s3fsList[startIndex:endIndex]
if pagedS3fsList == nil {
pagedS3fsList = []types.S3fsMount{}
}
return controllers.Success(ctx, http.Json{
"total": len(s3fsList),
"items": pagedS3fsList,
})
}
// Add 添加 S3fs 挂载
func (r *S3fsController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(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_-]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
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 controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
}
// 检查挂载目录是否存在且为空
if !tools.Exists(path) {
if err = tools.Mkdir(path, 0755); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录创建失败")
}
}
if !tools.Empty(path) {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
}
var s3fsList []types.S3fsMount
err = json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
}
for _, s := range s3fsList {
if s.Path == path {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在")
}
}
id := carbon.Now().TimestampMilli()
password := ak + ":" + sk
if err = tools.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil {
return nil
}
out, err := tools.Exec(`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 controllers.Error(ctx, http.StatusInternalServerError, out)
}
if mountCheck, err := tools.Exec("mount -a 2>&1"); err != nil {
_, _ = tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
}
if _, err := tools.Exec("df -h | grep " + path + " 2>&1"); err != nil {
_, _ = tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
return controllers.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 controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
}
err = r.setting.Set("s3fs", encoded)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
}
return controllers.Success(ctx, nil)
}
// Delete 删除 S3fs 挂载
func (r *S3fsController) Delete(ctx http.Context) http.Response {
id := ctx.Request().Input("id")
if len(id) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
}
var s3fsList []types.S3fsMount
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
return controllers.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 controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
}
if out, err := tools.Exec(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`umount '` + mount.Path + `' 2>&1`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if mountCheck, err := tools.Exec("mount -a 2>&1"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
}
if err := tools.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil {
return controllers.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 controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
}
err = r.setting.Set("s3fs", encoded)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
}
return controllers.Success(ctx, nil)
}
@@ -1,388 +0,0 @@
package plugins
import (
"strconv"
"strings"
"github.com/TheTNB/panel/pkg/tools"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
)
type SupervisorController struct {
ServiceName string
}
func NewSupervisorController() *SupervisorController {
var serviceName string
if tools.IsRHEL() {
serviceName = "supervisord"
} else {
serviceName = "supervisor"
}
return &SupervisorController{
ServiceName: serviceName,
}
}
// Status 状态
func (r *SupervisorController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus(r.ServiceName)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Supervisor状态失败")
}
return controllers.Success(ctx, status)
}
// Start 启动
func (r *SupervisorController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止
func (r *SupervisorController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启
func (r *SupervisorController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Reload 重载
func (r *SupervisorController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Log 日志
func (r *SupervisorController) Log(ctx http.Context) http.Response {
log, err := tools.Exec(`tail -n 200 /var/log/supervisor/supervisord.log`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *SupervisorController) ClearLog(ctx http.Context) http.Response {
if out, err := tools.Exec(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// Config 获取配置
func (r *SupervisorController) Config(ctx http.Context) http.Response {
var config string
var err error
if tools.IsRHEL() {
config, err = tools.Read(`/etc/supervisord.conf`)
} else {
config, err = tools.Read(`/etc/supervisor/supervisord.conf`)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *SupervisorController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
var err error
if tools.IsRHEL() {
err = tools.Write(`/etc/supervisord.conf`, config, 0644)
} else {
err = tools.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return r.Restart(ctx)
}
// Processes 进程列表
func (r *SupervisorController) Processes(ctx http.Context) http.Response {
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
type process struct {
Name string `json:"name"`
Status string `json:"status"`
Pid string `json:"pid"`
Uptime string `json:"uptime"`
}
out, err := tools.Exec(`supervisorctl status | awk '{print $1}'`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
var processList []process
for _, line := range strings.Split(out, "\n") {
if len(line) == 0 {
continue
}
var p process
p.Name = line
if status, err := tools.Exec(`supervisorctl status ` + line + ` | awk '{print $2}'`); err == nil {
p.Status = status
}
if p.Status == "RUNNING" {
pid, _ := tools.Exec(`supervisorctl status ` + line + ` | awk '{print $4}'`)
p.Pid = strings.ReplaceAll(pid, ",", "")
uptime, _ := tools.Exec(`supervisorctl status ` + line + ` | awk '{print $6}'`)
p.Uptime = uptime
} else {
p.Pid = "-"
p.Uptime = "-"
}
processList = append(processList, p)
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(processList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []process{},
})
}
if endIndex > len(processList) {
endIndex = len(processList)
}
pagedProcessList := processList[startIndex:endIndex]
if pagedProcessList == nil {
pagedProcessList = []process{}
}
return controllers.Success(ctx, http.Json{
"total": len(processList),
"items": pagedProcessList,
})
}
// StartProcess 启动进程
func (r *SupervisorController) StartProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl start ` + process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// StopProcess 停止进程
func (r *SupervisorController) StopProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl stop ` + process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// RestartProcess 重启进程
func (r *SupervisorController) RestartProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl restart ` + process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.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 tools.IsRHEL() {
logPath, err = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
} else {
logPath, err = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
}
log, err := tools.Exec(`tail -n 200 ` + logPath)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.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 tools.IsRHEL() {
logPath, err = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
} else {
logPath, err = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
}
if out, err := tools.Exec(`echo "" > ` + logPath); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.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 tools.IsRHEL() {
config, err = tools.Read(`/etc/supervisord.d/` + process + `.conf`)
} else {
config, err = tools.Read(`/etc/supervisor/conf.d/` + process + `.conf`)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.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 tools.IsRHEL() {
err = tools.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
} else {
err = tools.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
}
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
_, _ = tools.Exec(`supervisorctl reread`)
_, _ = tools.Exec(`supervisorctl update`)
_, _ = tools.Exec(`supervisorctl restart ` + process)
return controllers.Success(ctx, nil)
}
// AddProcess 添加进程
func (r *SupervisorController) AddProcess(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|alpha_dash",
"user": "required|alpha_dash",
"path": "required",
"command": "required",
"num": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
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
`
if tools.IsRHEL() {
err = tools.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
} else {
err = tools.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
}
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
_, _ = tools.Exec(`supervisorctl reread`)
_, _ = tools.Exec(`supervisorctl update`)
_, _ = tools.Exec(`supervisorctl start ` + name)
return controllers.Success(ctx, nil)
}
// DeleteProcess 删除进程
func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl stop ` + process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
var logPath string
var err error
if tools.IsRHEL() {
logPath, err = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
if err := tools.Remove(`/etc/supervisord.d/` + process + `.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
} else {
logPath, err = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
if err := tools.Remove(`/etc/supervisor/conf.d/` + process + `.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
}
if err := tools.Remove(logPath); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
_, _ = tools.Exec(`supervisorctl reread`)
_, _ = tools.Exec(`supervisorctl update`)
return controllers.Success(ctx, nil)
}
@@ -1,239 +0,0 @@
package plugins
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/tools"
)
type ToolBoxController struct {
}
func NewToolBoxController() *ToolBoxController {
return &ToolBoxController{}
}
// GetDNS 获取 DNS 信息
func (r *ToolBoxController) GetDNS(ctx http.Context) http.Response {
raw, err := tools.Read("/etc/resolv.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1)
if len(match) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 DNS 信息")
}
var dns []string
for _, m := range match {
dns = append(dns, m[1])
}
return controllers.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 controllers.Error(ctx, http.StatusUnprocessableEntity, "DNS 信息不能为空")
}
var dns string
dns += "nameserver " + dns1 + "\n"
dns += "nameserver " + dns2 + "\n"
if err := tools.Write("/etc/resolv.conf", dns, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入 DNS 信息失败")
}
return controllers.Success(ctx, nil)
}
// GetSWAP 获取 SWAP 信息
func (r *ToolBoxController) GetSWAP(ctx http.Context) http.Response {
var total, used, free string
var size int64
if tools.Exists("/www/swap") {
file, err := tools.FileInfo("/www/swap")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
}
size = file.Size() / 1024 / 1024
total = tools.FormatBytes(float64(file.Size()))
} else {
size = 0
total = "0.00 B"
}
raw, err := tools.Exec("free | grep Swap")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
}
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) > 0 {
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
}
return controllers.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 tools.Exists("/www/swap") {
if out, err := tools.Exec("swapoff /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("rm -f /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("sed -i '/www\\/swap/d' /etc/fstab"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
}
if size > 1 {
free, err := tools.Exec("df -k /www | awk '{print $4}' | tail -n 1")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取磁盘空间失败")
}
if cast.ToInt64(free)*1024 < int64(size)*1024*1024 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+tools.FormatBytes(cast.ToFloat64(free)))
}
btrfsCheck, _ := tools.Exec("df -T /www | awk '{print $2}' | tail -n 1")
if strings.Contains(btrfsCheck, "btrfs") {
if out, err := tools.Exec("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
} else {
if out, err := tools.Exec("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size)); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("mkswap -f /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if err := tools.Chmod("/www/swap", 0600); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "设置 SWAP 权限失败")
}
}
if out, err := tools.Exec("swapon /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
}
return controllers.Success(ctx, nil)
}
// GetTimezone 获取时区
func (r *ToolBoxController) GetTimezone(ctx http.Context) http.Response {
raw, err := tools.Exec("timedatectl | grep zone")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取时区信息失败")
}
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
if len(match) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "找不到时区信息")
}
type zone struct {
Label string `json:"label"`
Value string `json:"value"`
}
zonesRaw, err := tools.Exec("timedatectl list-timezones")
if err != nil {
return controllers.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 controllers.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 controllers.Error(ctx, http.StatusUnprocessableEntity, "时区不能为空")
}
if out, err := tools.Exec("timedatectl set-timezone " + timezone); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
return controllers.Success(ctx, nil)
}
// GetHosts 获取 hosts 信息
func (r *ToolBoxController) GetHosts(ctx http.Context) http.Response {
hosts, err := tools.Read("/etc/hosts")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
return controllers.Success(ctx, hosts)
}
// SetHosts 设置 hosts 信息
func (r *ToolBoxController) SetHosts(ctx http.Context) http.Response {
hosts := ctx.Request().Input("hosts")
if len(hosts) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "hosts 信息不能为空")
}
if err := tools.Write("/etc/hosts", hosts, 0644); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "写入 hosts 信息失败")
}
return controllers.Success(ctx, nil)
}
// SetRootPassword 设置 root 密码
func (r *ToolBoxController) SetRootPassword(ctx http.Context) http.Response {
password := ctx.Request().Input("password")
if len(password) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "密码不能为空")
}
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
}
password = strings.ReplaceAll(password, `'`, `\'`)
if out, err := tools.Exec(`yes '` + password + `' | passwd root`); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
return controllers.Success(ctx, nil)
}
-354
View File
@@ -1,354 +0,0 @@
package controllers
import (
"regexp"
"strings"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"github.com/TheTNB/panel/pkg/tools"
)
type SafeController struct {
ssh string
}
func NewSafeController() *SafeController {
var ssh string
if tools.IsRHEL() {
ssh = "sshd"
} else {
ssh = "ssh"
}
return &SafeController{
ssh: ssh,
}
}
// GetFirewallStatus 获取防火墙状态
func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response {
return Success(ctx, r.firewallStatus())
}
// SetFirewallStatus 设置防火墙状态
func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response {
var err error
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
err = tools.ServiceStart("firewalld")
} else {
_, err = tools.Exec("echo y | ufw enable")
}
} else {
if tools.IsRHEL() {
err = tools.ServiceStop("firewalld")
} else {
_, err = tools.Exec("ufw disable")
}
}
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// GetFirewallRules 获取防火墙规则
func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
if !r.firewallStatus() {
return Success(ctx, nil)
}
var rules []map[string]string
if tools.IsRHEL() {
out, err := tools.Exec("firewall-cmd --list-all 2>&1")
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
if len(match) == 0 {
return Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
}
ports := strings.Split(match[1], " ")
for _, port := range ports {
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
"protocol": rule[1],
})
}
} else {
out, err := tools.Exec("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if len(out) == 0 {
return Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
}
for _, port := range strings.Split(out, "\n") {
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
"protocol": rule[1],
})
}
}
startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
endIndex := paginateRequest.Page * paginateRequest.Limit
if startIndex > len(rules) {
return Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
}
if endIndex > len(rules) {
endIndex = len(rules)
}
pagedRules := rules[startIndex:endIndex]
return Success(ctx, http.Json{
"total": len(rules),
"items": pagedRules,
})
}
// AddFirewallRule 添加防火墙规则
func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response {
if !r.firewallStatus() {
return Error(ctx, http.StatusInternalServerError, "防火墙未启动")
}
port := ctx.Request().Input("port")
protocol := ctx.Request().Input("protocol")
if port == "" || protocol == "" || (protocol != "tcp" && protocol != "udp") {
return 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 Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
} else {
port := cast.ToInt(port)
if port < 1 || port > 65535 {
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
// ufw 需要替换 - 为 : 添加
if strings.Contains(port, "-") {
port = strings.ReplaceAll(port, "-", ":")
}
if out, err := tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw allow " + cast.ToString(port) + "/" + protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
return Success(ctx, nil)
}
// DeleteFirewallRule 删除防火墙规则
func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response {
if !r.firewallStatus() {
return Error(ctx, http.StatusUnprocessableEntity, "防火墙未启动")
}
port := ctx.Request().InputInt("port", 0)
protocol := ctx.Request().Input("protocol", "")
if port == 0 || protocol == "" {
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
return Success(ctx, nil)
}
// firewallStatus 获取防火墙状态
func (r *SafeController) firewallStatus() bool {
var running bool
if tools.IsRHEL() {
running, _ = tools.ServiceStatus("firewalld")
} else {
running, _ = tools.ServiceStatus("ufw")
}
return running
}
// GetSshStatus 获取 SSH 状态
func (r *SafeController) GetSshStatus(ctx http.Context) http.Response {
running, err := tools.ServiceStatus(r.ssh)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, running)
}
// SetSshStatus 设置 SSH 状态
func (r *SafeController) SetSshStatus(ctx http.Context) http.Response {
if ctx.Request().InputBool("status") {
if err := tools.ServiceEnable(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceStart(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
} else {
if err := tools.ServiceStop(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.ServiceDisable(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
}
return Success(ctx, nil)
}
// GetSshPort 获取 SSH 端口
func (r *SafeController) GetSshPort(ctx http.Context) http.Response {
out, err := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
return 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 Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
oldPort, err := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
if err != nil {
return Error(ctx, http.StatusInternalServerError, oldPort)
}
_, _ = tools.Exec("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
_, _ = tools.Exec("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
status, _ := tools.ServiceStatus(r.ssh)
if status {
_ = tools.ServiceRestart(r.ssh)
}
return Success(ctx, nil)
}
// GetPingStatus 获取 Ping 状态
func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
if tools.IsRHEL() {
out, err := tools.Exec(`firewall-cmd --list-all 2>&1`)
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
return Success(ctx, true)
} else {
return Success(ctx, false)
}
} else {
config, err := tools.Read("/etc/ufw/before.rules")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
return Success(ctx, true)
} else {
return Success(ctx, false)
}
}
}
// SetPingStatus 设置 Ping 状态
func (r *SafeController) SetPingStatus(ctx http.Context) http.Response {
var out string
var err error
if tools.IsRHEL() {
if ctx.Request().InputBool("status") {
out, err = tools.Exec(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
} else {
out, err = tools.Exec(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
}
} else {
if ctx.Request().InputBool("status") {
out, err = tools.Exec(`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 = tools.Exec(`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 Error(ctx, http.StatusInternalServerError, out)
}
if tools.IsRHEL() {
out, err = tools.Exec(`firewall-cmd --reload`)
} else {
out, err = tools.Exec(`ufw reload`)
}
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
return Success(ctx, nil)
}
-228
View File
@@ -1,228 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
requests "github.com/TheTNB/panel/app/http/requests/setting"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
)
type SettingController struct {
setting internal.Setting
}
func NewSettingController() *SettingController {
return &SettingController{
setting: services.NewSettingImpl(),
}
}
// List
//
// @Summary 设置列表
// @Description 获取面板设置列表
// @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 ErrorSystem(ctx)
}
var user models.User
err = facades.Auth(ctx).User(&user)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
}).Info("获取用户信息失败")
return ErrorSystem(ctx)
}
port, err := tools.Exec(`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 ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"name": r.setting.Get(models.SettingKeyName),
"language": facades.Config().GetString("app.locale"),
"entrance": facades.Config().GetString("http.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 更新设置
// @Description 更新面板设置
// @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 := Sanitize(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 ErrorSystem(ctx)
}
if !tools.Exists(updateRequest.BackupPath) {
if err = tools.Mkdir(updateRequest.BackupPath, 0644); err != nil {
return 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 ErrorSystem(ctx)
}
if !tools.Exists(updateRequest.WebsitePath) {
if err = tools.Mkdir(updateRequest.WebsitePath, 0755); err != nil {
return ErrorSystem(ctx)
}
if err = tools.Chown(updateRequest.WebsitePath, "www", "www"); err != nil {
return 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 ErrorSystem(ctx)
}
var user models.User
err = facades.Auth(ctx).User(&user)
if err != nil {
return ErrorSystem(ctx)
}
user.Username = updateRequest.UserName
user.Email = updateRequest.Email
if len(updateRequest.Password) > 0 {
hash, err := facades.Hash().Make(updateRequest.Password)
if err != nil {
return 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 ErrorSystem(ctx)
}
oldPort, err := tools.Exec(`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 ErrorSystem(ctx)
}
port := cast.ToString(updateRequest.Port)
if oldPort != port {
if out, err := tools.Exec("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/tcp --permanent 2>&1"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/tcp --permanent 2>&1"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw delete allow " + cast.ToString(port) + "/tcp"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw allow " + cast.ToString(port) + "/tcp"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
}
oldEntrance, err := tools.Exec(`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 ErrorSystem(ctx)
}
entrance := cast.ToString(updateRequest.Entrance)
if oldEntrance != entrance {
if out, err := tools.Exec("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=" + entrance + "!g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
oldLanguage, err := tools.Exec(`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 ErrorSystem(ctx)
}
if oldLanguage != updateRequest.Language {
if out, err := tools.Exec("sed -i 's/APP_LOCALE=" + oldLanguage + "/APP_LOCALE=" + updateRequest.Language + "/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
if updateRequest.SSL {
if out, err := tools.Exec("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language || updateRequest.SSL != facades.Config().GetBool("panel.ssl") {
tools.RestartPanel()
}
return Success(ctx, nil)
}
-160
View File
@@ -1,160 +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/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/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 Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
}
return 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 {
validator, err := ctx.Request().Validate(map[string]string{
"host": "required",
"port": "required",
"user": "required",
"password": "required",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
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 Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = r.setting.Set(models.SettingKeySshPort, port); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = r.setting.Set(models.SettingKeySshUser, user); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = r.setting.Set(models.SettingKeySshPassword, password); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return 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 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 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 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
}
@@ -1,36 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/swaggo/http-swagger/v2"
_ "github.com/TheTNB/panel/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 {
if !facades.Config().GetBool("app.debug") {
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
handler := httpSwagger.Handler()
handler(ctx.Response().Writer(), ctx.Request().Origin())
return nil
}
-87
View File
@@ -1,87 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/pkg/tools"
)
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 Success(ctx, http.Json{
"task": true,
})
}
return 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 ErrorSystem(ctx)
}
return 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 ErrorSystem(ctx)
}
log, err := tools.Exec(`tail -n 500 '` + task.Log + `'`)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "日志已被清理")
}
return 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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
-101
View File
@@ -1,101 +0,0 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/http/requests/user"
"github.com/TheTNB/panel/app/models"
)
type UserController struct {
// Dependent services
}
func NewUserController() *UserController {
return &UserController{
// Inject services
}
}
// Login
//
// @Summary 登录
// @Description 通过用户名和密码获取访问令牌
// @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 := Sanitize(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 ErrorSystem(ctx)
}
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
return 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 ErrorSystem(ctx)
}
}
token, loginErr := facades.Auth(ctx).LoginUsingID(user.ID)
if loginErr != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
}).Info("登录失败")
return ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"access_token": token,
})
}
// Info
//
// @Summary 用户信息
// @Description 获取当前登录用户信息
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/user/info [get]
func (r *UserController) Info(ctx http.Context) http.Response {
var user models.User
err := facades.Auth(ctx).User(&user)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
}).Info("获取用户信息失败")
return ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"id": user.ID,
"role": []string{"admin"},
"username": user.Username,
"email": user.Email,
})
}
-681
View File
@@ -1,681 +0,0 @@
package controllers
import (
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
requests "github.com/TheTNB/panel/app/http/requests/website"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/types"
)
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 获取网站列表
// @Description 获取网站管理的网站列表
// @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 := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"total": total,
"items": websites,
})
}
// Add
//
// @Summary 添加网站
// @Description 添加网站到网站管理
// @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 := Sanitize(ctx, &addRequest)
if sanitize != nil {
return sanitize
}
if len(addRequest.Path) == 0 {
addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name
}
website := types.Website{
Name: addRequest.Name,
Status: true,
Domains: addRequest.Domains,
Ports: addRequest.Ports,
Path: addRequest.Path,
Php: addRequest.Php,
Ssl: false,
Db: addRequest.Db,
DbType: addRequest.DbType,
DbName: addRequest.DbName,
DbUser: addRequest.DbUser,
DbPassword: addRequest.DbPassword,
}
_, err := r.website.Add(website)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("添加网站失败")
return ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// Delete
//
// @Summary 删除网站
// @Description 删除网站管理的网站
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id} [delete]
func (r *WebsiteController) Delete(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
err := r.website.Delete(idRequest.ID)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"id": idRequest.ID,
"error": err.Error(),
}).Info("删除网站失败")
return Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
}
return Success(ctx, nil)
}
// GetDefaultConfig
//
// @Summary 获取默认配置
// @Description 获取默认首页和停止页配置
// @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 := tools.Read("/www/server/openresty/html/index.html")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
stop, err := tools.Read("/www/server/openresty/html/stop.html")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, http.Json{
"index": index,
"stop": stop,
})
}
// SaveDefaultConfig
//
// @Summary 保存默认配置
// @Description 保存默认首页和停止页配置
// @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 := tools.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 ErrorSystem(ctx)
}
if err := tools.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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// GetConfig
//
// @Summary 获取配置
// @Description 获取网站的配置
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse{data=types.Website}
// @Router /panel/websites/{id}/config [get]
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(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 ErrorSystem(ctx)
}
return Success(ctx, config)
}
// SaveConfig
//
// @Summary 保存配置
// @Description 保存网站的配置
// @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 := Sanitize(ctx, &saveConfigRequest)
if sanitize != nil {
return sanitize
}
err := r.website.SaveConfig(saveConfigRequest)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// ClearLog
//
// @Summary 清空日志
// @Description 清空网站的日志
// @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 := Sanitize(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
website := models.Website{}
err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website)
if err != nil {
return ErrorSystem(ctx)
}
if err := tools.Remove("/www/wwwlogs/" + website.Name + ".log"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// UpdateRemark
//
// @Summary 更新备注
// @Description 更新网站的备注
// @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 := Sanitize(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
website := models.Website{}
err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website)
if err != nil {
return 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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// BackupList
//
// @Summary 获取备份列表
// @Description 获取网站的备份列表
// @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 := Sanitize(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
backupList, err := r.backup.WebsiteList()
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("获取备份列表失败")
return ErrorSystem(ctx)
}
startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
endIndex := paginateRequest.Page * paginateRequest.Limit
if startIndex > len(backupList) {
return Success(ctx, http.Json{
"total": 0,
"items": []types.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []types.BackupFile{}
}
return Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// CreateBackup
//
// @Summary 创建备份
// @Description 创建网站的备份
// @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 := Sanitize(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 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 Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error())
}
return Success(ctx, nil)
}
// UploadBackup
//
// @Summary 上传备份
// @Description 上传网站的备份
// @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 Error(ctx, http.StatusInternalServerError, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
if err = tools.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 ErrorSystem(ctx)
}
return Success(ctx, nil)
}
// RestoreBackup
//
// @Summary 还原备份
// @Description 还原网站的备份
// @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 := Sanitize(ctx, &restoreBackupRequest)
if sanitize != nil {
return sanitize
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", restoreBackupRequest.ID).Get(&website); err != nil {
return 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 Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error())
}
return Success(ctx, nil)
}
// DeleteBackup
//
// @Summary 删除备份
// @Description 删除网站的备份
// @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 := Sanitize(ctx, &deleteBackupRequest)
if sanitize != nil {
return sanitize
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
if err := tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
if err := tools.Remove(backupPath + "/" + deleteBackupRequest.Name); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// ResetConfig
//
// @Summary 重置配置
// @Description 重置网站的配置
// @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 := Sanitize(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil {
return 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 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;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
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)
if err := tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return nil
}
if err := tools.Write("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644); err != nil {
return nil
}
if err := tools.ServiceReload("openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// Status
//
// @Summary 状态
// @Description 启用或停用网站
// @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 := Sanitize(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil {
return ErrorSystem(ctx)
}
website.Status = ctx.Request().InputBool("status")
if err := facades.Orm().Query().Save(&website); err != nil {
return ErrorSystem(ctx)
}
raw, err := tools.Read("/www/server/vhost/" + website.Name + ".conf")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
// 运行目录
rootConfig := tools.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 := tools.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 = tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return ErrorSystem(ctx)
}
if err = tools.ServiceReload("openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
-18
View File
@@ -1,18 +0,0 @@
package http
import (
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/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{
middleware.Status(),
}
}
-47
View File
@@ -1,47 +0,0 @@
package middleware
import (
"errors"
"github.com/goravel/framework/auth"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// Jwt 确保通过 JWT 鉴权
func Jwt() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
token := ctx.Request().Header("Authorization", ctx.Request().Header("Sec-WebSocket-Protocol"))
if len(token) == 0 {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.token.missing"),
})
return
}
// JWT 鉴权
if _, err := facades.Auth(ctx).Parse(token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
token, err = facades.Auth(ctx).Refresh()
if err != nil {
// 到达刷新时间上限
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.token.expired"),
})
return
}
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.token.expired"),
})
return
}
}
ctx.Response().Header("Authorization", token)
ctx.Request().Next()
}
}
-93
View File
@@ -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/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()
}
}
-40
View File
@@ -1,40 +0,0 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/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
}
}
}
-34
View File
@@ -1,34 +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" filter:"uint"`
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
}
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) 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
}
@@ -1,32 +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) 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
}
-67
View File
@@ -1,67 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/spf13/cast"
)
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) 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 {
// TODO 由于验证器 filter 标签的问题,暂时这里这样处理
userID, exist := data.Get("user_id")
if exist {
err := data.Set("user_id", cast.ToUint(userID))
if err != nil {
return err
}
}
dnsID, exist := data.Get("dns_id")
if exist {
err := data.Set("dns_id", cast.ToUint(dnsID))
if err != nil {
return err
}
}
websiteID, exist := data.Get("website_id")
if exist {
err := data.Set("website_id", cast.ToUint(websiteID))
if err != nil {
return err
}
}
return nil
}
-69
View File
@@ -1,69 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/spf13/cast"
)
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) 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 {
// TODO 由于验证器 filter 标签的问题,暂时这里这样处理
userID, exist := data.Get("user_id")
if exist {
err := data.Set("user_id", cast.ToUint(userID))
if err != nil {
return err
}
}
dnsID, exist := data.Get("dns_id")
if exist {
err := data.Set("dns_id", cast.ToUint(dnsID))
if err != nil {
return err
}
}
websiteID, exist := data.Get("website_id")
if exist {
err := data.Set("website_id", cast.ToUint(websiteID))
if err != nil {
return err
}
}
return nil
}
@@ -1,32 +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) 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
}
-43
View File
@@ -1,43 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/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,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.api_key": "required_if:type,cloudflare",
}
}
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
}
-46
View File
@@ -1,46 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/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) 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
}
-32
View File
@@ -1,32 +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) 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
}
-32
View File
@@ -1,32 +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) 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
}
@@ -1,32 +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) 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
}
-40
View File
@@ -1,40 +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_unless:ca,letsencrypt,buypass",
"hmac_encoded": "required_unless:ca,letsencrypt,buypass",
"key_type": "required|in:P256,P384,2048,4096",
}
}
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
}
-42
View File
@@ -1,42 +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_unless:ca,letsencrypt,buypass",
"hmac_encoded": "required_unless:ca,letsencrypt,buypass",
"key_type": "required|in:P256,P384,2048,4096",
}
}
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
}
-34
View File
@@ -1,34 +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" filter:"int"`
Limit int `form:"limit" json:"limit" filter:"int"`
}
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) 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
}
@@ -1,77 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/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" filter:"int"`
CPUs int64 `form:"cpus" json:"cpus" filter:"int"`
Memory int64 `form:"memory" json:"memory" filter:"int"`
}
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) 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
}
@@ -1,34 +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) 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
}
@@ -1,70 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/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) 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
}
-32
View File
@@ -1,32 +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) 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
}
-38
View File
@@ -1,38 +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) 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
}
@@ -1,34 +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) 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
}
@@ -1,44 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/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) 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
}
@@ -1,40 +0,0 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/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) 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
}
-35
View File
@@ -1,35 +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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"file": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_not_exists`,
}
}
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
}
-34
View File
@@ -1,34 +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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"target": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
}
}
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
}
-32
View File
@@ -1,32 +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:^/([a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*)?$|path_exists`,
}
}
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
}
-34
View File
@@ -1,34 +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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"target": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
}
}
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
}
-32
View File
@@ -1,32 +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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_not_exists`,
}
}
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
}
-38
View File
@@ -1,38 +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 uint `form:"mode" json:"mode" filter:"uint"`
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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"mode": "regex:^[0-7]{3}$|uint",
"owner": "regex:^[a-zA-Z0-9_-]+$",
"group": "regex:^[a-zA-Z0-9_-]+$",
}
}
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
}
-34
View File
@@ -1,34 +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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"content": "required|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
}
-34
View File
@@ -1,34 +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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"keyword": "required|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
}

Some files were not shown because too many files have changed in this diff Show More