Compare commits

...

13 Commits

Author SHA1 Message Date
刘祥超
dbe7336f32 改进文字提示 2022-07-26 08:56:37 +08:00
刘祥超
aac953f483 改进文字 2022-07-26 08:00:42 +08:00
刘祥超
49bc469430 优化文字提示 2022-07-24 16:18:13 +08:00
刘祥超
f3ac8a5cc5 用户增加OTP认证设置 2022-07-24 16:14:38 +08:00
刘祥超
847d08a9bb 网站服务列表增加用户筛选 2022-07-24 14:26:14 +08:00
刘祥超
0563a363c2 用户列表中显示实名审核状态 2022-07-24 11:57:46 +08:00
刘祥超
f9dc0d6b54 实现基础的实名认证功能(商业版本专有,开源版本只显示认证状态) 2022-07-24 09:57:26 +08:00
刘祥超
40ef3604aa 增加本地API节点需要升级提示 2022-07-21 19:22:18 +08:00
刘祥超
970604dc73 API节点状态中增加主程序位置信息 2022-07-21 15:24:07 +08:00
刘祥超
d6617f214d 边缘节点详情中包含主程序位置 2022-07-21 15:07:59 +08:00
刘祥超
860fccbd4c API RPC配置增加disableUpdate,可以停用自动更新API节点 2022-07-21 14:13:23 +08:00
刘祥超
b3adb839e0 修改版本为v0.4.10 2022-07-20 18:14:18 +08:00
刘祥超
5e9654c3bc 改进文字提示 2022-07-18 20:40:48 +08:00
44 changed files with 851 additions and 146 deletions

View File

