Compare commits

..

71 Commits

Author SHA1 Message Date
耗子 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
147 changed files with 2225 additions and 1359 deletions
+1 -3
View File
@@ -19,11 +19,9 @@ jobs:
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
mv dist/* embed/frontend/
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
@@ -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.
-5
View File
@@ -67,8 +67,3 @@ out/
*.sublime*
__debug_bin
.project
# 前端工具链 #
.sass-cache/*
node_modules/
/public
+2 -3
View File
@@ -62,13 +62,12 @@ fetch:
- 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
- mv dist/* embed/frontend/
artifacts:
name: "frontend"
paths:
- public
- embed/frontend
expire_in: 3 days
release:
-1
View File
@@ -40,7 +40,6 @@ archives:
files:
- LICENSE
- docs/*
- public/*
- storage/*
- lang/*
- scripts/*
+5 -1
View File
@@ -2,7 +2,11 @@
[简体中文] | [<a href="README_EN.md">English</a>]
</p>
<h1 align="center">耗子面板</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>
+5 -1
View File
@@ -2,7 +2,11 @@
[<a href="README.md">简体中文</a>] | [English]
</p>
<h1 align="center">Rat 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>
+2 -3
View File
@@ -45,9 +45,8 @@ func (receiver *Monitoring) Handle(console.Context) error {
}
// 将等待中的任务分发
// TODO 有bug,需要设计一个锁机制防止重复分发
//task := services.NewTaskImpl()
//_ = task.DispatchWaiting()
task := services.NewTaskImpl()
_ = task.DispatchWaiting()
setting := services.NewSettingImpl()
monitor := setting.Get(models.SettingKeyMonitor)
+10 -11
View File
@@ -15,6 +15,7 @@ import (
"github.com/goravel/framework/support/color"
"github.com/spf13/cast"
requests "github.com/TheTNB/panel/app/http/requests/website"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/io"
@@ -164,8 +165,8 @@ func (receiver *Panel) Handle(ctx console.Context) error {
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"))
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("panel.entrance"))
color.Green().Printfln(translate.Get("commands.panel.getInfo.address") + ": " + protocol + "://" + ip + ":" + port + facades.Config().GetString("panel.entrance"))
case "getPort":
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
@@ -177,7 +178,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port)
case "getEntrance":
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("http.entrance"))
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("panel.entrance"))
case "deleteEntrance":
oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
@@ -505,8 +506,8 @@ func (receiver *Panel) Handle(ctx console.Context) error {
Name: name,
Status: status,
Path: path,
Php: php,
Ssl: ssl,
PHP: php,
SSL: ssl,
})
if err != nil {
color.Red().Printfln(translate.Get("commands.panel.writeSite.fail"))
@@ -614,15 +615,13 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
_, err = website.Add(types.Website{
_, err = website.Add(requests.Add{
Name: name,
Status: true,
Domains: domains,
Ports: uintPorts,
Path: path,
Php: php,
Ssl: false,
Db: false,
PHP: php,
DB: false,
})
if err != nil {
color.Red().Printfln(err.Error())
@@ -649,7 +648,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
if err = website.Delete(id); err != nil {
if err = website.Delete(requests.Delete{ID: id}); err != nil {
color.Red().Printfln(err.Error())
return nil
}
+6
View File
@@ -2,6 +2,8 @@ package commands
import (
"context"
"runtime"
"runtime/debug"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
@@ -68,6 +70,10 @@ func (receiver *PanelTask) Handle(console.Context) error {
return err
}
// 回收内存
runtime.GC()
debug.FreeOSMemory()
types.Status = types.StatusNormal
return nil
}
-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/io"
)
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 !io.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))
}
+4
View File
@@ -73,6 +73,10 @@ func (r *CertController) DNSProviders(ctx http.Context) http.Response {
"name": "DNSPod",
"dns": acme.DnsPod,
},
{
"name": "腾讯云",
"dns": acme.Tencent,
},
{
"name": "阿里云",
"dns": acme.AliYun,
+1 -1
View File
@@ -29,7 +29,7 @@ func Success(ctx http.Context, data any) http.Response {
// 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,
Message: message,
})
}
+11 -4
View File
@@ -5,6 +5,7 @@ import (
stdio "io"
stdos "os"
"path/filepath"
"strconv"
"strings"
"syscall"
@@ -354,10 +355,16 @@ func (r *FileController) Permission(ctx http.Context) http.Response {
return sanitize
}
if err := io.Chmod(request.Path, stdos.FileMode(request.Mode)); err != nil {
// 解析成8进制
mode, err := strconv.ParseUint(request.Mode, 8, 64)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := io.Chown(request.Path, request.Owner, request.Group); err != nil {
if err = io.Chmod(request.Path, stdos.FileMode(mode)); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = io.Chown(request.Path, request.Owner, request.Group); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -505,7 +512,7 @@ func (r *FileController) List(ctx http.Context) http.Response {
}
// setPermission
func (r *FileController) setPermission(path string, mode uint, owner, group string) {
_ = io.Chmod(path, stdos.FileMode(mode))
func (r *FileController) setPermission(path string, mode stdos.FileMode, owner, group string) {
_ = io.Chmod(path, mode)
_ = io.Chown(path, owner, group)
}
+24 -7
View File
@@ -8,6 +8,7 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/hashicorp/go-version"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
@@ -250,13 +251,21 @@ func (r *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response {
// CheckUpdate 检查面板更新
func (r *InfoController) CheckUpdate(ctx http.Context) http.Response {
version := facades.Config().GetString("panel.version")
remote, err := tools.GetLatestPanelVersion()
current := facades.Config().GetString("panel.version")
latest, err := tools.GetLatestPanelVersion()
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
if tools.VersionCompare(version, remote.Version, ">=") {
v1, err := version.NewVersion(current)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
v2, err := version.NewVersion(latest.Version)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
if v1.GreaterThanOrEqual(v2) {
return Success(ctx, http.Json{
"update": false,
})
@@ -269,17 +278,25 @@ func (r *InfoController) CheckUpdate(ctx http.Context) http.Response {
// UpdateInfo 获取更新信息
func (r *InfoController) UpdateInfo(ctx http.Context) http.Response {
version := facades.Config().GetString("panel.version")
current, err := tools.GetLatestPanelVersion()
current := facades.Config().GetString("panel.version")
latest, err := tools.GetLatestPanelVersion()
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
if tools.VersionCompare(version, current.Version, ">=") {
v1, err := version.NewVersion(current)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
v2, err := version.NewVersion(latest.Version)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
if v1.GreaterThanOrEqual(v2) {
return Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本")
}
versions, err := tools.GenerateVersions(version, current.Version)
versions, err := tools.GenerateVersions(current, latest.Version)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取更新信息失败")
}
+99 -151
View File
@@ -1,7 +1,6 @@
package plugins
import (
"database/sql"
"fmt"
"regexp"
@@ -12,6 +11,7 @@ import (
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
@@ -71,7 +71,7 @@ func (r *MySQLController) Load(ctx http.Context) http.Response {
return controllers.Success(ctx, []types.NV{})
}
raw, err := shell.Execf("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
}
@@ -175,80 +175,45 @@ func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response {
// SetRootPassword 设置root密码
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
status, err := systemctl.Status("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 = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';"`, oldRootPassword, rootPassword)); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置root密码失败: %v", err))
mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix")
if err != nil {
// 尝试安全模式直接改密
if err = db.MySQLResetRootPassword(rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if _, err = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "FLUSH PRIVILEGES;"`, rootPassword)); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
_, _ = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';"`, rootPassword, oldRootPassword))
_, _ = shell.Execf(fmt.Sprintf(`/www/server/mysql/bin/mysql -uroot -p%s -e "FLUSH PRIVILEGES;"`, oldRootPassword))
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
} else {
if err = mysql.UserPassword("root", rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
}
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
}
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)/")
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
defer db.Close()
if err = db.Ping(); err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
"items": []types.MySQLDatabase{},
})
}
rows, err := db.Query("SHOW DATABASES")
databases, err := mysql.Databases()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
if err = rows.Scan(&d.Name); err != nil {
continue
}
databases = append(databases, d)
}
if err = rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
}
paged, total := controllers.Paginate(ctx, databases)
return controllers.Success(ctx, http.Json{
@@ -260,9 +225,9 @@ func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
// AddDatabase 添加数据库
func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:32",
}); sanitize != nil {
return sanitize
}
@@ -272,17 +237,18 @@ func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := shell.Execf("/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)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/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 err = mysql.DatabaseCreate(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/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 err = mysql.UserCreate(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -291,15 +257,19 @@ func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
// DeleteDatabase 删除数据库
func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.DatabaseDrop(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -346,7 +316,7 @@ func (r *MySQLController) UploadBackup(ctx http.Context) http.Response {
// CreateBackup 创建备份
func (r *MySQLController) CreateBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
}); sanitize != nil {
return sanitize
}
@@ -380,7 +350,7 @@ func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response {
func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
}); sanitize != nil {
return sanitize
}
@@ -394,71 +364,20 @@ func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
// 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)/")
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
defer db.Close()
if err = db.Ping(); err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
"items": []types.MySQLUser{},
})
}
rows, err := db.Query("SELECT user, host FROM mysql.user")
users, err := mysql.Users()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
if err = rows.Scan(&u.User, &u.Host); 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
if err = grantsRows.Scan(&grant); 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, "获取用户列表失败")
}
paged, total := controllers.Paginate(ctx, userGrants)
paged, total := controllers.Paginate(ctx, users)
return controllers.Success(ctx, http.Json{
"total": total,
@@ -469,9 +388,9 @@ func (r *MySQLController) UserList(ctx http.Context) http.Response {
// AddUser 添加用户
func (r *MySQLController) AddUser(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:32",
}); sanitize != nil {
return sanitize
}
@@ -480,14 +399,15 @@ func (r *MySQLController) AddUser(ctx http.Context) http.Response {
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := shell.Execf("/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)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/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 err = mysql.UserCreate(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -496,15 +416,19 @@ func (r *MySQLController) AddUser(ctx http.Context) http.Response {
// DeleteUser 删除用户
func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.UserDrop(user); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -513,8 +437,8 @@ func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
// SetUserPassword 设置用户密码
func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:32",
}); sanitize != nil {
return sanitize
}
@@ -522,11 +446,12 @@ func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := shell.Execf("/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)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.UserPassword(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
@@ -535,8 +460,8 @@ func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
// SetUserPrivileges 设置用户权限
func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
}); sanitize != nil {
return sanitize
}
@@ -544,15 +469,38 @@ func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
if out, err := shell.Execf("/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)
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("/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 := shell.Execf("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// getSock 获取sock文件位置
func (r *MySQLController) getSock() string {
if io.Exists("/tmp/mysql.sock") {
return "/tmp/mysql.sock"
}
if io.Exists("/www/server/mysql/config/my.cnf") {
config, _ := io.Read("/www/server/mysql/config/my.cnf")
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 2 {
return matches[2]
}
}
if io.Exists("/etc/my.cnf") {
config, _ := io.Read("/etc/my.cnf")
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 2 {
return matches[2]
}
}
return "/tmp/mysql.sock"
}
@@ -1,6 +1,7 @@
package plugins
import (
"fmt"
"regexp"
"time"
@@ -61,7 +62,8 @@ func (r *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
}
if err := systemctl.Reload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err))
}
return controllers.Success(ctx, nil)
@@ -1,6 +1,7 @@
package plugins
import (
"fmt"
"regexp"
"strings"
@@ -87,8 +88,39 @@ func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
}
}
if err := systemctl.Reload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return controllers.Success(ctx, nil)
}
func (r *PhpMyAdminController) GetConfig(ctx http.Context) http.Response {
config, err := io.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PhpMyAdminController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
}
if err := io.Write("/www/server/vhost/phpmyadmin.conf", config, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{
"error": err.Error(),
}).Info("修改 phpMyAdmin 配置失败")
return controllers.ErrorSystem(ctx)
}
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return controllers.Success(ctx, nil)
@@ -202,9 +202,9 @@ func (r *PostgreSQLController) DatabaseList(ctx http.Context) http.Response {
// AddDatabase 添加数据库
func (r *PostgreSQLController) AddDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:40",
}); sanitize != nil {
return sanitize
}
@@ -241,7 +241,7 @@ func (r *PostgreSQLController) AddDatabase(ctx http.Context) http.Response {
// DeleteDatabase 删除数据库
func (r *PostgreSQLController) DeleteDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
}); sanitize != nil {
return sanitize
}
@@ -295,7 +295,7 @@ func (r *PostgreSQLController) UploadBackup(ctx http.Context) http.Response {
// CreateBackup 创建备份
func (r *PostgreSQLController) CreateBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
}); sanitize != nil {
return sanitize
}
@@ -329,7 +329,7 @@ func (r *PostgreSQLController) DeleteBackup(ctx http.Context) http.Response {
func (r *PostgreSQLController) RestoreBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
}); sanitize != nil {
return sanitize
}
@@ -420,9 +420,9 @@ func (r *PostgreSQLController) RoleList(ctx http.Context) http.Response {
// AddRole 添加角色
func (r *PostgreSQLController) AddRole(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:40",
}); sanitize != nil {
return sanitize
}
@@ -452,7 +452,7 @@ func (r *PostgreSQLController) AddRole(ctx http.Context) http.Response {
// DeleteRole 删除角色
func (r *PostgreSQLController) DeleteRole(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
}); sanitize != nil {
return sanitize
}
@@ -475,8 +475,8 @@ func (r *PostgreSQLController) DeleteRole(ctx http.Context) http.Response {
// SetRolePassword 设置用户密码
func (r *PostgreSQLController) SetRolePassword(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, 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",
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:40",
}); sanitize != nil {
return sanitize
}
+11 -12
View File
@@ -3,6 +3,7 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
"github.com/spf13/cast"
requests "github.com/TheTNB/panel/app/http/requests/setting"
@@ -44,13 +45,10 @@ func (r *SettingController) List(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
userID := cast.ToUint(ctx.Value("user_id"))
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)
if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil {
return Error(ctx, http.StatusInternalServerError, "获取用户信息失败")
}
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
@@ -64,7 +62,7 @@ func (r *SettingController) List(ctx http.Context) http.Response {
return Success(ctx, http.Json{
"name": r.setting.Get(models.SettingKeyName),
"language": facades.Config().GetString("app.locale"),
"entrance": facades.Config().GetString("http.entrance"),
"entrance": facades.Config().GetString("panel.entrance"),
"ssl": facades.Config().GetBool("panel.ssl"),
"website_path": r.setting.Get(models.SettingKeyWebsitePath),
"backup_path": r.setting.Get(models.SettingKeyBackupPath),
@@ -128,11 +126,12 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
userID := cast.ToUint(ctx.Value("user_id"))
var user models.User
err = facades.Auth(ctx).User(&user)
if err != nil {
return ErrorSystem(ctx)
if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil {
return Error(ctx, http.StatusInternalServerError, "获取用户信息失败")
}
user.Username = updateRequest.UserName
user.Email = updateRequest.Email
if len(updateRequest.Password) > 0 {
@@ -270,10 +269,10 @@ func (r *SettingController) UpdateHttps(ctx http.Context) http.Response {
if _, err := cert.ParseKey(httpsRequest.Key); err != nil {
return Error(ctx, http.StatusBadRequest, "密钥格式错误")
}
if err := io.Write(facades.App().ExecutablePath("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil {
if err := io.Write(path.Executable("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil {
return ErrorSystem(ctx)
}
if err := io.Write(facades.App().ExecutablePath("storage/ssl.key"), httpsRequest.Key, 0700); err != nil {
if err := io.Write(path.Executable("storage/ssl.key"), httpsRequest.Key, 0700); err != nil {
return ErrorSystem(ctx)
}
if out, err := shell.Execf("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil {
@@ -2,7 +2,6 @@ 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"
@@ -25,10 +24,6 @@ func NewSwaggerController() *SwaggerController {
// @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())
+32 -29
View File
@@ -3,6 +3,7 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/requests/user"
"github.com/TheTNB/panel/app/models"
@@ -20,16 +21,15 @@ func NewUserController() *UserController {
// 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]
// @Summary 登录
// @Tags 用户鉴权
// @Accept json
// @Produce json
// @Param data body requests.Login true "request"
// @Success 200 {object} SuccessResponse
// @Failure 403 {object} ErrorResponse "用户名或密码错误"
// @Failure 500 {object} ErrorResponse "系统内部错误
// @Router /panel/user/login [post]
func (r *UserController) Login(ctx http.Context) http.Response {
var loginRequest requests.Login
sanitize := SanitizeRequest(ctx, &loginRequest)
@@ -60,32 +60,35 @@ func (r *UserController) Login(ctx http.Context) http.Response {
}
}
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)
}
ctx.Request().Session().Put("user_id", user.ID)
return Success(ctx, nil)
}
return Success(ctx, http.Json{
"access_token": token,
})
// Logout
//
// @Summary 登出
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/user/logout [post]
func (r *UserController) Logout(ctx http.Context) http.Response {
ctx.Request().Session().Forget("user_id")
return Success(ctx, nil)
}
// Info
//
// @Summary 用户信息
// @Description 获取当前登录用户信息
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/user/info [get]
// @Summary 用户信息
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/user/info [get]
func (r *UserController) Info(ctx http.Context) http.Response {
userID := cast.ToUint(ctx.Value("user_id"))
var user models.User
err := facades.Auth(ctx).User(&user)
if err != nil {
if err := facades.Orm().Query().Where("id", userID).Get(&user); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
}).Info("获取用户信息失败")
+25 -39
View File
@@ -14,9 +14,9 @@ import (
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
type WebsiteController struct {
@@ -54,7 +54,7 @@ func (r *WebsiteController) List(ctx http.Context) http.Response {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("获取网站列表失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, http.Json{
@@ -84,27 +84,12 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
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)
_, err := r.website.Add(addRequest)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("添加网站失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
@@ -117,23 +102,22 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id} [delete]
// @Param data body requests.Delete true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/delete [post]
func (r *WebsiteController) Delete(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := SanitizeRequest(ctx, &idRequest)
var deleteRequest requests.Delete
sanitize := SanitizeRequest(ctx, &deleteRequest)
if sanitize != nil {
return sanitize
}
err := r.website.Delete(idRequest.ID)
if err != nil {
if err := r.website.Delete(deleteRequest); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"id": idRequest.ID,
"id": deleteRequest.ID,
"error": err.Error(),
}).Info("删除网站失败")
return Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
@@ -181,14 +165,14 @@ func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("保存默认首页配置失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := io.Write("/www/server/openresty/html/stop.html", stop, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("保存默认停止页配置失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
@@ -202,7 +186,7 @@ func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse{data=types.Website}
// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd}
// @Router /panel/websites/{id}/config [get]
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
var idRequest requests.ID
@@ -217,7 +201,7 @@ func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
"id": idRequest.ID,
"error": err.Error(),
}).Info("获取网站配置失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, config)
@@ -335,7 +319,7 @@ func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("获取备份列表失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
paged, total := Paginate(ctx, backups)
@@ -412,7 +396,7 @@ func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("上传备份失败")
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
@@ -506,7 +490,7 @@ func (r *WebsiteController) ResetConfig(ctx http.Context) http.Response {
}
website.Status = true
website.Ssl = false
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,
@@ -575,7 +559,7 @@ server
error_log /www/wwwlogs/%s.log;
}
`, website.Path, website.Php, website.Name, website.Name, website.Name, website.Name)
`, website.Path, website.PHP, website.Name, website.Name, website.Name, website.Name)
if err := io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return nil
}
@@ -586,7 +570,8 @@ server
return nil
}
if err := systemctl.Reload("openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
_, err = shell.Execf("openresty -t")
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return Success(ctx, nil)
@@ -649,10 +634,11 @@ func (r *WebsiteController) Status(ctx http.Context) http.Response {
}
if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return ErrorSystem(ctx)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = systemctl.Reload("openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
_, err = shell.Execf("openresty -t")
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return Success(ctx, nil)
+5
View File
@@ -2,6 +2,7 @@ package http
import (
"github.com/goravel/framework/contracts/http"
sessionmiddleware "github.com/goravel/framework/session/middleware"
"github.com/TheTNB/panel/app/http/middleware"
)
@@ -13,6 +14,10 @@ type Kernel struct {
// These middleware are run during every request to your application.
func (kernel Kernel) Middleware() []http.Middleware {
return []http.Middleware{
sessionmiddleware.StartSession(),
middleware.Log(),
middleware.Status(),
middleware.Entrance(),
middleware.Static(),
}
}
+39
View File
@@ -0,0 +1,39 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
)
func Entrance() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
if !ctx.Request().HasSession() {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.missing"),
})
return
}
entrance := facades.Config().GetString("panel.entrance")
if ctx.Request().Path() == entrance {
ctx.Request().Session().Put("verify_entrance", true)
_ = ctx.Response().Redirect(http.StatusFound, "/login").Render()
ctx.Request().AbortWithStatus(http.StatusFound)
return
}
if !facades.Config().GetBool("app.debug") &&
(ctx.Request().Session().Missing("verify_entrance") || !cast.ToBool(ctx.Request().Session().Get("verify_entrance"))) &&
ctx.Request().Path() != "/robots.txt" {
ctx.Request().AbortWithStatusJson(http.StatusTeapot, http.Json{
"message": "请通过正确的入口访问",
})
return
}
ctx.Request().Next()
}
}
-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()
}
}
+20
View File
@@ -0,0 +1,20 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// Log 记录请求日志
func Log() http.Middleware {
return func(ctx http.Context) {
facades.Log().Channel("http").With(map[string]any{
"Method": ctx.Request().Method(),
"URL": ctx.Request().FullUrl(),
"IP": ctx.Request().Ip(),
"UA": ctx.Request().Header("User-Agent"),
"Body": ctx.Request().All(),
}).Info("HTTP Request")
ctx.Request().Next()
}
}
+53
View File
@@ -0,0 +1,53 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
)
// Session 确保通过 JWT 鉴权
func Session() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
if !ctx.Request().HasSession() {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.missing"),
})
return
}
if ctx.Request().Session().Missing("user_id") {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.expired"),
})
return
}
userID := cast.ToUint(ctx.Request().Session().Get("user_id"))
if userID == 0 {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.invalid"),
})
return
}
// 刷新会话
/*if err := ctx.Request().Session().Regenerate(); err == nil {
ctx.Response().Cookie(http.Cookie{
Name: ctx.Request().Session().GetName(),
Value: ctx.Request().Session().GetID(),
MaxAge: facades.Config().GetInt("session.lifetime") * 60,
Path: facades.Config().GetString("session.path"),
Domain: facades.Config().GetString("session.domain"),
Secure: facades.Config().GetBool("session.secure"),
HttpOnly: facades.Config().GetBool("session.http_only"),
SameSite: facades.Config().GetString("session.same_site"),
})
}*/
ctx.WithValue("user_id", userID)
ctx.Request().Next()
}
}
+16
View File
@@ -0,0 +1,16 @@
package middleware
import (
"github.com/gin-contrib/static"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/gin"
"github.com/TheTNB/panel/embed"
)
func Static() http.Middleware {
return func(ctx http.Context) {
static.Serve("/", static.EmbedFolder(embed.PublicFS, "frontend"))(ctx.(*gin.Context).Instance())
ctx.Request().Next()
}
}
+9 -2
View File
@@ -6,8 +6,8 @@ import (
)
type CertDeploy struct {
ID uint `form:"id" json:"id" filter:"uint"`
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
ID uint `form:"id" json:"id"`
WebsiteID uint `form:"website_id" json:"website_id"`
}
func (r *CertDeploy) Authorize(ctx http.Context) error {
@@ -21,6 +21,13 @@ func (r *CertDeploy) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertDeploy) Filters(ctx http.Context) map[string]string {
return map[string]string{
"id": "uint",
"website_id": "uint",
}
}
func (r *CertDeploy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *CertShowAndDestroy) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertShowAndDestroy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *CertShowAndDestroy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+11 -3
View File
@@ -9,9 +9,9 @@ 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" filter:"uint"`
DNSID uint `form:"dns_id" json:"dns_id" filter:"uint"`
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
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 {
@@ -29,6 +29,14 @@ func (r *CertStore) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertStore) Filters(ctx http.Context) map[string]string {
return map[string]string{
"user_id": "uint",
"dns_id": "uint",
"website_id": "uint",
}
}
func (r *CertStore) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+11 -3
View File
@@ -10,9 +10,9 @@ type CertUpdate 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" filter:"uint"`
DNSID uint `form:"dns_id" json:"dns_id" filter:"uint"`
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
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 {
@@ -31,6 +31,14 @@ func (r *CertUpdate) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{
"user_id": "uint",
"dns_id": "uint",
"website_id": "uint",
}
}
func (r *CertUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *DNSShowAndDestroy) Rules(ctx http.Context) map[string]string {
}
}
func (r *DNSShowAndDestroy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DNSShowAndDestroy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+7 -3
View File
@@ -19,17 +19,21 @@ func (r *DNSStore) Authorize(ctx http.Context) error {
func (r *DNSStore) Rules(ctx http.Context) map[string]string {
return map[string]string{
"type": "required|in:dnspod,aliyun,cloudflare",
"type": "required|in:dnspod,tencent,aliyun,cloudflare",
"name": "required",
"data": "required",
"data.id": "required_if:type,dnspod",
"data.token": "required_if:type,dnspod",
"data.access_key": "required_if:type,aliyun",
"data.secret_key": "required_if:type,aliyun",
"data.access_key": "required_if:type,aliyun,tencent",
"data.secret_key": "required_if:type,aliyun,tencent",
"data.api_key": "required_if:type,cloudflare",
}
}
func (r *DNSStore) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DNSStore) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -33,6 +33,10 @@ func (r *DNSUpdate) Rules(ctx http.Context) map[string]string {
}
}
func (r *DNSUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DNSUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *Obtain) Rules(ctx http.Context) map[string]string {
}
}
func (r *Obtain) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Obtain) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *Renew) Rules(ctx http.Context) map[string]string {
}
}
func (r *Renew) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Renew) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *UserShowAndDestroy) Rules(ctx http.Context) map[string]string {
}
}
func (r *UserShowAndDestroy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UserShowAndDestroy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+6 -2
View File
@@ -21,12 +21,16 @@ 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",
"kid": "required_if:ca,sslcom,google",
"hmac_encoded": "required_if:ca,sslcom,google",
"key_type": "required|in:P256,P384,2048,4096",
}
}
func (r *UserStore) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UserStore) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+6 -2
View File
@@ -23,12 +23,16 @@ func (r *UserUpdate) Rules(ctx http.Context) 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",
"kid": "required_if:ca,sslcom,google",
"hmac_encoded": "required_if:ca,sslcom,google",
"key_type": "required|in:P256,P384,2048,4096",
}
}
func (r *UserUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UserUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+9 -2
View File
@@ -6,8 +6,8 @@ import (
)
type Paginate struct {
Page int `form:"page" json:"page" filter:"int"`
Limit int `form:"limit" json:"limit" filter:"int"`
Page int `form:"page" json:"page"`
Limit int `form:"limit" json:"limit"`
}
func (r *Paginate) Authorize(ctx http.Context) error {
@@ -21,6 +21,13 @@ func (r *Paginate) Rules(ctx http.Context) map[string]string {
}
}
func (r *Paginate) Filters(ctx http.Context) map[string]string {
return map[string]string{
"page": "int",
"limit": "int",
}
}
func (r *Paginate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -23,9 +23,9 @@ type ContainerCreate struct {
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"`
CPUShares int64 `form:"cpu_shares" json:"cpu_shares"`
CPUs int64 `form:"cpus" json:"cpus"`
Memory int64 `form:"memory" json:"memory"`
}
func (r *ContainerCreate) Authorize(ctx http.Context) error {
@@ -64,6 +64,14 @@ func (r *ContainerCreate) Rules(ctx http.Context) map[string]string {
}
}
func (r *ContainerCreate) Filters(ctx http.Context) map[string]string {
return map[string]string{
"cpu_shares": "int",
"cpus": "int",
"memory": "int",
}
}
func (r *ContainerCreate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -21,6 +21,10 @@ func (r *ContainerRename) Rules(ctx http.Context) map[string]string {
}
}
func (r *ContainerRename) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerRename) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -57,6 +57,10 @@ func (r *ContainerUpdate) Rules(ctx http.Context) map[string]string {
}
}
func (r *ContainerUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *ID) Rules(ctx http.Context) map[string]string {
}
}
func (r *ID) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ID) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -25,6 +25,10 @@ func (r *ImagePull) Rules(ctx http.Context) map[string]string {
}
}
func (r *ImagePull) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ImagePull) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -21,6 +21,10 @@ func (r *NetworkConnectDisConnect) Rules(ctx http.Context) map[string]string {
}
}
func (r *NetworkConnectDisConnect) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkConnectDisConnect) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -31,6 +31,10 @@ func (r *NetworkCreate) Rules(ctx http.Context) map[string]string {
}
}
func (r *NetworkCreate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkCreate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -27,6 +27,10 @@ func (r *VolumeCreate) Rules(ctx http.Context) map[string]string {
}
}
func (r *VolumeCreate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *VolumeCreate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -22,6 +22,10 @@ func (r *Archive) Rules(ctx http.Context) map[string]string {
}
}
func (r *Archive) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Archive) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -21,6 +21,10 @@ func (r *Copy) Rules(ctx http.Context) map[string]string {
}
}
func (r *Copy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Copy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *Exist) Rules(ctx http.Context) map[string]string {
}
}
func (r *Exist) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Exist) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -21,6 +21,10 @@ func (r *Move) Rules(ctx http.Context) map[string]string {
}
}
func (r *Move) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Move) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *NotExist) Rules(ctx http.Context) map[string]string {
}
}
func (r *NotExist) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NotExist) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+6 -2
View File
@@ -7,7 +7,7 @@ import (
type Permission struct {
Path string `form:"path" json:"path"`
Mode uint `form:"mode" json:"mode" filter:"uint"`
Mode string `form:"mode" json:"mode"`
Owner string `form:"owner" json:"owner"`
Group string `form:"group" json:"group"`
}
@@ -19,12 +19,16 @@ func (r *Permission) Authorize(ctx http.Context) error {
func (r *Permission) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_exists`,
"mode": "regex:^[0-7]{3}$|uint",
"mode": "regex:^0[0-7]{3}$",
"owner": "regex:^[a-zA-Z0-9_-]+$",
"group": "regex:^[a-zA-Z0-9_-]+$",
}
}
func (r *Permission) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Permission) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -21,6 +21,10 @@ func (r *Save) Rules(ctx http.Context) map[string]string {
}
}
func (r *Save) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Save) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -21,6 +21,10 @@ func (r *Search) Rules(ctx http.Context) map[string]string {
}
}
func (r *Search) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Search) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+5 -1
View File
@@ -17,10 +17,14 @@ func (r *UnArchive) Authorize(ctx http.Context) error {
func (r *UnArchive) Rules(ctx http.Context) map[string]string {
return map[string]string{
"file": `regex:^/.*$|path_exists`,
"path": `regex:^/.*$|path_not_exists`,
"path": `regex:^/.*$`,
}
}
func (r *UnArchive) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UnArchive) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -23,6 +23,10 @@ func (r *Upload) Rules(ctx http.Context) map[string]string {
}
}
func (r *Upload) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Upload) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *Service) Rules(ctx http.Context) map[string]string {
}
}
func (r *Service) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Service) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -21,6 +21,10 @@ func (r *UpdateConfig) Rules(ctx http.Context) map[string]string {
}
}
func (r *UpdateConfig) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UpdateConfig) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *UpdateConfig) Rules(ctx http.Context) map[string]string {
}
}
func (r *UpdateConfig) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UpdateConfig) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *UpdateRegistryConfig) Rules(ctx http.Context) map[string]string {
}
}
func (r *UpdateRegistryConfig) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UpdateRegistryConfig) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *UpdateStorageConfig) Rules(ctx http.Context) map[string]string {
}
}
func (r *UpdateStorageConfig) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UpdateStorageConfig) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -29,6 +29,10 @@ func (r *Create) Rules(ctx http.Context) map[string]string {
}
}
func (r *Create) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Create) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -29,6 +29,10 @@ func (r *Update) Rules(ctx http.Context) map[string]string {
}
}
func (r *Update) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Update) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *UpdateConfig) Rules(ctx http.Context) map[string]string {
}
}
func (r *UpdateConfig) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UpdateConfig) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -23,6 +23,10 @@ func (r *Https) Rules(ctx http.Context) map[string]string {
}
}
func (r *Https) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Https) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+7 -1
View File
@@ -8,7 +8,7 @@ import (
type Update struct {
Name string `form:"name" json:"name"`
Language string `form:"language" json:"language"`
Port uint `form:"port" json:"port" filter:"uint"`
Port uint `form:"port" json:"port"`
BackupPath string `form:"backup_path" json:"backup_path"`
WebsitePath string `form:"website_path" json:"website_path"`
Entrance string `form:"entrance" json:"entrance"`
@@ -35,6 +35,12 @@ func (r *Update) Rules(ctx http.Context) map[string]string {
}
}
func (r *Update) Filters(ctx http.Context) map[string]string {
return map[string]string{
"port": "uint",
}
}
func (r *Update) Messages(ctx http.Context) map[string]string {
return map[string]string{
"port.int": "port 值必须是一个整数且在 1000 - 65535 之间",
+5 -1
View File
@@ -23,12 +23,16 @@ func (r *Login) Rules(ctx http.Context) map[string]string {
func (r *Login) Messages(ctx http.Context) map[string]string {
return map[string]string{
"username.required": "登录名不能为空",
"username.required": "用户名不能为空",
"password.required": "密码不能为空",
"password.min_len": "密码长度不能小于 8 位",
}
}
func (r *Login) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Login) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
+10 -6
View File
@@ -10,12 +10,12 @@ type Add struct {
Domains []string `form:"domains" json:"domains"`
Ports []uint `form:"ports" json:"ports"`
Path string `form:"path" json:"path"`
Php string `form:"php" json:"php"`
Db bool `form:"db" json:"db"`
DbType string `form:"db_type" json:"db_type"`
DbName string `form:"db_name" json:"db_name"`
DbUser string `form:"db_user" json:"db_user"`
DbPassword string `form:"db_password" json:"db_password"`
PHP string `form:"php" json:"php"`
DB bool `form:"db" json:"db"`
DBType string `form:"db_type" json:"db_type"`
DBName string `form:"db_name" json:"db_name"`
DBUser string `form:"db_user" json:"db_user"`
DBPassword string `form:"db_password" json:"db_password"`
}
func (r *Add) Authorize(ctx http.Context) error {
@@ -37,6 +37,10 @@ func (r *Add) Rules(ctx http.Context) map[string]string {
}
}
func (r *Add) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Add) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+42
View File
@@ -0,0 +1,42 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Delete struct {
ID uint `form:"id" json:"id"`
Path bool `form:"path" json:"path"`
DB bool `form:"db" json:"db"`
}
func (r *Delete) Authorize(ctx http.Context) error {
return nil
}
func (r *Delete) Rules(ctx http.Context) map[string]string {
return map[string]string{
"id": "required|exists:websites,id",
"path": "bool",
"db": "bool",
}
}
func (r *Delete) Filters(ctx http.Context) map[string]string {
return map[string]string{
"id": "uint",
}
}
func (r *Delete) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Delete) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Delete) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
@@ -19,6 +19,10 @@ func (r *DeleteBackup) Rules(ctx http.Context) map[string]string {
}
}
func (r *DeleteBackup) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DeleteBackup) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+7 -1
View File
@@ -6,7 +6,7 @@ import (
)
type ID struct {
ID uint `form:"id" json:"id" filter:"uint"`
ID uint `form:"id" json:"id"`
}
func (r *ID) Authorize(ctx http.Context) error {
@@ -19,6 +19,12 @@ func (r *ID) Rules(ctx http.Context) map[string]string {
}
}
func (r *ID) Filters(ctx http.Context) map[string]string {
return map[string]string{
"id": "uint",
}
}
func (r *ID) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+7 -1
View File
@@ -6,7 +6,7 @@ import (
)
type RestoreBackup struct {
ID uint `form:"id" json:"id" filter:"uint"`
ID uint `form:"id" json:"id"`
Name string `form:"name" json:"name"`
}
@@ -21,6 +21,12 @@ func (r *RestoreBackup) Rules(ctx http.Context) map[string]string {
}
}
func (r *RestoreBackup) Filters(ctx http.Context) map[string]string {
return map[string]string{
"id": "uint",
}
}
func (r *RestoreBackup) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+20 -7
View File
@@ -6,12 +6,15 @@ import (
)
type SaveConfig struct {
ID uint `form:"id" json:"id" filter:"uint"`
ID uint `form:"id" json:"id"`
Domains []string `form:"domains" json:"domains"`
Ports []uint `form:"ports" json:"ports"`
Hsts bool `form:"hsts" json:"hsts"`
Ssl bool `form:"ssl" json:"ssl"`
HttpRedirect bool `form:"http_redirect" json:"http_redirect"`
SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"`
QUICPorts []uint `form:"quic_ports" json:"quic_ports"`
OCSP bool `form:"ocsp" json:"ocsp"`
HSTS bool `form:"hsts" json:"hsts"`
SSL bool `form:"ssl" json:"ssl"`
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
Waf bool `form:"waf" json:"waf"`
WafCache string `form:"waf_cache" json:"waf_cache"`
@@ -22,9 +25,9 @@ type SaveConfig struct {
Root string `form:"root" json:"root"`
Raw string `form:"raw" json:"raw"`
Rewrite string `form:"rewrite" json:"rewrite"`
Php int `form:"php" json:"php" filter:"int"`
SslCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
SslCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
PHP int `form:"php" json:"php"`
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
}
func (r *SaveConfig) Authorize(ctx http.Context) error {
@@ -36,6 +39,9 @@ func (r *SaveConfig) Rules(ctx http.Context) map[string]string {
"id": "required|exists:websites,id",
"domains": "required|slice",
"ports": "required|slice",
"ssl_ports": "slice|not_in:80",
"quic_ports": "slice|not_in:80",
"ocsp": "bool",
"hsts": "bool",
"ssl": "bool",
"http_redirect": "bool",
@@ -55,6 +61,13 @@ func (r *SaveConfig) Rules(ctx http.Context) map[string]string {
}
}
func (r *SaveConfig) Filters(ctx http.Context) map[string]string {
return map[string]string{
"id": "uint",
"php": "int",
}
}
func (r *SaveConfig) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+14 -20
View File
@@ -27,20 +27,11 @@ func (receiver *ProcessTask) Handle(args ...any) error {
}
var task models.Task
if err := facades.Orm().Query().Where("id = ?", taskID).Get(&task); err != nil {
_ = facades.Orm().Query().Where("id = ?", taskID).Get(&task)
if task.ID == 0 {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("获取任务失败")
return nil
}
task.Status = models.TaskStatusRunning
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务失败")
}).Infof("任务不存在")
return nil
}
@@ -48,15 +39,18 @@ func (receiver *ProcessTask) Handle(args ...any) error {
"task_id": taskID,
}).Infof("开始执行任务")
task.Status = models.TaskStatusRunning
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务状态失败")
return nil
}
if _, err := shell.Execf(task.Shell); err != nil {
task.Status = models.TaskStatusFailed
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务失败")
return nil
}
_ = facades.Orm().Query().Save(&task)
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
@@ -69,7 +63,7 @@ func (receiver *ProcessTask) Handle(args ...any) error {
facades.Log().Tags("面板", "异步任务").With(map[string]any{
"task_id": taskID,
"error": err.Error(),
}).Infof("更新任务失败")
}).Infof("更新任务状态失败")
return nil
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
type CertDNS struct {
orm.Model
Name string `gorm:"not null" json:"name"` // 备注名称
Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, aliyun, cloudflare)
Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, tencent, aliyun, cloudflare)
Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"`
Certs []*Cert `gorm:"foreignKey:DNSID" json:"-"`
+2 -2
View File
@@ -7,8 +7,8 @@ type Website struct {
Name string `gorm:"not null;unique" json:"name"`
Status bool `gorm:"not null;default:true" json:"status"`
Path string `gorm:"not null" json:"path"`
Php int `gorm:"not null" json:"php"`
Ssl bool `gorm:"not null" json:"ssl"`
PHP int `gorm:"not null" json:"php"`
SSL bool `gorm:"not null" json:"ssl"`
Remark string `gorm:"not null" json:"remark"`
Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"`
+2 -18
View File
@@ -1,14 +1,12 @@
package providers
import (
"fmt"
"github.com/goravel/framework/contracts/database/seeder"
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/database/gorm"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/pkg/migrate"
)
type DatabaseServiceProvider struct {
@@ -20,19 +18,5 @@ func (receiver *DatabaseServiceProvider) Register(app foundation.Application) {
func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) {
facades.Seeder().Register([]seeder.Seeder{})
if err := facades.Orm().Query().(*gorm.QueryImpl).Instance().AutoMigrate(
&models.Cert{},
&models.CertDNS{},
&models.CertUser{},
&models.Cron{},
&models.Database{},
&models.Monitor{},
&models.Plugin{},
&models.Setting{},
&models.Task{},
&models.User{},
&models.Website{},
); err != nil {
panic(fmt.Sprintf("Failed to migrate database: %v", err))
}
migrate.Migrate(facades.Orm().Query().(*gorm.QueryImpl).Instance())
}
+21 -1
View File
@@ -2,7 +2,9 @@ package providers
import (
"github.com/goravel/framework/contracts/foundation"
contractshttp "github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/http/limit"
"github.com/TheTNB/panel/app/http"
"github.com/TheTNB/panel/routes"
@@ -25,5 +27,23 @@ func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
}
func (receiver *RouteServiceProvider) configureRateLimiting() {
facades.RateLimiter().ForWithLimits("login", func(ctx contractshttp.Context) []contractshttp.Limit {
return []contractshttp.Limit{
limit.PerMinute(5).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{
"message": "请求过于频繁,请等待一分钟后再试",
})
}),
limit.PerHour(100).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{
"message": "请求过于频繁,请等待一小时后再试",
})
}),
limit.PerDay(1000).Response(func(ctx contractshttp.Context) {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{
"message": "面板遭受登录爆破攻击过多,已暂时屏蔽登录,请立刻更换面板端口",
})
}),
}
})
}
@@ -25,7 +25,6 @@ func (receiver *ValidationServiceProvider) rules() []validation.Rule {
return []validation.Rule{
&rules.Exists{},
&rules.NotExists{},
&rules.Captcha{},
&rules.PathExists{},
&rules.PathNotExists{},
}
-46
View File
@@ -1,46 +0,0 @@
package rules
import (
"strconv"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/pkg/captcha"
)
type Captcha struct {
}
// Signature The name of the rule.
func (receiver *Captcha) Signature() string {
return "captcha"
}
// Passes Determine if the validation rule passes.
func (receiver *Captcha) Passes(data validation.Data, val any, options ...any) bool {
captchaID, exist := data.Get("captcha_id")
if !exist {
return false
}
// 第一个参数(如果有),是否清除验证码,如 false
clear := true
var err error
if len(options) > 0 {
clear, err = strconv.ParseBool(options[0].(string))
if err != nil {
clear = true
}
}
if !captcha.NewCaptcha().VerifyCaptcha(captchaID.(string), val.(string), clear) {
return false
}
return true
}
// Message Get the validation error message.
func (receiver *Captcha) Message() string {
return ""
}
+5
View File
@@ -1,6 +1,8 @@
package bootstrap
import (
"runtime/debug"
"github.com/gookit/validate/locales/zhcn"
"github.com/goravel/framework/foundation"
@@ -8,6 +10,9 @@ import (
)
func Boot() {
debug.SetGCPercent(10)
debug.SetMemoryLimit(64 << 20)
zhcn.RegisterGlobal()
app := foundation.NewApplication()
+5 -2
View File
@@ -17,7 +17,9 @@ import (
"github.com/goravel/framework/queue"
"github.com/goravel/framework/route"
"github.com/goravel/framework/schedule"
"github.com/goravel/framework/session"
"github.com/goravel/framework/support/carbon"
"github.com/goravel/framework/support/path"
"github.com/goravel/framework/testing"
"github.com/goravel/framework/translation"
"github.com/goravel/framework/validation"
@@ -37,7 +39,7 @@ func init() {
// This value is the name of your application. This value is used when the
// framework needs to place the application's name in a notification or
// any other location as required by the application or its pkg.
"name": "Panel",
"name": "耗子面板",
// Application Environment
//
@@ -74,7 +76,7 @@ func init() {
//
// The path to the language files for the application. You may change
// the path to a different directory if you would like to customize it.
"lang_path": facades.App().ExecutablePath("lang"),
"lang_path": path.Executable("lang"),
// Encryption Key
//
@@ -103,6 +105,7 @@ func init() {
&crypt.ServiceProvider{},
&filesystem.ServiceProvider{},
&validation.ServiceProvider{},
&session.ServiceProvider{},
&translation.ServiceProvider{},
&testing.ServiceProvider{},
&providers.AppServiceProvider{},
-34
View File
@@ -1,34 +0,0 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("captcha", map[string]any{
// 验证码图片高度
"height": 60,
// 验证码图片宽度
"width": 120,
// 验证码的长度
"length": 6,
// 数字的最大倾斜角度
"maxskew": 0.6,
// 图片背景里的混淆点数量
"dotcount": 40,
// 过期时间,单位是分钟
"expire_time": 5,
// debug 模式下的过期时间,方便本地开发调试
"debug_expire_time": 3600,
// 非 production 环境,使用此 key 可跳过验证,方便测试
"testing_key": "captcha_skip_test",
})
}
+2 -2
View File
@@ -14,10 +14,10 @@ func init() {
// in web browsers. You are free to adjust these settings as needed.
//
// To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
"paths": []string{"*"},
"paths": []string{"1145141919810"}, // 避免框架使用CORS中间件
"allowed_methods": []string{"*"},
"allowed_origins": []string{"*"},
"allowed_headers": []string{"*"},
"allowed_headers": []string{""},
"exposed_headers": []string{""},
"max_age": 0,
"supports_credentials": false,
+2 -1
View File
@@ -2,6 +2,7 @@ package config
import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
)
func init() {
@@ -14,7 +15,7 @@ func init() {
"connections": map[string]any{
"panel": map[string]any{
"driver": "sqlite",
"database": config.Env("DB_FILE", facades.App().ExecutablePath("storage/panel.db")),
"database": config.Env("DB_FILE", path.Executable("storage/panel.db")),
"prefix": "",
"singular": false, // Table name is singular
},
+2 -1
View File
@@ -2,6 +2,7 @@ package config
import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
)
func init() {
@@ -24,7 +25,7 @@ func init() {
"disks": map[string]any{
"local": map[string]any{
"driver": "local",
"root": facades.App().ExecutablePath("storage"),
"root": path.Executable("storage"),
},
},
})
+3 -4
View File
@@ -3,6 +3,7 @@ package config
import (
"github.com/goravel/framework/contracts/route"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
ginfacades "github.com/goravel/gin/facades"
)
@@ -28,8 +29,6 @@ func init() {
"host": "",
// HTTP Port
"port": config.Env("APP_PORT", "8888"),
// HTTP Entrance
"entrance": config.Env("APP_ENTRANCE", "/"),
// HTTPS Configuration
"tls": map[string]any{
// HTTPS Host
@@ -39,9 +38,9 @@ func init() {
// SSL Certificate
"ssl": map[string]any{
// ca.pem
"cert": config.Env("APP_SSL_CERT", facades.App().ExecutablePath("storage/ssl.crt")),
"cert": config.Env("APP_SSL_CERT", path.Executable("storage/ssl.crt")),
// ca.key
"key": config.Env("APP_SSL_KEY", facades.App().ExecutablePath("storage/ssl.key")),
"key": config.Env("APP_SSL_KEY", path.Executable("storage/ssl.key")),
},
},
})
+10 -2
View File
@@ -2,6 +2,7 @@ package config
import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
)
func init() {
@@ -26,17 +27,24 @@ func init() {
},
"single": map[string]any{
"driver": "single",
"path": "storage/logs/panel.log",
"path": path.Executable("storage/logs/panel.log"),
"level": "info",
"print": true,
},
"daily": map[string]any{
"driver": "daily",
"path": "storage/logs/panel.log",
"path": path.Executable("storage/logs/panel.log"),
"level": "info",
"days": 7,
"print": true,
},
"http": map[string]any{
"driver": "daily",
"path": path.Executable("storage/logs/http.log"),
"level": "info",
"days": 7,
"print": false,
},
},
})
}
+3 -1
View File
@@ -8,7 +8,9 @@ func init() {
config := facades.Config()
config.Add("panel", map[string]any{
"name": "耗子面板",
"version": "v2.2.14",
"version": "v2.2.24",
"ssl": config.Env("APP_SSL", false),
// 安全入口
"entrance": config.Env("APP_ENTRANCE", "/"),
})
}
+85
View File
@@ -0,0 +1,85 @@
package config
import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
)
func init() {
config := facades.Config()
config.Add("session", map[string]any{
// Default Session Driver
//
// This option controls the default session "driver" that will be used on
// requests. By default, we will use the lightweight file session driver, but you
// may specify any of the other wonderful drivers provided here.
//
// Supported: "file"
"driver": config.Env("SESSION_DRIVER", "file"),
// Session Lifetime
//
// Here you may specify the number of minutes that you wish the session
// to be allowed to remain idle before it expires. If you want them
// to immediately expire when the browser is closed, then you may
// indicate that via the expire_on_close configuration option.
"lifetime": config.Env("SESSION_LIFETIME", 120),
"expire_on_close": config.Env("SESSION_EXPIRE_ON_CLOSE", false),
// Session File Location
//
// When using the file session driver, we need a location where the
// session files may be stored. A default has been set for you, but a
// different location may be specified. This is only needed for file sessions.
"files": path.Executable("storage/framework/sessions"),
// Session Sweeping Lottery
//
// Some session drivers must manually sweep their storage location to get
// rid of old sessions from storage. Here are the chances out of 100 that
// the sweeper will sweep the storage location. The default is 2 out of 100.
"lottery": []int{2, 100},
// Session Cookie Name
//
// Here you may change the name of the cookie used to identify a session
// in the application. The name specified here will get used every time
// a new session cookie is created by the framework for every driver.
"cookie": config.Env("SESSION_COOKIE", "panel_session"),
// Session Cookie Path
//
// The session cookie path determines the path for which the cookie will
// be regarded as available.Typically, this will be the root path of
// your application, but you are free to change this when necessary.
"path": config.Env("SESSION_PATH", "/"),
// Session Cookie Domain
//
// Here you may change the domain of the cookie used to identify a session
// in your application.This will determine which domains the cookie is
// available to in your application.A sensible default has been set.
"domain": config.Env("SESSION_DOMAIN", ""),
// HTTPS Only Cookies
//
// By setting this option to true, session cookies will only be sent back
// to the server if the browser has an HTTPS connection. This will keep
// the cookie from being sent to you if it cannot be done securely.
"secure": config.Env("SESSION_SECURE", false),
// HTTP Access Only
//
// Setting this to true will prevent JavaScript from accessing the value of
// the cookie, and the cookie will only be accessible through the HTTP protocol.
"http_only": config.Env("SESSION_HTTP_ONLY", true),
// Same-Site Cookies
//
// This option determines how your cookies behave when cross-site requests
// take place, and can be used to mitigate CSRF attacks.By default, we
// will set this value to "lax" since this is a secure default value.
"same_site": config.Env("SESSION_SAME_SITE", "lax"),
})
}
+57 -13
View File
@@ -3235,7 +3235,6 @@ const docTemplate = `{
"BearerToken": []
}
],
"description": "获取当前登录用户信息",
"produces": [
"application/json"
],
@@ -3255,7 +3254,6 @@ const docTemplate = `{
},
"/panel/user/login": {
"post": {
"description": "通过用户名和密码获取访问令牌",
"consumes": [
"application/json"
],
@@ -3299,6 +3297,30 @@ const docTemplate = `{
}
}
},
"/panel/user/logout": {
"post": {
"security": [
{
"BearerToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"用户鉴权"
],
"summary": "登出",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/panel/website/backupList": {
"get": {
"security": [
@@ -3574,8 +3596,8 @@ const docTemplate = `{
}
}
},
"/panel/websites/{id}": {
"delete": {
"/panel/websites/delete": {
"post": {
"security": [
{
"BearerToken": []
@@ -3593,11 +3615,13 @@ const docTemplate = `{
"summary": "删除网站",
"parameters": [
{
"type": "integer",
"description": "网站 ID",
"name": "id",
"in": "path",
"required": true
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.Delete"
}
}
],
"responses": {
@@ -3648,7 +3672,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/types.Website"
"$ref": "#/definitions/types.WebsiteAdd"
}
}
}
@@ -5156,7 +5180,7 @@ const docTemplate = `{
"type": "string"
},
"type": {
"description": "DNS 提供商 (dnspod, aliyun, cloudflare)",
"description": "DNS 提供商 (dnspod, tencent, aliyun, cloudflare)",
"type": "string"
},
"updated_at": {
@@ -5506,6 +5530,20 @@ const docTemplate = `{
}
}
},
"requests.Delete": {
"type": "object",
"properties": {
"db": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"path": {
"type": "boolean"
}
}
},
"requests.DeleteBackup": {
"type": "object",
"properties": {
@@ -5638,7 +5676,7 @@ const docTemplate = `{
"type": "string"
},
"mode": {
"type": "integer"
"type": "string"
},
"owner": {
"type": "string"
@@ -5721,6 +5759,12 @@ const docTemplate = `{
"ssl_certificate_key": {
"type": "string"
},
"tls_ports": {
"type": "array",
"items": {
"type": "integer"
}
},
"waf": {
"type": "boolean"
},
@@ -5915,7 +5959,7 @@ const docTemplate = `{
}
}
},
"types.Website": {
"types.WebsiteAdd": {
"type": "object",
"properties": {
"db": {
+57 -13
View File
@@ -3228,7 +3228,6 @@
"BearerToken": []
}
],
"description": "获取当前登录用户信息",
"produces": [
"application/json"
],
@@ -3248,7 +3247,6 @@
},
"/panel/user/login": {
"post": {
"description": "通过用户名和密码获取访问令牌",
"consumes": [
"application/json"
],
@@ -3292,6 +3290,30 @@
}
}
},
"/panel/user/logout": {
"post": {
"security": [
{
"BearerToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"用户鉴权"
],
"summary": "登出",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/panel/website/backupList": {
"get": {
"security": [
@@ -3567,8 +3589,8 @@
}
}
},
"/panel/websites/{id}": {
"delete": {
"/panel/websites/delete": {
"post": {
"security": [
{
"BearerToken": []
@@ -3586,11 +3608,13 @@
"summary": "删除网站",
"parameters": [
{
"type": "integer",
"description": "网站 ID",
"name": "id",
"in": "path",
"required": true
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.Delete"
}
}
],
"responses": {
@@ -3641,7 +3665,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/types.Website"
"$ref": "#/definitions/types.WebsiteAdd"
}
}
}
@@ -5149,7 +5173,7 @@
"type": "string"
},
"type": {
"description": "DNS 提供商 (dnspod, aliyun, cloudflare)",
"description": "DNS 提供商 (dnspod, tencent, aliyun, cloudflare)",
"type": "string"
},
"updated_at": {
@@ -5499,6 +5523,20 @@
}
}
},
"requests.Delete": {
"type": "object",
"properties": {
"db": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"path": {
"type": "boolean"
}
}
},
"requests.DeleteBackup": {
"type": "object",
"properties": {
@@ -5631,7 +5669,7 @@
"type": "string"
},
"mode": {
"type": "integer"
"type": "string"
},
"owner": {
"type": "string"
@@ -5714,6 +5752,12 @@
"ssl_certificate_key": {
"type": "string"
},
"tls_ports": {
"type": "array",
"items": {
"type": "integer"
}
},
"waf": {
"type": "boolean"
},
@@ -5908,7 +5952,7 @@
}
}
},
"types.Website": {
"types.WebsiteAdd": {
"type": "object",
"properties": {
"db": {
+54 -28
View File
@@ -148,7 +148,7 @@ definitions:
description: 备注名称
type: string
type:
description: DNS 提供商 (dnspod, aliyun, cloudflare)
description: DNS 提供商 (dnspod, tencent, aliyun, cloudflare)
type: string
updated_at:
$ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime'
@@ -377,6 +377,15 @@ definitions:
type:
type: string
type: object
requests.Delete:
properties:
db:
type: boolean
id:
type: integer
path:
type: boolean
type: object
requests.DeleteBackup:
properties:
name:
@@ -462,7 +471,7 @@ definitions:
group:
type: string
mode:
type: integer
type: string
owner:
type: string
path:
@@ -516,6 +525,10 @@ definitions:
type: string
ssl_certificate_key:
type: string
tls_ports:
items:
type: integer
type: array
waf:
type: boolean
waf_cache:
@@ -641,7 +654,7 @@ definitions:
value:
type: string
type: object
types.Website:
types.WebsiteAdd:
properties:
db:
type: boolean
@@ -2632,7 +2645,6 @@ paths:
- 系统
/panel/user/info:
get:
description: 获取当前登录用户信息
produces:
- application/json
responses:
@@ -2649,7 +2661,6 @@ paths:
post:
consumes:
- application/json
description: 通过用户名和密码获取访问令牌
parameters:
- description: request
in: body
@@ -2675,6 +2686,20 @@ paths:
summary: 登录
tags:
- 用户鉴权
/panel/user/logout:
post:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 登出
tags:
- 用户鉴权
/panel/website/backupList:
get:
parameters:
@@ -2836,28 +2861,6 @@ paths:
summary: 添加网站
tags:
- 网站
/panel/websites/{id}:
delete:
consumes:
- application/json
parameters:
- description: 网站 ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 删除网站
tags:
- 网站
/panel/websites/{id}/config:
get:
consumes:
@@ -2878,7 +2881,7 @@ paths:
- $ref: '#/definitions/controllers.SuccessResponse'
- properties:
data:
$ref: '#/definitions/types.Website'
$ref: '#/definitions/types.WebsiteAdd'
type: object
security:
- BearerToken: []
@@ -3044,6 +3047,29 @@ paths:
summary: 更新网站备注
tags:
- 网站
/panel/websites/delete:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: data
required: true
schema:
$ref: '#/definitions/requests.Delete'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 删除网站
tags:
- 网站
/plugins/frp/config:
get:
description: 获取 Frp 配置
+9
View File
@@ -0,0 +1,9 @@
package embed
import "embed"
//go:embed frontend/*
var PublicFS embed.FS
//go:embed website/*
var WebsiteFS embed.FS
+1
View File
@@ -0,0 +1 @@
*
+55
View File
@@ -0,0 +1,55 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
<style>
body {
background-color: #f9f9f9;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 2em auto;
background-color: #ffffff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2.5em;
margin-top: 0;
margin-bottom: 20px;
text-align: center;
color: #333;
border-bottom: 2px solid #ddd;
padding-bottom: 0.5em;
}
p {
color: #555;
line-height: 1.8;
text-align: center;
}
a {
text-decoration: none;
color: #007bff;
}
@media screen and (max-width: 768px) {
.container {
padding: 15px;
margin: 2em 15px;
}
h1 {
font-size: 1.8em;
}
}
</style>
</head>
<body>
<div class="container">
<h1>404 Not Found</h1>
<p><a target="_blank" href="https://panel.haozi.net">耗子面板</a> 强力驱动</p>
</div>
</body>
</html>

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