diff --git a/build/configs/.gitignore b/build/configs/.gitignore index fb0ef7fb..8d069de8 100644 --- a/build/configs/.gitignore +++ b/build/configs/.gitignore @@ -1,3 +1,4 @@ server.yaml api_db.yaml -api.yaml \ No newline at end of file +api.yaml +*.pem \ No newline at end of file diff --git a/internal/utils/strings.go b/internal/utils/strings.go new file mode 100644 index 00000000..d17693b6 --- /dev/null +++ b/internal/utils/strings.go @@ -0,0 +1,15 @@ +package utils + +import "strings" + +// format address +func FormatAddress(addr string) string { + if strings.HasSuffix(addr, "unix:") { + return addr + } + addr = strings.Replace(addr, " ", "", -1) + addr = strings.Replace(addr, "\t", "", -1) + addr = strings.Replace(addr, ":", ":", -1) + addr = strings.TrimSpace(addr) + return addr +} diff --git a/internal/web/actions/default/about/init.go b/internal/web/actions/default/about/init.go new file mode 100644 index 00000000..0cbca509 --- /dev/null +++ b/internal/web/actions/default/about/init.go @@ -0,0 +1,12 @@ +package about + +import "github.com/iwind/TeaGo" + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Prefix("/about"). + Get("/qq", new(QqAction)). + EndAll() + }) +} diff --git a/internal/web/actions/default/about/qq.go b/internal/web/actions/default/about/qq.go new file mode 100644 index 00000000..c04a4205 --- /dev/null +++ b/internal/web/actions/default/about/qq.go @@ -0,0 +1,15 @@ +package about + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type QqAction struct { + actionutils.ParentAction +} + +func (this *QqAction) Init() { + this.Nav("", "", "") +} + +func (this *QqAction) RunGet(params struct{}) { + this.Show() +} diff --git a/internal/web/actions/default/clusters/checkChange.go b/internal/web/actions/default/clusters/checkChange.go index 61c6d25b..b8034b95 100644 --- a/internal/web/actions/default/clusters/checkChange.go +++ b/internal/web/actions/default/clusters/checkChange.go @@ -4,7 +4,6 @@ import ( "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/iwind/TeaGo/maps" - "time" ) // 检查变更的集群列表 @@ -19,47 +18,20 @@ func (this *CheckChangeAction) Init() { func (this *CheckChangeAction) RunPost(params struct { IsNotifying bool }) { - timeout := time.NewTimer(55 * time.Second) // 比客户端提前结束,避免在客户端产生一个请求错误 - - this.Data["clusters"] = []interface{}{} - -Loop: - for { - select { - case <-this.Request.Context().Done(): - break Loop - case <-timeout.C: - break Loop - default: - // 继续 - } - - resp, err := this.RPC().NodeClusterRPC().FindAllChangedNodeClusters(this.AdminContext(), &pb.FindAllChangedNodeClustersRequest{}) - if err != nil { - this.ErrorPage(err) - return - } - - result := []maps.Map{} - for _, cluster := range resp.Clusters { - result = append(result, maps.Map{ - "id": cluster.Id, - "name": cluster.Name, - }) - } - - // 从提醒到提醒消失 - if len(result) == 0 && params.IsNotifying { - break - } - - this.Data["clusters"] = result - if len(result) > 0 { - break - } - - time.Sleep(1 * time.Second) + resp, err := this.RPC().NodeClusterRPC().FindAllChangedNodeClusters(this.AdminContext(), &pb.FindAllChangedNodeClustersRequest{}) + if err != nil { + this.ErrorPage(err) + return } + result := []maps.Map{} + for _, cluster := range resp.Clusters { + result = append(result, maps.Map{ + "id": cluster.Id, + "name": cluster.Name, + }) + } + + this.Data["clusters"] = result this.Success() } diff --git a/internal/web/actions/default/servers/components/ssl/selectPopup.go b/internal/web/actions/default/servers/components/ssl/selectPopup.go index 67268d08..f85584cd 100644 --- a/internal/web/actions/default/servers/components/ssl/selectPopup.go +++ b/internal/web/actions/default/servers/components/ssl/selectPopup.go @@ -19,10 +19,17 @@ func (this *SelectPopupAction) Init() { this.Nav("", "", "") } -func (this *SelectPopupAction) RunGet(params struct{}) { +func (this *SelectPopupAction) RunGet(params struct { + ViewSize string +}) { // TODO 支持关键词搜索 // TODO 列出常用的证书供用户选择 + if len(params.ViewSize) == 0 { + params.ViewSize = "normal" + } + this.Data["viewSize"] = params.ViewSize + countResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{}) if err != nil { this.ErrorPage(err) diff --git a/internal/web/actions/default/settings/index.go b/internal/web/actions/default/settings/index.go index c459abd2..9a3da65f 100644 --- a/internal/web/actions/default/settings/index.go +++ b/internal/web/actions/default/settings/index.go @@ -1,6 +1,8 @@ package settings -import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +) type IndexAction struct { actionutils.ParentAction @@ -11,5 +13,5 @@ func (this *IndexAction) Init() { } func (this *IndexAction) RunGet(params struct{}) { - this.Show() + this.RedirectURL("/settings/ui") } diff --git a/internal/web/actions/default/settings/init.go b/internal/web/actions/default/settings/init.go index 1e71dd3f..f7935152 100644 --- a/internal/web/actions/default/settings/init.go +++ b/internal/web/actions/default/settings/init.go @@ -1,7 +1,6 @@ package settings import ( - "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/settingutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" "github.com/iwind/TeaGo" ) @@ -11,7 +10,6 @@ func init() { server. Helper(helpers.NewUserMustAuth()). Helper(NewHelper()). - Helper(settingutils.NewHelper("console")). Prefix("/settings"). Get("", new(IndexAction)). EndAll() diff --git a/internal/web/actions/default/settings/settingutils/utils.go b/internal/web/actions/default/settings/settingutils/utils.go index d04337e0..ca372ab8 100644 --- a/internal/web/actions/default/settings/settingutils/utils.go +++ b/internal/web/actions/default/settings/settingutils/utils.go @@ -25,7 +25,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) // 标签栏 tabbar := actionutils.NewTabbar() - tabbar.Add("管理界面", "", "/settings", "", this.tab == "console") + tabbar.Add("管理界面", "", "/settings", "", this.tab == "ui") tabbar.Add("安全设置", "", "/settings/security", "", this.tab == "security") tabbar.Add("数据库", "", "/settings/database", "", this.tab == "database") tabbar.Add("API节点", "", "/api", "", this.tab == "apiNodes") diff --git a/internal/web/actions/default/settings/ui/index.go b/internal/web/actions/default/settings/ui/index.go new file mode 100644 index 00000000..317fc25c --- /dev/null +++ b/internal/web/actions/default/settings/ui/index.go @@ -0,0 +1,27 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" +) + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("", "", "") +} + +func (this *IndexAction) RunGet(params struct{}) { + this.Data["serverIsChanged"] = serverConfigIsChanged + + serverConfig, err := loadServerConfig() + if err != nil { + this.ErrorPage(err) + return + } + + this.Data["serverConfig"] = serverConfig + + this.Show() +} diff --git a/internal/web/actions/default/settings/ui/init.go b/internal/web/actions/default/settings/ui/init.go new file mode 100644 index 00000000..e6e533d8 --- /dev/null +++ b/internal/web/actions/default/settings/ui/init.go @@ -0,0 +1,20 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/settingutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth()). + Helper(settingutils.NewHelper("ui")). + Prefix("/settings/ui"). + Get("", new(IndexAction)). + GetPost("/updateHTTPPopup", new(UpdateHTTPPopupAction)). + GetPost("/updateHTTPSPopup", new(UpdateHTTPSPopupAction)). + EndAll() + }) +} diff --git a/internal/web/actions/default/settings/ui/updateHTTPPopup.go b/internal/web/actions/default/settings/ui/updateHTTPPopup.go new file mode 100644 index 00000000..db1abbe5 --- /dev/null +++ b/internal/web/actions/default/settings/ui/updateHTTPPopup.go @@ -0,0 +1,65 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/utils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/iwind/TeaGo/actions" + "net" +) + +type UpdateHTTPPopupAction struct { + actionutils.ParentAction +} + +func (this *UpdateHTTPPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *UpdateHTTPPopupAction) RunGet(params struct{}) { + serverConfig, err := loadServerConfig() + if err != nil { + this.ErrorPage(err) + return + } + this.Data["serverConfig"] = serverConfig + + this.Show() +} + +func (this *UpdateHTTPPopupAction) RunPost(params struct { + IsOn bool + Listens []string + + Must *actions.Must +}) { + if len(params.Listens) == 0 { + this.Fail("请输入绑定地址") + } + + serverConfig, err := loadServerConfig() + if err != nil { + this.Fail("保存失败:" + err.Error()) + } + + serverConfig.Http.On = params.IsOn + + listen := []string{} + for _, addr := range params.Listens { + addr = utils.FormatAddress(addr) + if len(addr) == 0 { + continue + } + if _, _, err := net.SplitHostPort(addr); err != nil { + addr += ":80" + } + listen = append(listen, addr) + } + serverConfig.Http.Listen = listen + + err = writeServerConfig(serverConfig) + if err != nil { + this.Fail("保存失败:" + err.Error()) + } + + this.Success() +} diff --git a/internal/web/actions/default/settings/ui/updateHTTPSPopup.go b/internal/web/actions/default/settings/ui/updateHTTPSPopup.go new file mode 100644 index 00000000..675d2b8f --- /dev/null +++ b/internal/web/actions/default/settings/ui/updateHTTPSPopup.go @@ -0,0 +1,140 @@ +package ui + +import ( + "encoding/json" + "github.com/TeaOSLab/EdgeAdmin/internal/utils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" + "github.com/iwind/TeaGo/Tea" + "github.com/iwind/TeaGo/actions" + "io/ioutil" + "net" +) + +type UpdateHTTPSPopupAction struct { + actionutils.ParentAction +} + +func (this *UpdateHTTPSPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *UpdateHTTPSPopupAction) RunGet(params struct{}) { + serverConfig, err := loadServerConfig() + if err != nil { + this.ErrorPage(err) + return + } + this.Data["serverConfig"] = serverConfig + + // 证书 + certConfigs := []*sslconfigs.SSLCertConfig{} + if len(serverConfig.Https.Cert) > 0 && len(serverConfig.Https.Key) > 0 { + certData, err := ioutil.ReadFile(Tea.Root + "/" + serverConfig.Https.Cert) + if err != nil { + this.ErrorPage(err) + return + } + keyData, err := ioutil.ReadFile(Tea.Root + "/" + serverConfig.Https.Key) + if err != nil { + this.ErrorPage(err) + return + } + certConfig := &sslconfigs.SSLCertConfig{ + Id: 0, + Name: "-", + CertData: certData, + KeyData: keyData, + } + _ = certConfig.Init() + certConfig.CertData = nil + certConfig.KeyData = nil + certConfigs = append(certConfigs, certConfig) + } + this.Data["certConfigs"] = certConfigs + + this.Show() +} + +func (this *UpdateHTTPSPopupAction) RunPost(params struct { + IsOn bool + Listens []string + CertIdsJSON []byte + + Must *actions.Must +}) { + if len(params.Listens) == 0 { + this.Fail("请输入绑定地址") + } + + serverConfig, err := loadServerConfig() + if err != nil { + this.Fail("保存失败:" + err.Error()) + } + + serverConfig.Https.On = params.IsOn + + listen := []string{} + for _, addr := range params.Listens { + addr = utils.FormatAddress(addr) + if len(addr) == 0 { + continue + } + if _, _, err := net.SplitHostPort(addr); err != nil { + addr += ":80" + } + listen = append(listen, addr) + } + serverConfig.Https.Listen = listen + + // 证书 + certIds := []int64{} + err = json.Unmarshal(params.CertIdsJSON, &certIds) + if err != nil { + this.ErrorPage(err) + return + } + if params.IsOn && len(certIds) == 0 { + this.Fail("要启用HTTPS,需要先选择或上传一个可用的证书") + } + + // 保存证书到本地 + if len(certIds) > 0 && certIds[0] != 0 { + certResp, err := this.RPC().SSLCertRPC().FindEnabledSSLCertConfig(this.AdminContext(), &pb.FindEnabledSSLCertConfigRequest{ + CertId: certIds[0], + }) + if err != nil { + this.ErrorPage(err) + return + } + if len(certResp.CertJSON) == 0 { + this.Fail("选择的证书已失效,请换一个") + } + + certConfig := &sslconfigs.SSLCertConfig{} + err = json.Unmarshal(certResp.CertJSON, certConfig) + if err != nil { + this.ErrorPage(err) + return + } + err = ioutil.WriteFile(Tea.ConfigFile("https.key.pem"), certConfig.KeyData, 0666) + if err != nil { + this.Fail("保存密钥失败:" + err.Error()) + } + err = ioutil.WriteFile(Tea.ConfigFile("https.cert.pem"), certConfig.CertData, 0666) + if err != nil { + this.Fail("保存证书失败:" + err.Error()) + } + + serverConfig.Https.Key = "configs/https.key.pem" + serverConfig.Https.Cert = "configs/https.cert.pem" + } + + err = writeServerConfig(serverConfig) + if err != nil { + this.Fail("保存配置失败:" + err.Error()) + } + + this.Success() +} diff --git a/internal/web/actions/default/settings/ui/utils.go b/internal/web/actions/default/settings/ui/utils.go new file mode 100644 index 00000000..94d1a077 --- /dev/null +++ b/internal/web/actions/default/settings/ui/utils.go @@ -0,0 +1,41 @@ +package ui + +import ( + "github.com/iwind/TeaGo" + "github.com/iwind/TeaGo/Tea" + "gopkg.in/yaml.v3" + "io/ioutil" +) + +var serverConfigIsChanged = false + +// 读取当前服务配置 +func loadServerConfig() (*TeaGo.ServerConfig, error) { + configFile := Tea.ConfigFile("server.yaml") + data, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, err + } + serverConfig := &TeaGo.ServerConfig{} + err = yaml.Unmarshal(data, serverConfig) + if err != nil { + return nil, err + } + return serverConfig, nil +} + +// 保存当前服务配置 +func writeServerConfig(serverConfig *TeaGo.ServerConfig) error { + data, err := yaml.Marshal(serverConfig) + if err != nil { + return err + } + err = ioutil.WriteFile(Tea.ConfigFile("server.yaml"), data, 0666) + if err != nil { + return err + } + + serverConfigIsChanged = true + + return nil +} diff --git a/internal/web/import.go b/internal/web/import.go index f7dc6cd3..95b4eb7f 100644 --- a/internal/web/import.go +++ b/internal/web/import.go @@ -1,6 +1,7 @@ package web import ( + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/about" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/api" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/api/node" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters" @@ -70,6 +71,7 @@ import ( _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/login" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/profile" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/security" + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/ui" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/upgrade" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ui" diff --git a/web/public/images/qq-group-qrcode.png b/web/public/images/qq-group-qrcode.png new file mode 100644 index 00000000..a1ba550c Binary files /dev/null and b/web/public/images/qq-group-qrcode.png differ diff --git a/web/public/js/components/server/ssl-certs-box.js b/web/public/js/components/server/ssl-certs-box.js index 079cebd5..49c31d99 100644 --- a/web/public/js/components/server/ssl-certs-box.js +++ b/web/public/js/components/server/ssl-certs-box.js @@ -1,5 +1,5 @@ Vue.component("ssl-certs-box", { - props: ["v-certs", "v-protocol"], + props: ["v-certs", "v-protocol", "v-view-size", "v-single-mode"], data: function () { let certs = this.vCerts if (certs == null) { @@ -26,9 +26,19 @@ Vue.component("ssl-certs-box", { // 选择证书 selectCert: function () { let that = this - teaweb.popup("/servers/components/ssl/selectPopup", { - width: "50em", - height: "30em", + let width = "50em" + let height = "30em" + let viewSize = this.vViewSize + if (viewSize == null) { + viewSize = "normal" + } + if (viewSize == "mini") { + width = "35em" + height = "20em" + } + teaweb.popup("/servers/components/ssl/selectPopup?viewSize=" + viewSize, { + width: width, + height: height, callback: function (resp) { that.certs.push(resp.data.cert) } @@ -51,6 +61,11 @@ Vue.component("ssl-certs-box", { // 格式化时间 formatTime: function (timestamp) { return new Date(timestamp * 1000).format("Y-m-d") + }, + + // 判断是否显示选择|上传按钮 + buttonsVisible: function () { + return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0 } }, template: `