@@ -12,7 +12,8 @@ import (
// APIConfig API配置
type APIConfig struct {
RPC struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
} `yaml:"rpc"`
NodeId string `yaml:"nodeId"`
Secret string `yaml:"secret"`

View File

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "0.4.9"
Version = "0.4.10"
APINodeVersion = "0.4.9"
APINodeVersion = "0.4.10"
ProductName = "Edge Admin"
ProcessName = "edge-admin"

View File

@@ -424,6 +424,10 @@ func (this *RPCClient) UserAccessKeyRPC() pb.UserAccessKeyServiceClient {
return pb.NewUserAccessKeyServiceClient(this.pickConn())
}
func (this *RPCClient) UserIdentityRPC() pb.UserIdentityServiceClient {
return pb.NewUserIdentityServiceClient(this.pickConn())
}
func (this *RPCClient) LoginRPC() pb.LoginServiceClient {
return pb.NewLoginServiceClient(this.pickConn())
}

View File

@@ -58,6 +58,16 @@ func (this *SyncAPINodesTask) Loop() error {
return nil
}
config, err := configs.LoadAPIConfig()
if err != nil {
return err
}
// 是否禁止自动升级
if config.RPC.DisableUpdate {
return nil
}
// 获取所有可用的节点
rpcClient, err := rpc.SharedRPC()
if err != nil {
@@ -68,7 +78,7 @@ func (this *SyncAPINodesTask) Loop() error {
return err
}
newEndpoints := []string{}
var newEndpoints = []string{}
for _, node := range resp.ApiNodes {
if !node.IsOn {
continue
@@ -77,10 +87,6 @@ func (this *SyncAPINodesTask) Loop() error {
}
// 和现有的对比
config, err := configs.LoadAPIConfig()
if err != nil {
return err
}
if this.isSame(newEndpoints, config.RPC.Endpoints) {
return nil
}

View File

@@ -27,14 +27,14 @@ func (this *UpdateAction) RunGet(params struct {
this.ErrorPage(err)
return
}
admin := adminResp.Admin
var admin = adminResp.Admin
if admin == nil {
this.NotFound("admin", params.AdminId)
return
}
// OTP认证
otpLoginIsOn := false
var otpLoginIsOn = false
if admin.OtpLogin != nil {
otpLoginIsOn = admin.OtpLogin.IsOn
}
@@ -45,7 +45,7 @@ func (this *UpdateAction) RunGet(params struct {
this.ErrorPage(err)
return
}
countAccessKeys := countAccessKeyResp.Count
var countAccessKeys = countAccessKeyResp.Count
this.Data["admin"] = maps.Map{
"id": admin.Id,
@@ -59,7 +59,7 @@ func (this *UpdateAction) RunGet(params struct {
}
// 权限
moduleMaps := configloaders.AllModuleMaps()
var moduleMaps = configloaders.AllModuleMaps()
for _, m := range moduleMaps {
code := m.GetString("code")
isChecked := false

View File

@@ -2,11 +2,17 @@ package node
import (
"encoding/json"
"fmt"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"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"
stringutil "github.com/iwind/TeaGo/utils/string"
"time"
)
type IndexAction struct {
@@ -25,7 +31,7 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
return
}
node := nodeResp.ApiNode
var node = nodeResp.ApiNode
if node == nil {
this.NotFound("apiNode", params.NodeId)
return
@@ -33,7 +39,7 @@ func (this *IndexAction) RunGet(params struct {
// 监听地址
var hasHTTPS = false
httpConfig := &serverconfigs.HTTPProtocolConfig{}
var httpConfig = &serverconfigs.HTTPProtocolConfig{}
if len(node.HttpJSON) > 0 {
err = json.Unmarshal(node.HttpJSON, httpConfig)
if err != nil {
@@ -41,7 +47,7 @@ func (this *IndexAction) RunGet(params struct {
return
}
}
httpsConfig := &serverconfigs.HTTPSProtocolConfig{}
var httpsConfig = &serverconfigs.HTTPSProtocolConfig{}
if len(node.HttpsJSON) > 0 {
err = json.Unmarshal(node.HttpsJSON, httpsConfig)
if err != nil {
@@ -52,21 +58,21 @@ func (this *IndexAction) RunGet(params struct {
}
// 监听地址
listens := []*serverconfigs.NetworkAddressConfig{}
var listens = []*serverconfigs.NetworkAddressConfig{}
listens = append(listens, httpConfig.Listen...)
listens = append(listens, httpsConfig.Listen...)
// 证书信息
certs := []*sslconfigs.SSLCertConfig{}
var certs = []*sslconfigs.SSLCertConfig{}
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
var sslPolicyConfigJSON = sslPolicyConfigResp.SslPolicyJSON
if len(sslPolicyConfigJSON) > 0 {
sslPolicy := &sslconfigs.SSLPolicy{}
var sslPolicy = &sslconfigs.SSLPolicy{}
err = json.Unmarshal(sslPolicyConfigJSON, sslPolicy)
if err != nil {
this.ErrorPage(err)
@@ -77,7 +83,7 @@ func (this *IndexAction) RunGet(params struct {
}
// 访问地址
accessAddrs := []*serverconfigs.NetworkAddressConfig{}
var accessAddrs = []*serverconfigs.NetworkAddressConfig{}
if len(node.AccessAddrsJSON) > 0 {
err = json.Unmarshal(node.AccessAddrsJSON, &accessAddrs)
if err != nil {
@@ -87,10 +93,10 @@ func (this *IndexAction) RunGet(params struct {
}
// Rest地址
restAccessAddrs := []*serverconfigs.NetworkAddressConfig{}
var restAccessAddrs = []*serverconfigs.NetworkAddressConfig{}
if node.RestIsOn {
if len(node.RestHTTPJSON) > 0 {
httpConfig := &serverconfigs.HTTPProtocolConfig{}
var httpConfig = &serverconfigs.HTTPProtocolConfig{}
err = json.Unmarshal(node.RestHTTPJSON, httpConfig)
if err != nil {
this.ErrorPage(err)
@@ -102,7 +108,7 @@ func (this *IndexAction) RunGet(params struct {
}
if len(node.RestHTTPSJSON) > 0 {
httpsConfig := &serverconfigs.HTTPSProtocolConfig{}
var httpsConfig = &serverconfigs.HTTPSProtocolConfig{}
err = json.Unmarshal(node.RestHTTPSJSON, httpsConfig)
if err != nil {
this.ErrorPage(err)
@@ -118,6 +124,27 @@ func (this *IndexAction) RunGet(params struct {
}
}
// 状态
var status = &nodeconfigs.NodeStatus{}
var statusIsValid = false
this.Data["newVersion"] = ""
if len(node.StatusJSON) > 0 {
err = json.Unmarshal(node.StatusJSON, &status)
if err != nil {
this.ErrorPage(err)
return
}
if status.UpdatedAt >= time.Now().Unix()-300 {
statusIsValid = true
// 是否为新版本
if stringutil.VersionCompare(status.BuildVersion, teaconst.APINodeVersion) < 0 {
this.Data["newVersion"] = teaconst.APINodeVersion
}
}
}
this.Data["node"] = maps.Map{
"id": node.Id,
"name": node.Name,
@@ -130,6 +157,26 @@ func (this *IndexAction) RunGet(params struct {
"hasHTTPS": hasHTTPS,
"certs": certs,
"isPrimary": node.IsPrimary,
"statusIsValid": statusIsValid,
"status": maps.Map{
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"connectionCount": status.ConnectionCount,
"buildVersion": status.BuildVersion,
"cpuPhysicalCount": status.CPUPhysicalCount,
"cpuLogicalCount": status.CPULogicalCount,
"load1m": numberutils.FormatFloat2(status.Load1m),
"load5m": numberutils.FormatFloat2(status.Load5m),
"load15m": numberutils.FormatFloat2(status.Load15m),
"cacheTotalDiskSize": numberutils.FormatBytes(status.CacheTotalDiskSize),
"cacheTotalMemorySize": numberutils.FormatBytes(status.CacheTotalMemorySize),
"exePath": status.ExePath,
},
}
this.Show()

View File

@@ -321,6 +321,7 @@ func (this *DetailAction) RunGet(params struct {
"load15m": numberutils.FormatFloat2(status.Load15m),
"cacheTotalDiskSize": numberutils.FormatBytes(status.CacheTotalDiskSize),
"cacheTotalMemorySize": numberutils.FormatBytes(status.CacheTotalMemorySize),
"exePath": status.ExePath,
},
"group": groupMap,

View File

@@ -0,0 +1,169 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dashboardutils
import (
"bytes"
"context"
"encoding/json"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/sizes"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
"github.com/shirou/gopsutil/v3/disk"
"os"
"os/exec"
"regexp"
"runtime"
)
// CheckDiskPartitions 检查服务器磁盘空间
func CheckDiskPartitions(thresholdPercent float64) (path string, usage uint64, usagePercent float64, shouldWarning bool) {
partitions, err := disk.Partitions(false)
if err != nil {
return
}
if !lists.ContainsString([]string{"darwin", "linux", "freebsd"}, runtime.GOOS) {
return
}
var rootFS = ""
for _, p := range partitions {
if p.Mountpoint == "/" {
rootFS = p.Fstype
break
}
}
for _, p := range partitions {
if p.Mountpoint == "/boot" {
continue
}
if p.Fstype != rootFS {
continue
}
stat, _ := disk.Usage(p.Mountpoint)
if stat != nil {
if stat.Used < 2*uint64(sizes.G) {
continue
}
if stat.UsedPercent > thresholdPercent {
path = stat.Path
usage = stat.Used
usagePercent = stat.UsedPercent
shouldWarning = true
break
}
}
}
return
}
// CheckLocalAPINode 检查本地的API节点
func CheckLocalAPINode(rpcClient *rpc.RPCClient, ctx context.Context) (exePath string, runtimeVersion string, fileVersion string, ok bool) {
resp, err := rpcClient.APINodeRPC().FindCurrentAPINode(ctx, &pb.FindCurrentAPINodeRequest{})
if err != nil {
return
}
if resp.ApiNode == nil {
return
}
var instanceCode = resp.ApiNode.InstanceCode
if len(instanceCode) == 0 {
return
}
var statusJSON = resp.ApiNode.StatusJSON
if len(statusJSON) == 0 {
return
}
var status = &nodeconfigs.NodeStatus{}
err = json.Unmarshal(statusJSON, status)
if err != nil {
return
}
runtimeVersion = status.BuildVersion
if len(runtimeVersion) == 0 {
return
}
if stringutil.VersionCompare(runtimeVersion, teaconst.APINodeVersion) >= 0 {
return
}
exePath = status.ExePath
if len(exePath) == 0 {
return
}
stat, err := os.Stat(exePath)
if err != nil {
return
}
if stat.IsDir() {
return
}
// 实例信息
{
var outputBuffer = &bytes.Buffer{}
var cmd = exec.Command(exePath, "instance")
cmd.Stdout = outputBuffer
err = cmd.Run()
if err != nil {
return
}
var outputBytes = outputBuffer.Bytes()
if len(outputBytes) == 0 {
return
}
var instanceMap = maps.Map{}
err = json.Unmarshal(bytes.TrimSpace(outputBytes), &instanceMap)
if err != nil {
return
}
if instanceMap.GetString("code") != instanceCode {
return
}
}
// 文件版本
{
var outputBuffer = &bytes.Buffer{}
var cmd = exec.Command(exePath, "-v")
cmd.Stdout = outputBuffer
err = cmd.Run()
if err != nil {
return
}
var outputString = outputBuffer.String()
if len(outputString) == 0 {
return
}
var subMatch = regexp.MustCompile(`\s+v([\d.]+)\s+`).FindStringSubmatch(outputString)
if len(subMatch) == 0 {
return
}
fileVersion = subMatch[1]
// 文件版本是否为最新
if fileVersion != teaconst.APINodeVersion {
fileVersion = runtimeVersion
}
}
ok = true
return
}

View File

@@ -7,16 +7,13 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/sizes"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard/dashboardutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"github.com/shirou/gopsutil/v3/disk"
"regexp"
"runtime"
)
type IndexAction struct {
@@ -66,7 +63,7 @@ func (this *IndexAction) RunPost(params struct{}) {
// 检查当前服务器空间
var diskUsageWarning = ""
diskPath, diskUsage, diskUsagePercent, shouldWarning := this.checkDiskPartitions(90)
diskPath, diskUsage, diskUsagePercent, shouldWarning := dashboardutils.CheckDiskPartitions(90)
if shouldWarning {
diskUsageWarning = "当前服务器磁盘空间不足,请立即扩充容量,文件路径:" + diskPath + ",已使用:" + types.String(diskUsage/1024/1024/1024) + "G已使用比例" + fmt.Sprintf("%.2f%%", diskUsagePercent) + ",仅剩余空间:" + fmt.Sprintf("%.2f%%", 100-diskUsagePercent) + "。"
}
@@ -263,49 +260,18 @@ func (this *IndexAction) RunPost(params struct{}) {
this.Data["metricCharts"] = chartMaps
}
// 当前API节点版本
{
exePath, runtimeVersion, fileVersion, ok := dashboardutils.CheckLocalAPINode(this.RPC(), this.AdminContext())
if ok {
this.Data["localLowerVersionAPINode"] = maps.Map{
"exePath": exePath,
"runtimeVersion": runtimeVersion,
"fileVersion": fileVersion,
"isRestarting": false,
}
}
}
this.Success()
}
// 检查服务器磁盘空间
func (this *IndexAction) checkDiskPartitions(thresholdPercent float64) (path string, usage uint64, usagePercent float64, shouldWarning bool) {
partitions, err := disk.Partitions(false)
if err != nil {
return
}
if !lists.ContainsString([]string{"darwin", "linux", "freebsd"}, runtime.GOOS) {
return
}
var rootFS = ""
for _, p := range partitions {
if p.Mountpoint == "/" {
rootFS = p.Fstype
break
}
}
for _, p := range partitions {
if p.Mountpoint == "/boot" {
continue
}
if p.Fstype != rootFS {
continue
}
stat, _ := disk.Usage(p.Mountpoint)
if stat != nil {
if stat.Used < 2*uint64(sizes.G) {
continue
}
if stat.UsedPercent > thresholdPercent {
path = stat.Path
usage = stat.Used
usagePercent = stat.UsedPercent
shouldWarning = true
break
}
}
}
return
}

View File

@@ -12,6 +12,7 @@ func init() {
Data("teaMenu", "dashboard").
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)).
GetPost("", new(IndexAction)).
Post("/restartLocalAPINode", new(RestartLocalAPINodeAction)).
EndAll()
})
}

View File

@@ -0,0 +1,73 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dashboard
import (
"bytes"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"os/exec"
"regexp"
"time"
)
type RestartLocalAPINodeAction struct {
actionutils.ParentAction
}
func (this *RestartLocalAPINodeAction) RunPost(params struct {
ExePath string
}) {
// 检查当前用户是超级用户
adminResp, err := this.RPC().AdminRPC().FindEnabledAdmin(this.AdminContext(), &pb.FindEnabledAdminRequest{AdminId: this.AdminId()})
if err != nil {
this.ErrorPage(err)
return
}
if adminResp.Admin == nil || !adminResp.Admin.IsSuper {
this.Fail("请切换到超级用户进行此操作")
}
var exePath = params.ExePath
if len(exePath) == 0 {
this.Fail("找不到要重启的API节点文件")
}
{
var stdoutBuffer = &bytes.Buffer{}
var cmd = exec.Command(exePath, "restart")
cmd.Stdout = stdoutBuffer
err = cmd.Run()
if err != nil {
this.Fail("运行失败:输出:" + stdoutBuffer.String())
}
}
// 检查是否已启动
var countTries = 120
for {
countTries--
if countTries < 0 {
this.Fail("启动超时,请尝试手动启动")
break
}
var stdoutBuffer = &bytes.Buffer{}
var cmd = exec.Command(exePath, "status")
cmd.Stdout = stdoutBuffer
err = cmd.Run()
if err != nil {
time.Sleep(1 * time.Second)
continue
}
if regexp.MustCompile(`pid:\s*\d+`).
MatchString(stdoutBuffer.String()) {
break
}
time.Sleep(1 * time.Second)
}
this.Success()
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package files
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/types"
"mime"
"path/filepath"
)
type FileAction struct {
actionutils.ParentAction
}
func (this *FileAction) Init() {
this.Nav("", "", "")
}
func (this *FileAction) RunGet(params struct {
FileId int64
}) {
fileResp, err := this.RPC().FileRPC().FindEnabledFile(this.AdminContext(), &pb.FindEnabledFileRequest{FileId: params.FileId})
if err != nil {
this.ErrorPage(err)
return
}
var file = fileResp.File
if file == nil {
this.NotFound("File", params.FileId)
return
}
chunkIdsResp, err := this.RPC().FileChunkRPC().FindAllFileChunkIds(this.AdminContext(), &pb.FindAllFileChunkIdsRequest{FileId: file.Id})
if err != nil {
this.ErrorPage(err)
return
}
this.AddHeader("Content-Length", types.String(file.Size))
if len(file.MimeType) > 0 {
this.AddHeader("Content-Type", file.MimeType)
} else if len(file.Filename) > 0 {
var ext = filepath.Ext(file.Filename)
var mimeType = mime.TypeByExtension(ext)
this.AddHeader("Content-Type", mimeType)
}
for _, chunkId := range chunkIdsResp.FileChunkIds {
chunkResp, err := this.RPC().FileChunkRPC().DownloadFileChunk(this.AdminContext(), &pb.DownloadFileChunkRequest{FileChunkId: chunkId})
if err != nil {
this.ErrorPage(err)
return
}
if chunkResp.FileChunk == nil {
continue
}
this.Write(chunkResp.FileChunk.Data)
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package files
import "github.com/iwind/TeaGo"
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/files").
Get("/file", new(FileAction)).
EndAll()
})
}

View File

@@ -102,7 +102,8 @@ func SendMessageToCluster(ctx context.Context, clusterId int64, code string, msg
apiRPCClient, err := rpc.NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: apiNode.AccessAddrs,
},
@@ -282,7 +283,8 @@ func SendMessageToNodeIds(ctx context.Context, nodeIds []int64, code string, msg
apiRPCClient, err := rpc.NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: apiNode.AccessAddrs,
},

View File

@@ -36,7 +36,8 @@ func (this *UpdateHostsAction) RunPost(params struct {
client, err := rpc.NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{params.Protocol + "://" + configutils.QuoteIP(params.Host) + ":" + params.Port},
},
@@ -167,7 +168,8 @@ func (this *UpdateHostsAction) RunPost(params struct {
// 修改api.yaml
var apiConfig = &configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: endpoints,
},

View File

@@ -43,7 +43,8 @@ func (this *ValidateApiAction) RunPost(params struct {
Require("请输入节点secret")
client, err := rpc.NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{params.Protocol + "://" + configutils.QuoteIP(params.Host) + ":" + params.Port},
},

View File

@@ -27,6 +27,7 @@ func (this *IndexAction) RunGet(params struct {
Keyword string
AuditingFlag int32
CheckDNS bool
UserId int64
TrafficOutOrder string
}) {
@@ -36,6 +37,7 @@ func (this *IndexAction) RunGet(params struct {
this.Data["auditingFlag"] = params.AuditingFlag
this.Data["checkDNS"] = params.CheckDNS
this.Data["hasOrder"] = len(params.TrafficOutOrder) > 0
this.Data["userId"] = params.UserId
isSearching := params.AuditingFlag == 1 || params.ClusterId > 0 || params.GroupId > 0 || len(params.Keyword) > 0
@@ -76,6 +78,7 @@ func (this *IndexAction) RunGet(params struct {
ServerGroupId: params.GroupId,
Keyword: params.Keyword,
AuditingFlag: params.AuditingFlag,
UserId: params.UserId,
})
if err != nil {
this.ErrorPage(err)
@@ -95,6 +98,7 @@ func (this *IndexAction) RunGet(params struct {
AuditingFlag: params.AuditingFlag,
TrafficOutDesc: params.TrafficOutOrder == "desc",
TrafficOutAsc: params.TrafficOutOrder == "asc",
UserId: params.UserId,
})
if err != nil {
this.ErrorPage(err)
@@ -288,5 +292,13 @@ func (this *IndexAction) RunGet(params struct {
}
this.Data["countNeedFixLogs"] = countNeedFixLogsResp.Count
// 是否有用户
countUsersResp, err := this.RPC().UserRPC().CountAllEnabledUsers(this.AdminContext(), &pb.CountAllEnabledUsersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["hasUsers"] = countUsersResp.Count > 0
this.Show()
}

View File

@@ -29,6 +29,7 @@ func (this *OptionsAction) RunPost(params struct {
"id": user.Id,
"fullname": user.Fullname,
"username": user.Username,
"name": user.Fullname + "(" + user.Username + ")",
})
}
this.Data["users"] = userMaps

View File

@@ -223,7 +223,8 @@ func (this *InstallAction) RunPost(params struct {
// 写入API节点配置完成安装
apiConfig := &configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{"http://" + configutils.QuoteIP(apiNodeMap.GetString("newHost")) + ":" + apiNodeMap.GetString("newPort")},
},
@@ -285,7 +286,8 @@ func (this *InstallAction) RunPost(params struct {
// 构造RPC
apiConfig := &configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{apiNodeMap.GetString("oldProtocol") + "://" + configutils.QuoteIP(apiNodeMap.GetString("oldHost")) + ":" + apiNodeMap.GetString("oldPort")},
},

View File

@@ -85,7 +85,8 @@ func (this *ValidateApiAction) RunPost(params struct {
Require("请输入节点secret")
client, err := rpc.NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
Endpoints []string `yaml:"endpoints"`
DisableUpdate bool `yaml:"disableUpdate"`
}{
Endpoints: []string{params.OldProtocol + "://" + configutils.QuoteIP(params.OldHost) + ":" + params.OldPort},
},

View File

@@ -5,6 +5,8 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/xlzd/gotp"
)
type CreatePopupAction struct {
@@ -30,6 +32,9 @@ func (this *CreatePopupAction) RunPost(params struct {
Remark string
ClusterId int64
// OTP
OtpOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
@@ -91,7 +96,28 @@ func (this *CreatePopupAction) RunPost(params struct {
this.ErrorPage(err)
return
}
defer this.CreateLogInfo("创建用户 %d", createResp.UserId)
var userId = createResp.UserId
defer this.CreateLogInfo("创建用户 %d", userId)
// OTP
if params.OtpOn {
_, err = this.RPC().LoginRPC().UpdateLogin(this.AdminContext(), &pb.UpdateLoginRequest{Login: &pb.Login{
Id: 0,
Type: "otp",
ParamsJSON: maps.Map{
"secret": gotp.RandomSecret(16), // TODO 改成可以设置secret长度
}.AsJSON(),
IsOn: true,
AdminId: 0,
UserId: userId,
}})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -65,19 +65,27 @@ func (this *IndexAction) RunGet(params struct {
}
}
isSubmittedResp, err := this.RPC().UserIdentityRPC().CheckUserIdentityIsSubmitted(this.AdminContext(), &pb.CheckUserIdentityIsSubmittedRequest{UserId: user.Id})
if err != nil {
this.ErrorPage(err)
return
}
var identityIsSubmitted = isSubmittedResp.IsSubmitted
userMaps = append(userMaps, maps.Map{
"id": user.Id,
"username": user.Username,
"isOn": user.IsOn,
"fullname": user.Fullname,
"email": user.Email,
"mobile": user.Mobile,
"tel": user.Tel,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", user.CreatedAt),
"cluster": clusterMap,
"registeredIP": user.RegisteredIP,
"isVerified": user.IsVerified,
"isRejected": user.IsRejected,
"id": user.Id,
"username": user.Username,
"isOn": user.IsOn,
"fullname": user.Fullname,
"email": user.Email,
"mobile": user.Mobile,
"tel": user.Tel,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", user.CreatedAt),
"cluster": clusterMap,
"registeredIP": user.RegisteredIP,
"isVerified": user.IsVerified,
"isRejected": user.IsRejected,
"identityIsSubmitted": identityIsSubmitted,
})
}
this.Data["users"] = userMaps

View File

@@ -20,6 +20,7 @@ func init() {
Post("/delete", new(DeleteAction)).
GetPost("/features", new(FeaturesAction)).
GetPost("/verifyPopup", new(VerifyPopupAction)).
Get("/otpQrcode", new(OtpQrcodeAction)).
// AccessKeys
Prefix("/users/accessKeys").

View File

@@ -0,0 +1,75 @@
package users
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"github.com/skip2/go-qrcode"
"github.com/xlzd/gotp"
)
type OtpQrcodeAction struct {
actionutils.ParentAction
}
func (this *OtpQrcodeAction) Init() {
this.Nav("", "", "")
}
func (this *OtpQrcodeAction) RunGet(params struct {
UserId int64
}) {
loginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
UserId: params.UserId,
Type: "otp",
})
if err != nil {
this.ErrorPage(err)
return
}
login := loginResp.Login
if login == nil || !login.IsOn {
this.NotFound("userLogin", params.UserId)
return
}
loginParams := maps.Map{}
err = json.Unmarshal(login.ParamsJSON, &loginParams)
if err != nil {
this.ErrorPage(err)
return
}
secret := loginParams.GetString("secret")
// 当前用户信息
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.AdminContext(), &pb.FindEnabledUserRequest{UserId: params.UserId})
if err != nil {
this.ErrorPage(err)
return
}
var user = userResp.User
if user == nil {
this.NotFound("user", params.UserId)
return
}
uiConfig, err := configloaders.LoadAdminUIConfig()
if err != nil {
this.ErrorPage(err)
return
}
var productName = uiConfig.ProductName
if len(productName) == 0 {
productName = "GoEdge用户"
}
var url = gotp.NewDefaultTOTP(secret).ProvisioningUri(user.Username, productName)
data, err := qrcode.Encode(url, qrcode.Medium, 256)
if err != nil {
this.ErrorPage(err)
return
}
this.AddHeader("Content-Type", "image/png")
this.Write(data)
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/xlzd/gotp"
)
type UpdateAction struct {
@@ -30,7 +31,7 @@ func (this *UpdateAction) RunGet(params struct {
this.ErrorPage(err)
return
}
user := userResp.User
var user = userResp.User
if user == nil {
this.NotFound("user", params.UserId)
return
@@ -42,7 +43,20 @@ func (this *UpdateAction) RunGet(params struct {
this.ErrorPage(err)
return
}
countAccessKeys := countAccessKeyResp.Count
var countAccessKeys = countAccessKeyResp.Count
// 是否有实名认证
hasNewIndividualIdentity, hasNewEnterpriseIdentity, identityTag, err := userutils.CheckUserIdentity(this.RPC(), this.AdminContext(), params.UserId)
if err != nil {
this.ErrorPage(err)
return
}
// OTP认证
var otpLoginIsOn = false
if user.OtpLogin != nil {
otpLoginIsOn = user.OtpLogin.IsOn
}
this.Data["user"] = maps.Map{
"id": user.Id,
@@ -54,6 +68,14 @@ func (this *UpdateAction) RunGet(params struct {
"mobile": user.Mobile,
"isOn": user.IsOn,
"countAccessKeys": countAccessKeys,
// 实名认证
"hasNewIndividualIdentity": hasNewIndividualIdentity,
"hasNewEnterpriseIdentity": hasNewEnterpriseIdentity,
"identityTag": identityTag,
// otp
"otpLoginIsOn": otpLoginIsOn,
}
this.Data["clusterId"] = 0
@@ -77,6 +99,9 @@ func (this *UpdateAction) RunPost(params struct {
IsOn bool
ClusterId int64
// OTP
OtpOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
@@ -140,5 +165,50 @@ func (this *UpdateAction) RunPost(params struct {
return
}
// 修改OTP
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
UserId: params.UserId,
Type: "otp",
})
if err != nil {
this.ErrorPage(err)
return
}
{
var otpLogin = otpLoginResp.Login
if params.OtpOn {
if otpLogin == nil {
otpLogin = &pb.Login{
Id: 0,
Type: "otp",
ParamsJSON: maps.Map{
"secret": gotp.RandomSecret(16), // TODO 改成可以设置secret长度
}.AsJSON(),
IsOn: true,
UserId: params.UserId,
}
} else {
// 如果已经有了,就覆盖,这样可以保留既有的参数
otpLogin.IsOn = true
}
_, err = this.RPC().LoginRPC().UpdateLogin(this.AdminContext(), &pb.UpdateLoginRequest{Login: otpLogin})
if err != nil {
this.ErrorPage(err)
return
}
} else {
_, err = this.RPC().LoginRPC().UpdateLogin(this.AdminContext(), &pb.UpdateLoginRequest{Login: &pb.Login{
Type: "otp",
IsOn: false,
UserId: params.UserId,
}})
if err != nil {
this.ErrorPage(err)
return
}
}
}
this.Success()
}

View File

@@ -1,6 +1,7 @@
package users
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users/userutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -34,7 +35,7 @@ func (this *UserAction) RunGet(params struct {
this.ErrorPage(err)
return
}
user := userResp.User
var user = userResp.User
if user == nil {
this.NotFound("user", params.UserId)
return
@@ -69,6 +70,28 @@ func (this *UserAction) RunGet(params struct {
}
}
// 是否有实名认证
hasNewIndividualIdentity, hasNewEnterpriseIdentity, identityTag, err := userutils.CheckUserIdentity(this.RPC(), this.AdminContext(), params.UserId)
if err != nil {
this.ErrorPage(err)
return
}
// OTP
this.Data["otp"] = nil
if user.OtpLogin != nil && user.OtpLogin.IsOn {
loginParams := maps.Map{}
err = json.Unmarshal(user.OtpLogin.ParamsJSON, &loginParams)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["otp"] = maps.Map{
"isOn": true,
"params": loginParams,
}
}
this.Data["user"] = maps.Map{
"id": user.Id,
"username": user.Username,
@@ -85,6 +108,11 @@ func (this *UserAction) RunGet(params struct {
"isVerified": user.IsVerified,
"registeredIP": user.RegisteredIP,
"registeredRegion": registeredRegion,
// 实名认证
"hasNewIndividualIdentity": hasNewIndividualIdentity,
"hasNewEnterpriseIdentity": hasNewEnterpriseIdentity,
"identityTag": identityTag,
}
this.Show()

View File

@@ -1,10 +1,14 @@
package userutils
import (
"context"
"errors"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/iwind/TeaGo/maps"
"strings"
)
var ErrUserNotFound = errors.New("not found user")
@@ -28,11 +32,59 @@ func InitUser(p *actionutils.ParentAction, userId int64) error {
return err
}
// 是否有实名认证
hasNewIndividualIdentity, hasNewEnterpriseIdentity, identityTag, err := CheckUserIdentity(p.RPC(), p.AdminContext(), userId)
if err != nil {
return err
}
p.Data["user"] = maps.Map{
"id": userId,
"fullname": resp.User.Fullname,
"username": resp.User.Username,
"countAccessKeys": countAccessKeysResp.Count,
"id": userId,
"fullname": resp.User.Fullname,
"username": resp.User.Username,
"countAccessKeys": countAccessKeysResp.Count,
"hasNewIndividualIdentity": hasNewIndividualIdentity,
"hasNewEnterpriseIdentity": hasNewEnterpriseIdentity,
"identityTag": identityTag,
}
return nil
}
// CheckUserIdentity 实名认证信息
func CheckUserIdentity(rpcClient *rpc.RPCClient, ctx context.Context, userId int64) (hasNewIndividualIdentity bool, hasNewEnterpriseIdentity bool, identityTag string, err error) {
var tags = []string{}
// 个人
individualIdentityResp, err := rpcClient.UserIdentityRPC().FindEnabledUserIdentityWithOrgType(ctx, &pb.FindEnabledUserIdentityWithOrgTypeRequest{
UserId: userId,
OrgType: userconfigs.UserIdentityOrgTypeIndividual,
})
if err != nil {
return false, false, "", err
}
var individualIdentity = individualIdentityResp.UserIdentity
hasNewIndividualIdentity = individualIdentity != nil && individualIdentity.Status == userconfigs.UserIdentityStatusSubmitted
if individualIdentity != nil && individualIdentity.Status == userconfigs.UserIdentityStatusVerified {
tags = append(tags, "个人")
}
// 企业
enterpriseIdentityResp, err := rpcClient.UserIdentityRPC().FindEnabledUserIdentityWithOrgType(ctx, &pb.FindEnabledUserIdentityWithOrgTypeRequest{
UserId: userId,
OrgType: userconfigs.UserIdentityOrgTypeEnterprise,
})
if err != nil {
return false, false, "", err
}
var enterpriseIdentity = enterpriseIdentityResp.UserIdentity
hasNewEnterpriseIdentity = enterpriseIdentity != nil && enterpriseIdentity.Status == userconfigs.UserIdentityStatusSubmitted
if enterpriseIdentity != nil && enterpriseIdentity.Status == userconfigs.UserIdentityStatusVerified {
tags = append(tags, "企业")
}
identityTag = strings.Join(tags, "+")
return
}

View File

@@ -3,6 +3,7 @@ package web
import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/tasks"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/about"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/files"
// 系统用户
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/admins"

View File

@@ -1590,7 +1590,7 @@ Vue.component("traffic-map-box",{props:["v-stats","v-is-attack"],mounted:functio
<td>忽略常见文件</td>
<td>
<checkbox v-model="ignoreCommonFiles"></checkbox>
<p class="comment">忽略js、css、jpg等常见文件名。</p>
<p class="comment">忽略js、css、jpg等常见在网页里被引用的文件名。</p>
</td>
</tr>
</table>
@@ -2323,11 +2323,8 @@ Vue.component("traffic-map-box",{props:["v-stats","v-is-attack"],mounted:functio
<button class="ui button small" type="button" @click.prevent="add">+添加认证方式</button>
</div>
<div class="margin"></div>
</div>`}),Vue.component("user-selector",{mounted:function(){let t=this;Tea.action("/servers/users/options").post().success(function(e){t.users=e.data.users})},props:["v-user-id"],data:function(){let e=this.vUserId;return{users:[],userId:e=null==e?0:e}},watch:{userId:function(e){this.$emit("change",e)}},template:`<div>
<select class="ui dropdown auto-width" name="userId" v-model="userId">
<option value="0">[选择用户]</option>
<option v-for="user in users" :value="user.id">{{user.fullname}} ({{user.username}})</option>
</select>
</div>`}),Vue.component("user-selector",{props:["v-user-id"],data:function(){let e=this.vUserId;return{users:[],userId:e=null==e?0:e}},watch:{userId:function(e){this.$emit("change",e)}},template:`<div>
<combo-box placeholder="选择用户" :data-url="'/servers/users/options'" :data-key="'users'" name="userId" :v-value="userId"></combo-box>
</div>`}),Vue.component("uam-config-box",{props:["v-uam-config","v-is-location","v-is-group"],data:function(){let e=this.vUamConfig;return{config:e=null==e?{isPrior:!1,isOn:!1}:e}},template:`<div>
<input type="hidden" name="uamJSON" :value="JSON.stringify(config)"/>
<table class="ui table definition selectable">

View File

@@ -4948,7 +4948,7 @@ Vue.component("http-firewall-checkpoint-cc", {
<td>忽略常见文件</td>
<td>
<checkbox v-model="ignoreCommonFiles"></checkbox>
<p class="comment">忽略js、css、jpg等常见文件名。</p>
<p class="comment">忽略js、css、jpg等常见在网页里被引用的文件名。</p>
</td>
</tr>
</table>
@@ -7073,15 +7073,6 @@ Vue.component("http-auth-config-box", {
})
Vue.component("user-selector", {
mounted: function () {
let that = this
Tea.action("/servers/users/options")
.post()
.success(function (resp) {
that.users = resp.data.users
})
},
props: ["v-user-id"],
data: function () {
let userId = this.vUserId
@@ -7099,10 +7090,7 @@ Vue.component("user-selector", {
}
},
template: `<div>
<select class="ui dropdown auto-width" name="userId" v-model="userId">
<option value="0">[选择用户]</option>
<option v-for="user in users" :value="user.id">{{user.fullname}} ({{user.username}})</option>
</select>
<combo-box placeholder="选择用户" :data-url="'/servers/users/options'" :data-key="'users'" name="userId" :v-value="userId"></combo-box>
</div>`
})

View File

@@ -214,7 +214,7 @@ Vue.component("http-firewall-checkpoint-cc", {
<td>忽略常见文件</td>
<td>
<checkbox v-model="ignoreCommonFiles"></checkbox>
<p class="comment">忽略js、css、jpg等常见文件名。</p>
<p class="comment">忽略js、css、jpg等常见在网页里被引用的文件名。</p>
</td>
</tr>
</table>

View File

@@ -1,13 +1,4 @@
Vue.component("user-selector", {
mounted: function () {
let that = this
Tea.action("/servers/users/options")
.post()
.success(function (resp) {
that.users = resp.data.users
})
},
props: ["v-user-id"],
data: function () {
let userId = this.vUserId
@@ -25,9 +16,6 @@ Vue.component("user-selector", {
}
},
template: `<div>
<select class="ui dropdown auto-width" name="userId" v-model="userId">
<option value="0">[选择用户]</option>
<option v-for="user in users" :value="user.id">{{user.fullname}} ({{user.username}})</option>
</select>
<combo-box placeholder="选择用户" :data-url="'/servers/users/options'" :data-key="'users'" name="userId" :v-value="userId"></combo-box>
</div>`
})

View File

@@ -65,7 +65,7 @@
<td>认证二维码</td>
<td>
<img :src="'/admins/otpQrcode?adminId=' + admin.id"/>
<p class="comment">可以通过二维码快速添加OTP认证信息到App中。</p>
<p class="comment">可以通过二维码快速添加OTP认证信息到认证App中。</p>
</td>
</tr>
<tr v-if="otp != null && otp.isOn && moreOptionsVisible">

View File

@@ -44,6 +44,24 @@
<p class="comment">通过HTTP访问API节点的网络地址。</p>
</td>
</tr>
<tr v-if="node.statusIsValid">
<td>CPU用量</td>
<td>{{node.status.cpuUsageText}} &nbsp; <span v-if="node.status.cpuPhysicalCount > 0" class="small grey">{{node.status.cpuPhysicalCount}}核心/{{node.status.cpuLogicalCount}}线程)</span></td>
</tr>
<tr v-if="node.statusIsValid">
<td>内存用量</td>
<td>{{node.status.memUsageText}}</td>
</tr>
<tr v-if="node.statusIsValid">
<td>版本</td>
<td>v{{node.status.buildVersion}}
&nbsp; <span class="red" v-if="newVersion.length > 0">需要升级到新版本v{{newVersion}}</span>
</td>
</tr>
<tr v-if="node.statusIsValid">
<td>主程序位置</td>
<td>{{node.status.exePath}}</td>
</tr>
<tr>
<td>主节点</td>
<td>

View File

@@ -206,6 +206,10 @@
&nbsp; <a :href="'/clusters/cluster/upgradeRemote?clusterId=' + clusterId" v-if="shouldUpgrade"><span class="red">发现新版本v{{newVersion}} &raquo;</span></a>
</td>
</tr>
<tr v-if="node.status.exePath != null && node.status.exePath.length > 0">
<td>主程序位置</td>
<td>{{node.status.exePath}}</td>
</tr>
<tr v-if="nodeDatetime.length > 0">
<td>上次更新时间</td>
<td>

View File

@@ -23,16 +23,26 @@
<a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div>
<!-- 升级提醒 -->
<!-- 边缘节点升级提醒 -->
<div class="ui icon message error" v-if="!isLoading && nodeUpgradeInfo.count > 0">
<i class="icon warning circle"></i>
<a href="/clusters">升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,系统正在尝试自动升级...</a><a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div>
<!-- API节点升级提醒 -->
<div class="ui icon message error" v-if="!isLoading && apiNodeUpgradeInfo.count > 0">
<i class="icon warning circle"></i>
<a href="/api">升级提醒:有 {{apiNodeUpgradeInfo.count}} 个API节点需要升级到 v{{apiNodeUpgradeInfo.version}} 版本如果已经升级请尝试重启API节点进程。</a><a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div>
<!-- 本地API节点 -->
<div class="ui icon message error" v-if="!isLoading && localLowerVersionAPINode != null">
<i class="icon warning circle"></i>
<span v-if="localLowerVersionAPINode.runtimeVersion == localLowerVersionAPINode.fileVersion">升级提醒发现一个正在使用的本地API节点版本需要升级文件位置{{localLowerVersionAPINode.exePath}}当前版本v{{localLowerVersionAPINode.runtimeVersion}}。</span>
<span v-if="localLowerVersionAPINode.runtimeVersion != localLowerVersionAPINode.fileVersion">升级提醒发现一个正在使用的本地API节点版本文件已经更新但需要重启后生效文件位置{{localLowerVersionAPINode.exePath}}。 <a href="" @click.prevent="restartAPINode" v-if="!isRestartingLocalAPINode">[帮我重启]</a><span v-if="isRestartingLocalAPINode">尝试重启中...</span></span>
<a href="" title="关闭" @click.prevent="closeMessage"><i class="ui icon remove small"></i></a>
</div>
<!-- 没有磁盘空间提醒 -->
<div class="ui icon message error" v-if="!isLoading && dashboard.diskUsageWarning != null && dashboard.diskUsageWarning.length > 0">
<i class="icon warning circle"></i>

View File

@@ -3,6 +3,7 @@ Tea.context(function () {
this.trafficTab = "hourly"
this.metricCharts = []
this.dashboard = {}
this.localLowerVersionAPINode = null
this.$delay(function () {
this.$post("$")
@@ -193,4 +194,29 @@ Tea.context(function () {
}
}
}
// 重启本地API节点
this.isRestartingLocalAPINode = false
this.restartAPINode = function () {
if (this.isRestartingLocalAPINode) {
return
}
if (this.localLowerVersionAPINode == null) {
return
}
this.isRestartingLocalAPINode = true
this.localLowerVersionAPINode.isRestarting = true
this.$post("/dashboard/restartLocalAPINode")
.params({
"exePath": this.localLowerVersionAPINode.exePath
})
.timeout(300)
.success(function () {
teaweb.reload()
})
.done(function () {
this.isRestartingLocalAPINode = false
this.localLowerVersionAPINode.isRestarting = false
})
}
})

View File

@@ -9,14 +9,14 @@
<td class="title">单个域名 *</td>
<td>
<input type="text" name="serverName" ref="focus" maxlength="1024" v-model="serverName.name"/>
<p class="comment">请输入单个域名,域名中<strong></strong>包含<code-label>http</code-label><code-label>https</code-label></p>
<p class="comment">请输入单个域名,域名中<strong></strong>包含<code-label>http://</code-label><code-label>https://</code-label></p>
</td>
</tr>
<tr v-if="mode == 'multiple'">
<td class="title">多个域名 *</td>
<td>
<textarea name="serverNames" ref="serverNames" v-model="multipleServerNames"></textarea>
<p class="comment">每行一个域名,域名中<strong></strong>包含<code-label>http</code-label><code-label>https</code-label></p>
<p class="comment">每行一个域名,域名中<strong></strong>包含<code-label>http://</code-label><code-label>https://</code-label></p>
</td>
</tr>
</table>

View File

@@ -26,20 +26,25 @@
<option v-for="group in groups" :value="group.id">{{group.name}}</option>
</select>
</div>
<div class="ui field" v-if="hasUsers">
<user-selector :v-user-id="userId"></user-selector>
</div>
<div class="ui field">
<input type="text" name="keyword" style="width:10em" placeholder="关键词、域名等" v-model="keyword"/>
<input type="text" name="keyword" style="width:14em" placeholder="关键词、域名、端口等" v-model="keyword"/>
</div>
<div class="ui field">
<button type="submit" class="ui button">搜索</button>
&nbsp;
<a href="/servers" v-if="clusterId > 0 || groupId > 0 || keyword.length > 0 || hasOrder">[清除条件]</a>
<a href="/servers" v-if="clusterId > 0 || groupId > 0 || keyword.length > 0 || hasOrder || userId > 0">[清除条件]</a>
</div>
</div>
<div class="ui fields inline" style="margin-top: 1em">
<div class="ui field">
<checkbox name="checkDNS" :v-value="1" v-model="checkDNS">检查域名解析</checkbox>
</div>
<div class="ui field" v-if="clusterId == 0 && groupId == 0 && keyword.length == 0 && latestServers.length > 0">
<a href="" @click.prevent="showLatest()">常用<i class="icon angle" :class="{down: !latestVisible, up: latestVisible}"></i> </a>
</div>
<div class="ui field">
<checkbox name="checkDNS" :v-value="1" v-model="checkDNS">检查域名解析</checkbox>
</div>
</div>
</form>

View File

@@ -4,5 +4,6 @@
<menu-item :href="'/users/user?userId=' + user.id" code="index">{{user.fullname}}&nbsp; <span class="small">({{user.username}})</span></menu-item>
<menu-item :href="'/users/update?userId=' + user.id" code="update">修改</menu-item>
<menu-item :href="'/users/features?userId=' + user.id" code="feature">功能</menu-item>
<menu-item :href="'/users/identity?userId=' + user.id" code="identity" v-if="teaIsPlus">实名认证<span v-if="user.hasNewIndividualIdentity || user.hasNewEnterpriseIdentity" class="red small">(待审核)</span><span v-if="user.identityTag != null && user.identityTag.length > 0" class="green">({{user.identityTag}})</span></menu-item>
<menu-item :href="'/users/accessKeys?userId=' + user.id" code="accessKey">API AccessKey({{user.countAccessKeys}})</menu-item>
</first-menu>

View File

@@ -60,6 +60,13 @@
<input type="text" name="email" maxlength="100"/>
</td>
</tr>
<tr>
<td>OTP认证</td>
<td>
<checkbox name="otpOn">启用OTP</checkbox>
<p class="comment">启用OTP认证后在用户登录的时候需要同时填写OTP动态密码。</p>
</td>
</tr>
<tr>
<td>备注</td>
<td>

View File

@@ -41,10 +41,13 @@
<keyword :v-word="keyword">{{user.username}}</keyword>
</a>
<div v-if="!user.isVerified">
<grey-label color="red">未审核</grey-label>
<a :href="'/users/user?userId=' + user.id"><grey-label color="red"><i class="icon info circle"></i>信息未审核</grey-label></a>
</div>
<div v-if="user.isRejected">
<grey-label color="red">已拒绝</grey-label>
<grey-label color="red"><i class="icon info circle"></i>信息已拒绝</grey-label>
</div>
<div v-if="user.identityIsSubmitted">
<a :href="'/users/identity?userId=' + user.id"><grey-label color="red"><i class="icon address card"></i>实名未审核</grey-label></a>
</div>
</td>
<td :class="{disabled:!user.isOn}">

View File

@@ -67,6 +67,13 @@
<input type="text" name="email" maxlength="100" v-model="user.email"/>
</td>
</tr>
<tr>
<td>OTP认证</td>
<td>
<checkbox name="otpOn" v-model="user.otpLoginIsOn">启用OTP</checkbox>
<p class="comment">启用OTP认证后在用户登录的时候需要同时填写OTP动态密码。</p>
</td>
</tr>
<tr>
<td>备注</td>
<td>

View File

@@ -6,7 +6,7 @@
<td>状态</td>
<td>
<span v-if="!user.isVerified" class="red">
未审核 &nbsp; <a href="" @click.prevent="verify">[审核]</a>
信息未审核 &nbsp; <a href="" @click.prevent="verify">[审核]</a>
</span>
<span v-else-if="user.isRejected" class="red">已拒绝
&nbsp; <a href="" @click.prevent="verify">[重新审核]</a>
@@ -72,4 +72,29 @@
<span v-else>{{user.registeredIP}}<span class="grey small">{{user.registeredRegion}}</span></span>
</td>
</tr>
</table>
<h3>OTP认证</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">状态</td>
<td>
<span v-if="otp != null && otp.isOn" class="green">已启用</span>
<span v-else class="disabled">未启用</span>
</td>
</tr>
<tr v-if="otp != null && otp.isOn">
<td colspan="2"><more-options-indicator>更多信息</more-options-indicator></td>
</tr>
<tr v-if="otp != null && otp.isOn && moreOptionsVisible">
<td>认证二维码</td>
<td>
<img :src="'/users/otpQrcode?userId=' + user.id"/>
<p class="comment">可以通过二维码快速添加OTP认证信息到认证App中。</p>
</td>
</tr>
<tr v-if="otp != null && otp.isOn && moreOptionsVisible">
<td>密钥</td>
<td>{{otp.params.secret}}</td>
</tr>
</table>