mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-10 01:10:29 +08:00
[用户节点]可以管理用户节点
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o edge-admin main.go
|
|
||||||
Binary file not shown.
@@ -9,13 +9,13 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sharedUIConfig *systemconfigs.UIConfig = nil
|
var sharedUIConfig *systemconfigs.AdminUIConfig = nil
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UISettingName = "adminUIConfig"
|
UISettingName = "adminUIConfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadUIConfig() (*systemconfigs.UIConfig, error) {
|
func LoadUIConfig() (*systemconfigs.AdminUIConfig, error) {
|
||||||
locker.Lock()
|
locker.Lock()
|
||||||
defer locker.Unlock()
|
defer locker.Unlock()
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ func LoadUIConfig() (*systemconfigs.UIConfig, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.Indirect(reflect.ValueOf(config)).Interface().(systemconfigs.UIConfig)
|
v := reflect.Indirect(reflect.ValueOf(config)).Interface().(systemconfigs.AdminUIConfig)
|
||||||
return &v, nil
|
return &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUIConfig(uiConfig *systemconfigs.UIConfig) error {
|
func UpdateUIConfig(uiConfig *systemconfigs.AdminUIConfig) error {
|
||||||
locker.Lock()
|
locker.Lock()
|
||||||
defer locker.Unlock()
|
defer locker.Unlock()
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ func UpdateUIConfig(uiConfig *systemconfigs.UIConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUIConfig() (*systemconfigs.UIConfig, error) {
|
func loadUIConfig() (*systemconfigs.AdminUIConfig, error) {
|
||||||
if sharedUIConfig != nil {
|
if sharedUIConfig != nil {
|
||||||
return sharedUIConfig, nil
|
return sharedUIConfig, nil
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ func loadUIConfig() (*systemconfigs.UIConfig, error) {
|
|||||||
return sharedUIConfig, nil
|
return sharedUIConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &systemconfigs.UIConfig{}
|
config := &systemconfigs.AdminUIConfig{}
|
||||||
err = json.Unmarshal(resp.ValueJSON, config)
|
err = json.Unmarshal(resp.ValueJSON, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logs.Println("[UI_MANAGER]" + err.Error())
|
logs.Println("[UI_MANAGER]" + err.Error())
|
||||||
@@ -82,8 +82,8 @@ func loadUIConfig() (*systemconfigs.UIConfig, error) {
|
|||||||
return sharedUIConfig, nil
|
return sharedUIConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultUIConfig() *systemconfigs.UIConfig {
|
func defaultUIConfig() *systemconfigs.AdminUIConfig {
|
||||||
return &systemconfigs.UIConfig{
|
return &systemconfigs.AdminUIConfig{
|
||||||
ProductName: "GoEdge",
|
ProductName: "GoEdge",
|
||||||
AdminSystemName: "GoEdge管理员系统",
|
AdminSystemName: "GoEdge管理员系统",
|
||||||
ShowOpenSourceInfo: true,
|
ShowOpenSourceInfo: true,
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ func (this *AdminNode) Run() {
|
|||||||
// 检查server配置
|
// 检查server配置
|
||||||
err := this.checkServer()
|
err := this.checkServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err != nil {
|
||||||
|
logs.Println("[NODE]" + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +62,7 @@ func (this *AdminNode) Run() {
|
|||||||
// 启动API节点
|
// 启动API节点
|
||||||
this.startAPINode()
|
this.startAPINode()
|
||||||
|
|
||||||
|
// 启动Web服务
|
||||||
TeaGo.NewServer(false).
|
TeaGo.NewServer(false).
|
||||||
AccessLog(false).
|
AccessLog(false).
|
||||||
EndAll().
|
EndAll().
|
||||||
@@ -129,7 +134,7 @@ func (this *AdminNode) startAPINode() {
|
|||||||
|
|
||||||
// 生成Secret
|
// 生成Secret
|
||||||
func (this *AdminNode) genSecret() string {
|
func (this *AdminNode) genSecret() string {
|
||||||
tmpFile := os.TempDir() + "/edge-secret.tmp"
|
tmpFile := os.TempDir() + "/edge-admin-secret.tmp"
|
||||||
data, err := ioutil.ReadFile(tmpFile)
|
data, err := ioutil.ReadFile(tmpFile)
|
||||||
if err == nil && len(data) == 32 {
|
if err == nil && len(data) == 32 {
|
||||||
return string(data)
|
return string(data)
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ func (this *RPCClient) APINodeRPC() pb.APINodeServiceClient {
|
|||||||
return pb.NewAPINodeServiceClient(this.pickConn())
|
return pb.NewAPINodeServiceClient(this.pickConn())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *RPCClient) UserNodeRPC() pb.UserNodeServiceClient {
|
||||||
|
return pb.NewUserNodeServiceClient(this.pickConn())
|
||||||
|
}
|
||||||
|
|
||||||
func (this *RPCClient) DBNodeRPC() pb.DBNodeServiceClient {
|
func (this *RPCClient) DBNodeRPC() pb.DBNodeServiceClient {
|
||||||
return pb.NewDBNodeServiceClient(this.pickConn())
|
return pb.NewDBNodeServiceClient(this.pickConn())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func init() {
|
|||||||
server.
|
server.
|
||||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
||||||
Helper(NewHelper()).
|
Helper(NewHelper()).
|
||||||
Helper(settingutils.NewHelper("apiNodes")).
|
Helper(settingutils.NewAdvancedHelper("apiNodes")).
|
||||||
Prefix("/api").
|
Prefix("/api").
|
||||||
Get("", new(IndexAction)).
|
Get("", new(IndexAction)).
|
||||||
GetPost("/node/createPopup", new(node.CreatePopupAction)).
|
GetPost("/node/createPopup", new(node.CreatePopupAction)).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func init() {
|
|||||||
server.
|
server.
|
||||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
||||||
Helper(new(Helper)).
|
Helper(new(Helper)).
|
||||||
Helper(settingutils.NewHelper("dbNodes")).
|
Helper(settingutils.NewAdvancedHelper("dbNodes")).
|
||||||
Prefix("/db").
|
Prefix("/db").
|
||||||
Get("", new(IndexAction)).
|
Get("", new(IndexAction)).
|
||||||
GetPost("/createPopup", new(CreatePopupAction)).
|
GetPost("/createPopup", new(CreatePopupAction)).
|
||||||
|
|||||||
16
internal/web/actions/default/settings/advanced.go
Normal file
16
internal/web/actions/default/settings/advanced.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
|
||||||
|
type AdvancedAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *AdvancedAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *AdvancedAction) RunGet(params struct{}) {
|
||||||
|
// 跳转到高级设置的第一个Tab
|
||||||
|
this.RedirectURL("/settings/database")
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ func init() {
|
|||||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||||
server.
|
server.
|
||||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
||||||
Helper(settingutils.NewHelper("database")).
|
Helper(settingutils.NewAdvancedHelper("database")).
|
||||||
Prefix("/settings/database").
|
Prefix("/settings/database").
|
||||||
Get("", new(IndexAction)).
|
Get("", new(IndexAction)).
|
||||||
GetPost("/update", new(UpdateAction)).
|
GetPost("/update", new(UpdateAction)).
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func init() {
|
|||||||
Helper(NewHelper()).
|
Helper(NewHelper()).
|
||||||
Prefix("/settings").
|
Prefix("/settings").
|
||||||
Get("", new(IndexAction)).
|
Get("", new(IndexAction)).
|
||||||
|
Get("/advanced", new(AdvancedAction)).
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package settingutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdvancedHelper struct {
|
||||||
|
tab string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdvancedHelper(tab string) *AdvancedHelper {
|
||||||
|
return &AdvancedHelper{
|
||||||
|
tab: tab,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *AdvancedHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
|
||||||
|
goNext = true
|
||||||
|
|
||||||
|
action := actionPtr.Object()
|
||||||
|
|
||||||
|
// 左侧菜单
|
||||||
|
action.Data["teaMenu"] = "settings"
|
||||||
|
action.Data["teaSubMenu"] = "advanced"
|
||||||
|
|
||||||
|
// 标签栏
|
||||||
|
tabbar := actionutils.NewTabbar()
|
||||||
|
var session = action.Session()
|
||||||
|
var adminId = session.GetInt64("adminId")
|
||||||
|
if configloaders.AllowModule(adminId, configloaders.AdminModuleCodeSetting) {
|
||||||
|
tabbar.Add("数据库", "", "/settings/database", "", this.tab == "database")
|
||||||
|
tabbar.Add("API节点", "", "/api", "", this.tab == "apiNodes")
|
||||||
|
tabbar.Add("用户节点", "", "/settings/userNodes", "", this.tab == "userNodes")
|
||||||
|
tabbar.Add("日志数据库", "", "/db", "", this.tab == "dbNodes")
|
||||||
|
}
|
||||||
|
actionutils.SetTabbar(actionPtr, tabbar)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -32,9 +32,6 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
|
|||||||
tabbar.Add("Web服务", "", "/settings/server", "", this.tab == "server")
|
tabbar.Add("Web服务", "", "/settings/server", "", this.tab == "server")
|
||||||
tabbar.Add("界面设置", "", "/settings/ui", "", this.tab == "ui")
|
tabbar.Add("界面设置", "", "/settings/ui", "", this.tab == "ui")
|
||||||
tabbar.Add("安全设置", "", "/settings/security", "", this.tab == "security")
|
tabbar.Add("安全设置", "", "/settings/security", "", this.tab == "security")
|
||||||
tabbar.Add("数据库", "", "/settings/database", "", this.tab == "database")
|
|
||||||
tabbar.Add("API节点", "", "/api", "", this.tab == "apiNodes")
|
|
||||||
tabbar.Add("日志数据库", "", "/db", "", this.tab == "dbNodes")
|
|
||||||
tabbar.Add("IP库", "", "/settings/ip-library", "", this.tab == "ipLibrary")
|
tabbar.Add("IP库", "", "/settings/ip-library", "", this.tab == "ipLibrary")
|
||||||
tabbar.Add("备份", "", "/settings/backup", "", this.tab == "backup")
|
tabbar.Add("备份", "", "/settings/backup", "", this.tab == "backup")
|
||||||
}
|
}
|
||||||
28
internal/web/actions/default/settings/user-nodes/delete.go
Normal file
28
internal/web/actions/default/settings/user-nodes/delete.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package usernodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *DeleteAction) RunPost(params struct {
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
// TODO 检查权限
|
||||||
|
|
||||||
|
_, err := this.RPC().UserNodeRPC().DeleteUserNode(this.AdminContext(), &pb.DeleteUserNodeRequest{NodeId: params.NodeId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志
|
||||||
|
defer this.CreateLog(oplogs.LevelInfo, "删除用户节点 %d", params.NodeId)
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
15
internal/web/actions/default/settings/user-nodes/helper.go
Normal file
15
internal/web/actions/default/settings/user-nodes/helper.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package usernodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Helper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHelper() *Helper {
|
||||||
|
return &Helper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Helper) BeforeAction(action *actions.ActionObject) {
|
||||||
|
}
|
||||||
50
internal/web/actions/default/settings/user-nodes/index.go
Normal file
50
internal/web/actions/default/settings/user-nodes/index.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package usernodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *IndexAction) Init() {
|
||||||
|
this.Nav("", "node", "index")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *IndexAction) RunGet(params struct{}) {
|
||||||
|
countResp, err := this.RPC().UserNodeRPC().CountAllEnabledUserNodes(this.AdminContext(), &pb.CountAllEnabledUserNodesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count := countResp.Count
|
||||||
|
page := this.NewPage(count)
|
||||||
|
this.Data["page"] = page.AsHTML()
|
||||||
|
|
||||||
|
nodeMaps := []maps.Map{}
|
||||||
|
if count > 0 {
|
||||||
|
nodesResp, err := this.RPC().UserNodeRPC().ListEnabledUserNodes(this.AdminContext(), &pb.ListEnabledUserNodesRequest{
|
||||||
|
Offset: page.Offset,
|
||||||
|
Size: page.Size,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range nodesResp.Nodes {
|
||||||
|
nodeMaps = append(nodeMaps, maps.Map{
|
||||||
|
"id": node.Id,
|
||||||
|
"isOn": node.IsOn,
|
||||||
|
"name": node.Name,
|
||||||
|
"accessAddrs": node.AccessAddrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.Data["nodes"] = nodeMaps
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
23
internal/web/actions/default/settings/user-nodes/init.go
Normal file
23
internal/web/actions/default/settings/user-nodes/init.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package usernodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/settingutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/user-nodes/node"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||||
|
"github.com/iwind/TeaGo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||||
|
server.
|
||||||
|
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
||||||
|
Helper(NewHelper()).
|
||||||
|
Helper(settingutils.NewAdvancedHelper("userNodes")).
|
||||||
|
Prefix("/settings/userNodes").
|
||||||
|
Get("", new(IndexAction)).
|
||||||
|
GetPost("/node/createPopup", new(node.CreatePopupAction)).
|
||||||
|
Post("/delete", new(DeleteAction)).
|
||||||
|
EndAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 添加地址
|
||||||
|
type CreateAddrPopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreateAddrPopupAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreateAddrPopupAction) RunGet(params struct {
|
||||||
|
}) {
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreateAddrPopupAction) RunPost(params struct {
|
||||||
|
Protocol string
|
||||||
|
Addr string
|
||||||
|
Must *actions.Must
|
||||||
|
}) {
|
||||||
|
params.Must.
|
||||||
|
Field("addr", params.Addr).
|
||||||
|
Require("请输入访问地址")
|
||||||
|
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(params.Addr)
|
||||||
|
if err != nil {
|
||||||
|
this.FailField("addr", "错误的访问地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
addrConfig := &serverconfigs.NetworkAddressConfig{
|
||||||
|
Protocol: serverconfigs.Protocol(params.Protocol),
|
||||||
|
Host: host,
|
||||||
|
PortRange: port,
|
||||||
|
}
|
||||||
|
this.Data["addr"] = addrConfig
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreatePopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreatePopupAction) Init() {
|
||||||
|
this.Nav("", "node", "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreatePopupAction) RunGet(params struct{}) {
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *CreatePopupAction) RunPost(params struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
ListensJSON []byte
|
||||||
|
CertIdsJSON []byte
|
||||||
|
AccessAddrsJSON []byte
|
||||||
|
IsOn bool
|
||||||
|
|
||||||
|
Must *actions.Must
|
||||||
|
}) {
|
||||||
|
params.Must.
|
||||||
|
Field("name", params.Name).
|
||||||
|
Require("请输入用户节点名称")
|
||||||
|
|
||||||
|
httpConfig := &serverconfigs.HTTPProtocolConfig{}
|
||||||
|
httpsConfig := &serverconfigs.HTTPSProtocolConfig{}
|
||||||
|
|
||||||
|
// 监听地址
|
||||||
|
listens := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
err := json.Unmarshal(params.ListensJSON, &listens)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(listens) == 0 {
|
||||||
|
this.Fail("请添加至少一个进程监听地址")
|
||||||
|
}
|
||||||
|
for _, addr := range listens {
|
||||||
|
if addr.Protocol.IsHTTPFamily() {
|
||||||
|
httpConfig.IsOn = true
|
||||||
|
httpConfig.Listen = append(httpConfig.Listen, addr)
|
||||||
|
} else if addr.Protocol.IsHTTPSFamily() {
|
||||||
|
httpsConfig.IsOn = true
|
||||||
|
httpsConfig.Listen = append(httpsConfig.Listen, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 证书
|
||||||
|
certIds := []int64{}
|
||||||
|
if len(params.CertIdsJSON) > 0 {
|
||||||
|
err = json.Unmarshal(params.CertIdsJSON, &certIds)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if httpsConfig.IsOn && len(httpsConfig.Listen) > 0 && len(certIds) == 0 {
|
||||||
|
this.Fail("请添加至少一个证书")
|
||||||
|
}
|
||||||
|
|
||||||
|
certRefs := []*sslconfigs.SSLCertRef{}
|
||||||
|
for _, certId := range certIds {
|
||||||
|
certRefs = append(certRefs, &sslconfigs.SSLCertRef{
|
||||||
|
IsOn: true,
|
||||||
|
CertId: certId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
certRefsJSON, err := json.Marshal(certRefs)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建策略
|
||||||
|
if len(certIds) > 0 {
|
||||||
|
sslPolicyCreateResp, err := this.RPC().SSLPolicyRPC().CreateSSLPolicy(this.AdminContext(), &pb.CreateSSLPolicyRequest{
|
||||||
|
CertsJSON: certRefsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sslPolicyId := sslPolicyCreateResp.SslPolicyId
|
||||||
|
httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{
|
||||||
|
IsOn: true,
|
||||||
|
SSLPolicyId: sslPolicyId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 访问地址
|
||||||
|
accessAddrs := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
err = json.Unmarshal(params.AccessAddrsJSON, &accessAddrs)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(accessAddrs) == 0 {
|
||||||
|
this.Fail("请添加至少一个外部访问地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpJSON, err := json.Marshal(httpConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpsJSON, err := json.Marshal(httpsConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createResp, err := this.RPC().UserNodeRPC().CreateUserNode(this.AdminContext(), &pb.CreateUserNodeRequest{
|
||||||
|
Name: params.Name,
|
||||||
|
Description: params.Description,
|
||||||
|
HttpJSON: httpJSON,
|
||||||
|
HttpsJSON: httpsJSON,
|
||||||
|
AccessAddrsJSON: params.AccessAddrsJSON,
|
||||||
|
IsOn: params.IsOn,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志
|
||||||
|
defer this.CreateLog(oplogs.LevelInfo, "创建用户节点 %d", createResp.NodeId)
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Helper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHelper() *Helper {
|
||||||
|
return &Helper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Helper) BeforeAction(action *actions.ActionObject) (goNext bool) {
|
||||||
|
if action.Request.Method != http.MethodGet {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
102
internal/web/actions/default/settings/user-nodes/node/index.go
Normal file
102
internal/web/actions/default/settings/user-nodes/node/index.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *IndexAction) Init() {
|
||||||
|
this.Nav("", "", "index")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *IndexAction) RunGet(params struct {
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
nodeResp, err := this.RPC().UserNodeRPC().FindEnabledUserNode(this.AdminContext(), &pb.FindEnabledUserNodeRequest{NodeId: params.NodeId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node := nodeResp.Node
|
||||||
|
if node == nil {
|
||||||
|
this.NotFound("userNode", params.NodeId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听地址
|
||||||
|
httpConfig := &serverconfigs.HTTPProtocolConfig{}
|
||||||
|
if len(node.HttpJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.HttpJSON, httpConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpsConfig := &serverconfigs.HTTPSProtocolConfig{}
|
||||||
|
if len(node.HttpsJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.HttpsJSON, httpsConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听地址
|
||||||
|
listens := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
listens = append(listens, httpConfig.Listen...)
|
||||||
|
listens = append(listens, httpsConfig.Listen...)
|
||||||
|
|
||||||
|
// 证书信息
|
||||||
|
certs := []*sslconfigs.SSLCertConfig{}
|
||||||
|
sslPolicyId := int64(0)
|
||||||
|
if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 {
|
||||||
|
sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sslPolicyConfigJSON := sslPolicyConfigResp.SslPolicyJSON
|
||||||
|
if len(sslPolicyConfigJSON) > 0 {
|
||||||
|
sslPolicyId = httpsConfig.SSLPolicyRef.SSLPolicyId
|
||||||
|
|
||||||
|
sslPolicy := &sslconfigs.SSLPolicy{}
|
||||||
|
err = json.Unmarshal(sslPolicyConfigJSON, sslPolicy)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
certs = sslPolicy.Certs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 访问地址
|
||||||
|
accessAddrs := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
if len(node.AccessAddrsJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.AccessAddrsJSON, &accessAddrs)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["node"] = maps.Map{
|
||||||
|
"id": node.Id,
|
||||||
|
"name": node.Name,
|
||||||
|
"description": node.Description,
|
||||||
|
"isOn": node.IsOn,
|
||||||
|
"listens": listens,
|
||||||
|
"accessAddrs": accessAddrs,
|
||||||
|
"hasHTTPS": sslPolicyId > 0,
|
||||||
|
"certs": certs,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||||
|
"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(configloaders.AdminModuleCodeSetting)).
|
||||||
|
Helper(settingutils.NewAdvancedHelper("userNodes")).
|
||||||
|
Prefix("/settings/userNodes/node").
|
||||||
|
|
||||||
|
// 这里不受Helper的约束
|
||||||
|
GetPost("/createAddrPopup", new(CreateAddrPopupAction)).
|
||||||
|
GetPost("/updateAddrPopup", new(UpdateAddrPopupAction)).
|
||||||
|
|
||||||
|
// 节点相关
|
||||||
|
Helper(NewHelper()).
|
||||||
|
Get("", new(IndexAction)).
|
||||||
|
GetPost("/update", new(UpdateAction)).
|
||||||
|
Get("/install", new(InstallAction)).
|
||||||
|
|
||||||
|
EndAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstallAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallAction) Init() {
|
||||||
|
this.Nav("", "", "install")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallAction) RunGet(params struct {
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
// 用户节点信息
|
||||||
|
nodeResp, err := this.RPC().UserNodeRPC().FindEnabledUserNode(this.AdminContext(), &pb.FindEnabledUserNodeRequest{NodeId: params.NodeId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node := nodeResp.Node
|
||||||
|
if node == nil {
|
||||||
|
this.NotFound("userNode", params.NodeId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["node"] = maps.Map{
|
||||||
|
"id": node.Id,
|
||||||
|
"name": node.Name,
|
||||||
|
"uniqueId": node.UniqueId,
|
||||||
|
"secret": node.Secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
// API节点列表
|
||||||
|
apiNodesResp, err := this.RPC().APINodeRPC().FindAllEnabledAPINodes(this.AdminContext(), &pb.FindAllEnabledAPINodesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiNodes := apiNodesResp.Nodes
|
||||||
|
apiEndpoints := []string{}
|
||||||
|
for _, apiNode := range apiNodes {
|
||||||
|
if !apiNode.IsOn {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiEndpoints = append(apiEndpoints, apiNode.AccessAddrs...)
|
||||||
|
}
|
||||||
|
this.Data["apiEndpoints"] = "\"" + strings.Join(apiEndpoints, "\", \"") + "\""
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
240
internal/web/actions/default/settings/user-nodes/node/update.go
Normal file
240
internal/web/actions/default/settings/user-nodes/node/update.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdateAction) Init() {
|
||||||
|
this.Nav("", "", "update")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdateAction) RunGet(params struct {
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
nodeResp, err := this.RPC().UserNodeRPC().FindEnabledUserNode(this.AdminContext(), &pb.FindEnabledUserNodeRequest{
|
||||||
|
NodeId: params.NodeId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node := nodeResp.Node
|
||||||
|
if node == nil {
|
||||||
|
this.WriteString("要操作的节点不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConfig := &serverconfigs.HTTPProtocolConfig{}
|
||||||
|
if len(node.HttpJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.HttpJSON, httpConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpsConfig := &serverconfigs.HTTPSProtocolConfig{}
|
||||||
|
if len(node.HttpsJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.HttpsJSON, httpsConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听地址
|
||||||
|
listens := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
listens = append(listens, httpConfig.Listen...)
|
||||||
|
listens = append(listens, httpsConfig.Listen...)
|
||||||
|
|
||||||
|
// 证书信息
|
||||||
|
certs := []*sslconfigs.SSLCertConfig{}
|
||||||
|
sslPolicyId := int64(0)
|
||||||
|
if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 {
|
||||||
|
sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sslPolicyConfigJSON := sslPolicyConfigResp.SslPolicyJSON
|
||||||
|
if len(sslPolicyConfigJSON) > 0 {
|
||||||
|
sslPolicyId = httpsConfig.SSLPolicyRef.SSLPolicyId
|
||||||
|
|
||||||
|
sslPolicy := &sslconfigs.SSLPolicy{}
|
||||||
|
err = json.Unmarshal(sslPolicyConfigJSON, sslPolicy)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
certs = sslPolicy.Certs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accessAddrs := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
if len(node.AccessAddrsJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.AccessAddrsJSON, &accessAddrs)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["node"] = maps.Map{
|
||||||
|
"id": node.Id,
|
||||||
|
"name": node.Name,
|
||||||
|
"description": node.Description,
|
||||||
|
"isOn": node.IsOn,
|
||||||
|
"listens": listens,
|
||||||
|
"certs": certs,
|
||||||
|
"sslPolicyId": sslPolicyId,
|
||||||
|
"accessAddrs": accessAddrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存基础设置
|
||||||
|
func (this *UpdateAction) RunPost(params struct {
|
||||||
|
NodeId int64
|
||||||
|
Name string
|
||||||
|
SslPolicyId int64
|
||||||
|
ListensJSON []byte
|
||||||
|
CertIdsJSON []byte
|
||||||
|
AccessAddrsJSON []byte
|
||||||
|
Description string
|
||||||
|
IsOn bool
|
||||||
|
|
||||||
|
Must *actions.Must
|
||||||
|
}) {
|
||||||
|
params.Must.
|
||||||
|
Field("name", params.Name).
|
||||||
|
Require("请输入用户节点名称")
|
||||||
|
|
||||||
|
httpConfig := &serverconfigs.HTTPProtocolConfig{}
|
||||||
|
httpsConfig := &serverconfigs.HTTPSProtocolConfig{}
|
||||||
|
|
||||||
|
// 监听地址
|
||||||
|
listens := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
err := json.Unmarshal(params.ListensJSON, &listens)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(listens) == 0 {
|
||||||
|
this.Fail("请添加至少一个进程监听地址")
|
||||||
|
}
|
||||||
|
for _, addr := range listens {
|
||||||
|
if addr.Protocol.IsHTTPFamily() {
|
||||||
|
httpConfig.IsOn = true
|
||||||
|
httpConfig.Listen = append(httpConfig.Listen, addr)
|
||||||
|
} else if addr.Protocol.IsHTTPSFamily() {
|
||||||
|
httpsConfig.IsOn = true
|
||||||
|
httpsConfig.Listen = append(httpsConfig.Listen, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 证书
|
||||||
|
certIds := []int64{}
|
||||||
|
if len(params.CertIdsJSON) > 0 {
|
||||||
|
err = json.Unmarshal(params.CertIdsJSON, &certIds)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if httpsConfig.IsOn && len(httpsConfig.Listen) > 0 && len(certIds) == 0 {
|
||||||
|
this.Fail("请添加至少一个证书")
|
||||||
|
}
|
||||||
|
|
||||||
|
certRefs := []*sslconfigs.SSLCertRef{}
|
||||||
|
for _, certId := range certIds {
|
||||||
|
certRefs = append(certRefs, &sslconfigs.SSLCertRef{
|
||||||
|
IsOn: true,
|
||||||
|
CertId: certId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
certRefsJSON, err := json.Marshal(certRefs)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建策略
|
||||||
|
sslPolicyId := params.SslPolicyId
|
||||||
|
if sslPolicyId == 0 {
|
||||||
|
if len(certIds) > 0 {
|
||||||
|
sslPolicyCreateResp, err := this.RPC().SSLPolicyRPC().CreateSSLPolicy(this.AdminContext(), &pb.CreateSSLPolicyRequest{
|
||||||
|
CertsJSON: certRefsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sslPolicyId = sslPolicyCreateResp.SslPolicyId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = this.RPC().SSLPolicyRPC().UpdateSSLPolicy(this.AdminContext(), &pb.UpdateSSLPolicyRequest{
|
||||||
|
SslPolicyId: sslPolicyId,
|
||||||
|
CertsJSON: certRefsJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{
|
||||||
|
IsOn: true,
|
||||||
|
SSLPolicyId: sslPolicyId,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 访问地址
|
||||||
|
accessAddrs := []*serverconfigs.NetworkAddressConfig{}
|
||||||
|
err = json.Unmarshal(params.AccessAddrsJSON, &accessAddrs)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(accessAddrs) == 0 {
|
||||||
|
this.Fail("请添加至少一个外部访问地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpJSON, err := json.Marshal(httpConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpsJSON, err := json.Marshal(httpsConfig)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = this.RPC().UserNodeRPC().UpdateUserNode(this.AdminContext(), &pb.UpdateUserNodeRequest{
|
||||||
|
NodeId: params.NodeId,
|
||||||
|
Name: params.Name,
|
||||||
|
Description: params.Description,
|
||||||
|
HttpJSON: httpJSON,
|
||||||
|
HttpsJSON: httpsJSON,
|
||||||
|
AccessAddrsJSON: params.AccessAddrsJSON,
|
||||||
|
IsOn: params.IsOn,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志
|
||||||
|
defer this.CreateLog(oplogs.LevelInfo, "修改用户节点 %d", params.NodeId)
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateAddrPopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdateAddrPopupAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdateAddrPopupAction) RunGet(params struct{}) {
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpdateAddrPopupAction) RunPost(params struct {
|
||||||
|
Protocol string
|
||||||
|
Addr string
|
||||||
|
Must *actions.Must
|
||||||
|
}) {
|
||||||
|
params.Must.
|
||||||
|
Field("addr", params.Addr).
|
||||||
|
Require("请输入访问地址")
|
||||||
|
host, port, err := net.SplitHostPort(params.Addr)
|
||||||
|
if err != nil {
|
||||||
|
this.FailField("addr", "错误的访问地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
addrConfig := &serverconfigs.NetworkAddressConfig{
|
||||||
|
Protocol: serverconfigs.Protocol(params.Protocol),
|
||||||
|
Host: host,
|
||||||
|
PortRange: port,
|
||||||
|
}
|
||||||
|
this.Data["addr"] = addrConfig
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -226,6 +226,13 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map {
|
|||||||
"module": configloaders.AdminModuleCodeSetting,
|
"module": configloaders.AdminModuleCodeSetting,
|
||||||
"name": "系统设置",
|
"name": "系统设置",
|
||||||
"icon": "setting",
|
"icon": "setting",
|
||||||
|
"subItems": []maps.Map{
|
||||||
|
{
|
||||||
|
"name": "高级设置",
|
||||||
|
"url": "/settings/advanced",
|
||||||
|
"code": "advanced",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import (
|
|||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/ui"
|
_ "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/settings/upgrade"
|
||||||
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/user-nodes"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ui"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ui"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Vue.component("network-addresses-view", {
|
|||||||
props: ["v-addresses"],
|
props: ["v-addresses"],
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<div class="ui label tiny basic" v-if="vAddresses != null" v-for="addr in vAddresses">
|
<div class="ui label tiny basic" v-if="vAddresses != null" v-for="addr in vAddresses">
|
||||||
{{addr.protocol}}://{{addr.host}}:{{addr.portRange}}
|
{{addr.protocol}}://<span v-if="addr.host.length > 0">{{addr.host}}</span><span v-else>*</span>:{{addr.portRange}}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
})
|
})
|
||||||
35
web/views/@default/settings/user-nodes/index.html
Normal file
35
web/views/@default/settings/user-nodes/index.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{$layout}
|
||||||
|
|
||||||
|
<first-menu>
|
||||||
|
<a href="" class="item" @click.prevent="createNode()">[添加节点]</a>
|
||||||
|
</first-menu>
|
||||||
|
|
||||||
|
<p class="comment" v-if="nodes.length == 0">暂时还没有节点。</p>
|
||||||
|
|
||||||
|
<table class="ui table selectable celled" v-if="nodes.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>节点名称</th>
|
||||||
|
<th>访问地址</th>
|
||||||
|
<th class="center width10">状态</th>
|
||||||
|
<th class="two op">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr v-for="node in nodes">
|
||||||
|
<td>{{node.name}}</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="node.accessAddrs != null && node.accessAddrs.length > 0">
|
||||||
|
<span class="ui label tiny basic" v-for="addr in node.accessAddrs">{{addr}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="center">
|
||||||
|
<label-on :v-is-on="node.isOn"></label-on>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a :href="'/settings/userNodes/node?nodeId=' + node.id">详情</a>
|
||||||
|
<a href="" @click.prevent="deleteNode(node.id)">删除</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="page" v-html="page"></div>
|
||||||
26
web/views/@default/settings/user-nodes/index.js
Normal file
26
web/views/@default/settings/user-nodes/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
// 创建节点
|
||||||
|
this.createNode = function () {
|
||||||
|
teaweb.popup("/settings/userNodes/node/createPopup", {
|
||||||
|
width: "50em",
|
||||||
|
height: "30em",
|
||||||
|
callback: function () {
|
||||||
|
teaweb.success("保存成功", function () {
|
||||||
|
teaweb.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除节点
|
||||||
|
this.deleteNode = function (nodeId) {
|
||||||
|
let that = this
|
||||||
|
teaweb.confirm("确定要删除此节点吗?", function () {
|
||||||
|
that.$post("/settings/userNodes/delete")
|
||||||
|
.params({
|
||||||
|
nodeId: nodeId
|
||||||
|
})
|
||||||
|
.refresh()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
7
web/views/@default/settings/user-nodes/node/@menu.html
Normal file
7
web/views/@default/settings/user-nodes/node/@menu.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<first-menu>
|
||||||
|
<menu-item href="/settings/userNodes">节点列表</menu-item>
|
||||||
|
<span class="item">|</span>
|
||||||
|
<menu-item :href="'/settings/userNodes/node?nodeId=' + node.id" code="index">"{{node.name}}"详情</menu-item>
|
||||||
|
<menu-item :href="'/settings/userNodes/node/install?nodeId=' + node.id" code="install">安装节点</menu-item>
|
||||||
|
<menu-item :href="'/settings/userNodes/node/update?nodeId=' + node.id" code="update">修改节点</menu-item>
|
||||||
|
</first-menu>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{$layout "layout_popup"}
|
||||||
|
|
||||||
|
<h3>添加访问地址</h3>
|
||||||
|
<form class="ui form" method="post" data-tea-action="$" data-tea-success="success">
|
||||||
|
<table class="ui table definition selectable">
|
||||||
|
<tr>
|
||||||
|
<td>网络协议</td>
|
||||||
|
<td>
|
||||||
|
<select class="ui dropdown auto-width" name="protocol">
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="https">HTTPS</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="title">访问地址</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="addr" maxlength="100" ref="focus"/>
|
||||||
|
<p class="comment">可以是"IP:端口"或者"域名:端口"。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<submit-btn></submit-btn>
|
||||||
|
</form>
|
||||||
55
web/views/@default/settings/user-nodes/node/createPopup.html
Normal file
55
web/views/@default/settings/user-nodes/node/createPopup.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{$layout "layout_popup"}
|
||||||
|
|
||||||
|
<h3>添加用户节点</h3>
|
||||||
|
<form class="ui form" method="post" data-tea-action="$" data-tea-success="success">
|
||||||
|
<table class="ui table selectable definition">
|
||||||
|
<tr>
|
||||||
|
<td class="title">节点名称 *</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="name" maxlength="100" ref="focus"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>进程监听端口 *</td>
|
||||||
|
<td>
|
||||||
|
<network-addresses-box :v-name="'listensJSON'" :v-server-type="'httpWeb'" @change="changeListens"></network-addresses-box>
|
||||||
|
<p class="comment">用户节点进程监听的网络端口。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="hasHTTPS">
|
||||||
|
<td>HTTPS证书 *</td>
|
||||||
|
<td>
|
||||||
|
<ssl-certs-box :v-protocol="'https'"></ssl-certs-box>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>外部访问地址 *</td>
|
||||||
|
<td>
|
||||||
|
<api-node-addresses-box :v-name="'accessAddrsJSON'"></api-node-addresses-box>
|
||||||
|
<p class="comment">可以公开访问的网络地址。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><more-options-indicator></more-options-indicator></td>
|
||||||
|
</tr>
|
||||||
|
<tbody v-show="moreOptionsVisible">
|
||||||
|
<tr>
|
||||||
|
<td>描述</td>
|
||||||
|
<td>
|
||||||
|
<textarea name="description" maxlength="200" rows="3"></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>是否启用</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" name="isOn" value="1" checked="checked"/>
|
||||||
|
<label></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<submit-btn></submit-btn>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
this.hasHTTPS = false
|
||||||
|
this.changeListens = function (addrs) {
|
||||||
|
this.hasHTTPS = addrs.$any(function (k, v) {
|
||||||
|
return v.protocol == "https"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
44
web/views/@default/settings/user-nodes/node/index.html
Normal file
44
web/views/@default/settings/user-nodes/node/index.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{$layout}
|
||||||
|
{$template "menu"}
|
||||||
|
|
||||||
|
<table class="ui table selectable definition">
|
||||||
|
<tr>
|
||||||
|
<td class="title">节点名称</td>
|
||||||
|
<td>
|
||||||
|
{{node.name}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>状态</td>
|
||||||
|
<td>
|
||||||
|
<label-on :v-is-on="node.isOn"></label-on>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>进程监听端口</td>
|
||||||
|
<td>
|
||||||
|
<network-addresses-view :v-addresses="node.listens"></network-addresses-view>
|
||||||
|
<p class="comment">用户节点进程监听的网络端口。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="node.hasHTTPS">
|
||||||
|
<td>HTTPS证书</td>
|
||||||
|
<td>
|
||||||
|
<ssl-certs-view :v-certs="node.certs"></ssl-certs-view>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>外部访问地址</td>
|
||||||
|
<td>
|
||||||
|
<network-addresses-view :v-addresses="node.accessAddrs"></network-addresses-view>
|
||||||
|
<p class="comment">可以公开访问的网络地址。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>描述</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="node.description.length > 0">{{node.description}}</span>
|
||||||
|
<span v-else class="disabled">暂时还没有描述。</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
23
web/views/@default/settings/user-nodes/node/install.html
Normal file
23
web/views/@default/settings/user-nodes/node/install.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{$layout}
|
||||||
|
{$template "menu"}
|
||||||
|
|
||||||
|
<h3>安装步骤</h3>
|
||||||
|
<ol class="ui list">
|
||||||
|
<li>按照下面的配置信息替换<code-label>configs/api.yaml</code-label>内容</li>
|
||||||
|
<li>使用<code-label>bin/edge-user start</code-label>启动节点</li>
|
||||||
|
<li>可以在<code-label>logs/run.log</code-label>中查看启动是否有异常</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<h3>配置信息</h3>
|
||||||
|
<table class="ui table definition selectable">
|
||||||
|
<tr>
|
||||||
|
<td class="title">configs/api.yaml<em><br/><download-link :v-element="'api-code'" :v-file="'api.yaml'">[下载]</download-link></em></td>
|
||||||
|
<td>
|
||||||
|
<pre id="api-code">rpc:
|
||||||
|
endpoints: [ {{apiEndpoints}} ]
|
||||||
|
nodeId: "{{node.uniqueId}}"
|
||||||
|
secret: "{{node.secret}}"</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
58
web/views/@default/settings/user-nodes/node/update.html
Normal file
58
web/views/@default/settings/user-nodes/node/update.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{$layout}
|
||||||
|
|
||||||
|
{$template "menu"}
|
||||||
|
|
||||||
|
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||||
|
<input type="hidden" name="nodeId" :value="node.id"/>
|
||||||
|
<input type="hidden" name="sslPolicyId" :value="node.sslPolicyId"/>
|
||||||
|
<table class="ui table selectable definition">
|
||||||
|
<tr>
|
||||||
|
<td class="title">节点名称 *</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="name" maxlength="100" ref="focus" v-model="node.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>进程监听端口 *</td>
|
||||||
|
<td>
|
||||||
|
<network-addresses-box :v-name="'listensJSON'" :v-server-type="'httpWeb'" :v-addresses="node.listens" @change="changeListens"></network-addresses-box>
|
||||||
|
<p class="comment">API节点进程监听的网络端口。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="hasHTTPS">
|
||||||
|
<td>HTTPS证书 *</td>
|
||||||
|
<td>
|
||||||
|
<ssl-certs-box :v-certs="node.certs" :v-protocol="'https'"></ssl-certs-box>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>外部访问地址 *</td>
|
||||||
|
<td>
|
||||||
|
<api-node-addresses-box :v-name="'accessAddrsJSON'" :v-addrs="node.accessAddrs"></api-node-addresses-box>
|
||||||
|
<p class="comment">边缘节点和管理平台等外部节点访问API节点的网络地址。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><more-options-indicator></more-options-indicator></td>
|
||||||
|
</tr>
|
||||||
|
<tbody v-show="moreOptionsVisible">
|
||||||
|
<tr>
|
||||||
|
<td>描述</td>
|
||||||
|
<td>
|
||||||
|
<textarea name="description" maxlength="200" rows="3" v-model="node.description"></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>是否启用</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" name="isOn" value="1" v-model="node.isOn"/>
|
||||||
|
<label></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<submit-btn></submit-btn>
|
||||||
|
</form>
|
||||||
12
web/views/@default/settings/user-nodes/node/update.js
Normal file
12
web/views/@default/settings/user-nodes/node/update.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
this.success = NotifySuccess("保存成功", "/settings/userNodes/node?nodeId=" + this.node.id)
|
||||||
|
|
||||||
|
this.hasHTTPS = this.node.listens.$any(function (k, v) {
|
||||||
|
return v.protocol == "https"
|
||||||
|
})
|
||||||
|
this.changeListens = function (addrs) {
|
||||||
|
this.hasHTTPS = addrs.$any(function (k, v) {
|
||||||
|
return v.protocol == "https"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{$layout "layout_popup"}
|
||||||
|
|
||||||
|
<h3>修改访问地址</h3>
|
||||||
|
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||||
|
<table class="ui table definition selectable">
|
||||||
|
<tr>
|
||||||
|
<td>网络协议</td>
|
||||||
|
<td>
|
||||||
|
<select class="ui dropdown auto-width" name="protocol" v-model="protocol">
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="https">HTTPS</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="title">访问地址</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="addr" maxlength="100" ref="focus" v-model="addr"/>
|
||||||
|
<p class="comment">可以是"IP:端口"或者"域名:端口"。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<submit-btn></submit-btn>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
let addr = window.parent.UPDATING_ADDR
|
||||||
|
this.protocol = addr.protocol
|
||||||
|
this.addr = addr.host + ":" + addr.portRange
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user