diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 73811c1a..f365e244 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -17,6 +17,13 @@ jobs: uses: actions/setup-go@v4 with: go-version: 'stable' + - name: Fetch Latest Frontend + run: | + apt install -y curl jq unzip zip + curl -s https://api.github.com/repos/haozi-team/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 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5145566..370de05b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: golang:bookworm +image: golang:alpine # 在每个任务执行前运行 before_script: @@ -52,11 +52,29 @@ build: - $OUTPUT_NAME expire_in: 3 days +fetch: + stage: build + before_script: + - sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories + - apk add --no-cache curl jq unzip zip + script: + - curl -s "https://jihulab.com/api/v4/projects/haozi-team%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip + - rm -rf public + - unzip frontend.zip + - mv dist public + artifacts: + name: "frontend" + paths: + - public + expire_in: 3 days + release: stage: release + dependencies: + - build image: name: goreleaser/goreleaser - entrypoint: [''] + entrypoint: [ '' ] only: - tags variables: diff --git a/app/http/controllers/cron_controller.go b/app/http/controllers/cron_controller.go index 15ba4858..644630eb 100644 --- a/app/http/controllers/cron_controller.go +++ b/app/http/controllers/cron_controller.go @@ -74,7 +74,7 @@ func (c *CronController) Add(ctx http.Context) http.Response { backupName = ctx.Request().Input("website") } backupPath := ctx.Request().Input("backup_path") - if len(backupName) == 0 { + if len(backupPath) == 0 { backupPath = c.setting.Get(models.SettingKeyBackupPath) + "/" + backupType } backupSave := ctx.Request().InputInt("save", 10) diff --git a/app/http/controllers/info_controller.go b/app/http/controllers/info_controller.go index aa55ad57..9b042511 100644 --- a/app/http/controllers/info_controller.go +++ b/app/http/controllers/info_controller.go @@ -123,24 +123,32 @@ func (c *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response { } type data struct { - Slug string `json:"slug"` - Name string `json:"name"` + Label string `json:"label"` + Value string `json:"value"` } var phpData []data - phpData = append(phpData, data{Slug: "0", Name: "不使用"}) + var dbData []data + phpData = append(phpData, data{Value: "0", Label: "不使用"}) + dbData = append(dbData, data{Value: "0", Label: "不使用"}) for _, p := range php { match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug) if len(match) == 0 { continue } - phpData = append(phpData, data{Slug: strings.ReplaceAll(p.Slug, "php", ""), Name: c.plugin.GetBySlug(p.Slug).Name}) + phpData = append(phpData, data{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: c.plugin.GetBySlug(p.Slug).Name}) + } + + if mysqlInstalled { + dbData = append(dbData, data{Value: "mysql", Label: "MySQL"}) + } + if postgresqlInstalled { + dbData = append(dbData, data{Value: "postgresql", Label: "PostgreSQL"}) } return Success(ctx, http.Json{ - "php": phpData, - "mysql": mysqlInstalled, - "postgresql": postgresqlInstalled, + "php": phpData, + "db": dbData, }) } diff --git a/app/http/controllers/monitor_controller.go b/app/http/controllers/monitor_controller.go index bf3f259b..cf95174b 100644 --- a/app/http/controllers/monitor_controller.go +++ b/app/http/controllers/monitor_controller.go @@ -23,7 +23,7 @@ func NewMonitorController() *MonitorController { // Switch 监控开关 func (r *MonitorController) Switch(ctx http.Context) http.Response { - value := ctx.Request().InputBool("switch") + value := ctx.Request().InputBool("monitor") err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value)) if err != nil { facades.Log().Error("[面板][MonitorController] 更新监控开关失败 ", err) diff --git a/app/http/controllers/safe_controller.go b/app/http/controllers/safe_controller.go index 33e8759c..92163c5e 100644 --- a/app/http/controllers/safe_controller.go +++ b/app/http/controllers/safe_controller.go @@ -132,17 +132,35 @@ func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response { return Error(ctx, http.StatusBadRequest, "防火墙未启动") } - port := ctx.Request().InputInt("port", 0) - protocol := ctx.Request().Input("protocol", "") - if port == 0 || protocol == "" { + port := ctx.Request().Input("port") + protocol := ctx.Request().Input("protocol") + if port == "" || protocol == "" || (protocol != "tcp" && protocol != "udp") { return Error(ctx, http.StatusBadRequest, "参数错误") } + // 端口有 2 种写法,一种是 80-443,一种是 80 + if strings.Contains(port, "-") { + ports := strings.Split(port, "-") + startPort := cast.ToInt(ports[0]) + endPort := cast.ToInt(ports[1]) + if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 || startPort > endPort { + return Error(ctx, http.StatusBadRequest, "参数错误") + } + } else { + port := cast.ToInt(port) + if port < 1 || port > 65535 { + return Error(ctx, http.StatusBadRequest, "参数错误") + } + } if tools.IsRHEL() { tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1") tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1") tools.Exec("firewall-cmd --reload") } else { + // ufw 需要替换 - 为 : 添加 + if strings.Contains(port, "-") { + port = strings.ReplaceAll(port, "-", ":") + } tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol) tools.Exec("ufw allow " + cast.ToString(port) + "/" + protocol) tools.Exec("ufw reload") diff --git a/app/http/controllers/setting_controller.go b/app/http/controllers/setting_controller.go index 89ed761c..467d9c53 100644 --- a/app/http/controllers/setting_controller.go +++ b/app/http/controllers/setting_controller.go @@ -28,25 +28,33 @@ func (r *SettingController) List(ctx http.Context) http.Response { return Error(ctx, http.StatusInternalServerError, "系统内部错误") } - var result = make(map[string]string) - for _, setting := range settings { - if setting.Key == models.SettingKeyMysqlRootPassword { - continue - } - - result[setting.Key] = setting.Value + type data struct { + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Port string `json:"port"` + Entrance string `json:"entrance"` + WebsitePath string `json:"website_path"` + BackupPath string `json:"backup_path"` } + var result data + result.Name = r.setting.Get(models.SettingKeyName) + result.Entrance = r.setting.Get(models.SettingKeyEntrance) + result.WebsitePath = r.setting.Get(models.SettingKeyWebsitePath) + result.BackupPath = r.setting.Get(models.SettingKeyBackupPath) + var user models.User err = facades.Auth().User(ctx, &user) if err != nil { facades.Log().Error("[面板][SettingController] 获取用户失败 ", err) return Error(ctx, http.StatusInternalServerError, "系统内部错误") } - result["username"] = user.Username - result["email"] = user.Email + result.Username = user.Username + result.Email = user.Email - result["port"] = tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) + result.Port = tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) return Success(ctx, result) } diff --git a/app/http/controllers/ssh_controller.go b/app/http/controllers/ssh_controller.go index c81f61f9..8a46787a 100644 --- a/app/http/controllers/ssh_controller.go +++ b/app/http/controllers/ssh_controller.go @@ -10,6 +10,7 @@ import ( "github.com/goravel/framework/contracts/http" "github.com/goravel/framework/facades" "github.com/gorilla/websocket" + "github.com/spf13/cast" "panel/app/models" "panel/app/services" @@ -40,7 +41,7 @@ func (r *SshController) GetInfo(ctx http.Context) http.Response { return Success(ctx, http.Json{ "host": host, - "port": port, + "port": cast.ToInt(port), "user": user, "password": password, }) diff --git a/app/http/controllers/user_controller.go b/app/http/controllers/user_controller.go index cd93c146..b0140013 100644 --- a/app/http/controllers/user_controller.go +++ b/app/http/controllers/user_controller.go @@ -26,7 +26,7 @@ func (r *UserController) Login(ctx http.Context) http.Response { return Error(ctx, http.StatusUnprocessableEntity, err.Error()) } if errors != nil { - return Error(ctx, http.StatusUnprocessableEntity, errors.All()) + return Error(ctx, http.StatusUnprocessableEntity, errors.One()) } var user models.User @@ -61,12 +61,18 @@ func (r *UserController) Login(ctx http.Context) http.Response { // Info 用户信息 func (r *UserController) Info(ctx http.Context) http.Response { - user, ok := ctx.Value("user").(models.User) - if !ok { - return Error(ctx, http.StatusUnauthorized, "登录已过期") + var user models.User + err := facades.Auth().User(ctx, &user) + if err != nil { + facades.Log().With(map[string]any{ + "error": err.Error(), + }).Error("[面板][UserController] 查询用户信息失败") + return Error(ctx, http.StatusInternalServerError, "系统内部错误") } return Success(ctx, http.Json{ + "id": user.ID, + "role": []string{"admin"}, "username": user.Username, "email": user.Email, }) diff --git a/app/http/controllers/website_controller.go b/app/http/controllers/website_controller.go index 47d6aabb..37940194 100644 --- a/app/http/controllers/website_controller.go +++ b/app/http/controllers/website_controller.go @@ -53,7 +53,8 @@ func (c *WebsiteController) Add(ctx http.Context) http.Response { } validator, err := ctx.Request().Validate(map[string]string{ "name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name", - "domain": "required", + "domains": "required|slice", + "ports": "required|slice", "php": "required", "db": "bool", "db_type": "required_if:db,true", @@ -70,7 +71,8 @@ func (c *WebsiteController) Add(ctx http.Context) http.Response { var website services.PanelWebsite website.Name = ctx.Request().Input("name") - website.Domain = ctx.Request().Input("domain") + website.Domains = ctx.Request().InputArray("domains") + website.Ports = ctx.Request().InputArray("ports") website.Php = ctx.Request().InputInt("php") website.Db = ctx.Request().InputBool("db") website.DbType = ctx.Request().Input("db_type") @@ -167,14 +169,13 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) http.Response { return check } validator, err := ctx.Request().Validate(map[string]string{ - "id": "required", - "domains": "required", - "ports": "required", + "domains": "required|slice", + "ports": "required|slice", "hsts": "bool", "ssl": "bool", "http_redirect": "bool", "open_basedir": "bool", - "waf": "required", + "waf": "bool", "waf_cache": "required", "waf_mode": "required", "waf_cc_deny": "required", @@ -219,7 +220,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) http.Response { // 域名 domain := "server_name" - domains := strings.Split(ctx.Request().Input("domains"), "\n") + domains := ctx.Request().InputArray("domains") if len(domains) == 0 { return Error(ctx, http.StatusBadRequest, "域名不能为空") } @@ -238,7 +239,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) http.Response { // 端口 var port strings.Builder - ports := strings.Split(ctx.Request().Input("ports"), "\n") + ports := ctx.Request().InputArray("ports") if len(ports) == 0 { return Error(ctx, http.StatusBadRequest, "端口不能为空") } @@ -299,12 +300,16 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) http.Response { } // WAF - waf := ctx.Request().Input("waf") + waf := ctx.Request().InputBool("waf") + wafStr := "off" + if waf { + wafStr = "on" + } wafMode := ctx.Request().Input("waf_mode", "DYNAMIC") wafCcDeny := ctx.Request().Input("waf_cc_deny", "rate=1000r/m duration=60m") wafCache := ctx.Request().Input("waf_cache", "capacity=50") wafConfig := `# waf标记位开始 - waf ` + waf + `; + waf ` + wafStr + `; waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; waf_mode ` + wafMode + `; waf_cc_deny ` + wafCcDeny + `; diff --git a/app/http/middleware/jwt.go b/app/http/middleware/jwt.go index ebc87c65..6b6f8539 100644 --- a/app/http/middleware/jwt.go +++ b/app/http/middleware/jwt.go @@ -6,16 +6,14 @@ import ( "github.com/goravel/framework/auth" "github.com/goravel/framework/contracts/http" "github.com/goravel/framework/facades" - - "panel/app/models" ) // Jwt 确保通过 JWT 鉴权 func Jwt() http.Middleware { return func(ctx http.Context) { - token := ctx.Request().Header("access_token", ctx.Request().Input("access_token", ctx.Request().Header("Sec-WebSocket-Protocol"))) + token := ctx.Request().Header("Authorization", ctx.Request().Header("Sec-WebSocket-Protocol")) if len(token) == 0 { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ + ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{ "code": 401, "message": "未登录", }) @@ -27,7 +25,7 @@ func Jwt() http.Middleware { if errors.Is(err, auth.ErrorTokenExpired) { token, err = facades.Auth().Refresh(ctx) if err != nil { - // Refresh time exceeded + // 到达刷新时间上限 ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{ "code": 401, "message": "登录已过期", @@ -45,18 +43,6 @@ func Jwt() http.Middleware { } } - // 取出用户信息 - var user models.User - if err := facades.Auth().User(ctx, &user); err != nil { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "code": 403, - "message": "用户不存在", - }) - return - } - - ctx.WithValue("user", user) - ctx.Response().Header("Authorization", token) ctx.Request().Next() } diff --git a/app/services/website.go b/app/services/website.go index 51e7113b..09f29790 100644 --- a/app/services/website.go +++ b/app/services/website.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "regexp" + "strconv" "strings" "github.com/goravel/framework/facades" @@ -21,32 +22,34 @@ type Website interface { Add(website PanelWebsite) (models.Website, error) Delete(id int) error GetConfig(id int) (WebsiteSetting, error) + GetConfigByName(name string) (WebsiteSetting, error) } type PanelWebsite struct { - Name string `json:"name"` - Status bool `json:"status"` - Domain string `json:"domain"` - Path string `json:"path"` - Php int `json:"php"` - Ssl bool `json:"ssl"` - Remark string `json:"remark"` - Db bool `json:"db"` - DbType string `json:"db_type"` - DbName string `json:"db_name"` - DbUser string `json:"db_user"` - DbPassword string `json:"db_password"` + Name string `json:"name"` + Status bool `json:"status"` + Domains []string `json:"domains"` + Ports []string `json:"ports"` + Path string `json:"path"` + Php int `json:"php"` + Ssl bool `json:"ssl"` + Remark string `json:"remark"` + Db bool `json:"db"` + DbType string `json:"db_type"` + DbName string `json:"db_name"` + DbUser string `json:"db_user"` + DbPassword string `json:"db_password"` } // WebsiteSetting 网站设置 type WebsiteSetting struct { Name string `json:"name"` - Ports []string `json:"ports"` Domains []string `json:"domains"` + Ports []string `json:"ports"` Root string `json:"root"` Path string `json:"path"` Index string `json:"index"` - Php int `json:"php"` + Php string `json:"php"` OpenBasedir bool `json:"open_basedir"` Ssl bool `json:"ssl"` SslCertificate string `json:"ssl_certificate"` @@ -107,7 +110,6 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) { website.Ssl = false website.Status = true - website.Domain = strings.TrimSpace(website.Domain) w := models.Website{ Name: website.Name, @@ -138,32 +140,28 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) { ` tools.Write(website.Path+"/index.html", index, 0644) - domainArr := strings.Split(website.Domain, "\n") portList := "" - portArr := make(map[string]bool) domainList := "" - for key, value := range domainArr { - temp := strings.Split(value, ":") - domainList += " " + temp[0] + portUsed := make(map[string]bool) + domainUsed := make(map[string]bool) - if len(temp) < 2 { - if _, ok := portArr["80"]; !ok { - if key == len(domainArr)-1 { - portList += " listen 80;" - } else { - portList += " listen 80;\n" - } - portArr["80"] = true - } - } else { - if _, ok := portArr[temp[1]]; !ok { - if key == len(domainArr)-1 { - portList += " listen " + temp[1] + ";" - } else { - portList += " listen " + temp[1] + ";\n" - } - portArr[temp[1]] = true + for i, port := range website.Ports { + if _, ok := portUsed[port]; !ok { + if i == len(website.Ports)-1 { + portList += " listen " + port + ";" + } else { + portList += " listen " + port + ";\n" } + portUsed[port] = true + } + } + if len(website.Ports) == 0 { + portList += " listen 80;\n" + } + for _, domain := range website.Domains { + if _, ok := domainUsed[domain]; !ok { + domainList += " " + domain + domainUsed[domain] = true } } @@ -288,7 +286,7 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) { setting.Name = website.Name setting.Path = website.Path setting.Ssl = website.Ssl - setting.Php = website.Php + setting.Php = strconv.Itoa(website.Php) setting.Raw = config ports := tools.Cut(config, "# port标记位开始", "# port标记位结束") @@ -370,3 +368,13 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) { return setting, nil } + +// GetConfigByName 根据网站名称获取网站配置 +func (r *WebsiteImpl) GetConfigByName(name string) (WebsiteSetting, error) { + var website models.Website + if err := facades.Orm().Query().Where("name", name).First(&website); err != nil { + return WebsiteSetting{}, err + } + + return r.GetConfig(int(website.ID)) +} diff --git a/routes/api.go b/routes/api.go index 9dda676a..0b4ab9c1 100644 --- a/routes/api.go +++ b/routes/api.go @@ -40,20 +40,20 @@ func Api() { websiteController := controllers.NewWebsiteController() r.Get("list", websiteController.List) r.Post("add", websiteController.Add) - r.Post("delete", websiteController.Delete) + r.Delete("delete/{id}", websiteController.Delete) r.Get("defaultConfig", websiteController.GetDefaultConfig) r.Post("defaultConfig", websiteController.SaveDefaultConfig) - r.Get("config", websiteController.GetConfig) - r.Post("config", websiteController.SaveConfig) - r.Get("clearLog", websiteController.ClearLog) - r.Post("updateRemark", websiteController.UpdateRemark) + r.Get("config/{id}", websiteController.GetConfig) + r.Post("config/{id}", websiteController.SaveConfig) + r.Delete("log/{id}", websiteController.ClearLog) + r.Post("updateRemark/{id}", websiteController.UpdateRemark) r.Get("backupList", websiteController.BackupList) r.Post("createBackup", websiteController.CreateBackup) r.Post("uploadBackup", websiteController.UploadBackup) - r.Post("restoreBackup", websiteController.RestoreBackup) - r.Post("deleteBackup", websiteController.DeleteBackup) - r.Post("resetConfig", websiteController.ResetConfig) - r.Post("status", websiteController.Status) + r.Post("restoreBackup/{id}", websiteController.RestoreBackup) + r.Post("deleteBackup/{id}", websiteController.DeleteBackup) + r.Post("resetConfig/{id}", websiteController.ResetConfig) + r.Post("status/{id}", websiteController.Status) }) r.Prefix("plugin").Middleware(middleware.Jwt()).Group(func(r route.Router) { pluginController := controllers.NewPluginController() @@ -66,20 +66,20 @@ func Api() { r.Prefix("cron").Middleware(middleware.Jwt()).Group(func(r route.Router) { cronController := controllers.NewCronController() r.Get("list", cronController.List) - r.Get("script", cronController.Script) + r.Get("{id}", cronController.Script) r.Post("add", cronController.Add) - r.Post("update", cronController.Update) - r.Post("delete", cronController.Delete) + r.Put("{id}", cronController.Update) + r.Delete("{id}", cronController.Delete) r.Post("status", cronController.Status) - r.Get("log", cronController.Log) + r.Get("log/{id}", cronController.Log) }) r.Prefix("safe").Middleware(middleware.Jwt()).Group(func(r route.Router) { safeController := controllers.NewSafeController() r.Get("firewallStatus", safeController.GetFirewallStatus) r.Post("firewallStatus", safeController.SetFirewallStatus) r.Get("firewallRules", safeController.GetFirewallRules) - r.Post("addFirewallRule", safeController.AddFirewallRule) - r.Post("deleteFirewallRule", safeController.DeleteFirewallRule) + r.Post("firewallRules", safeController.AddFirewallRule) + r.Delete("firewallRules", safeController.DeleteFirewallRule) r.Get("sshStatus", safeController.GetSshStatus) r.Post("sshStatus", safeController.SetSshStatus) r.Get("sshPort", safeController.GetSshPort)