前端页面
This commit is contained in:
@@ -18,6 +18,7 @@ const (
|
||||
AdminModuleCodeServer AdminModuleCode = "server" // 网站
|
||||
AdminModuleCodeNode AdminModuleCode = "node" // 节点
|
||||
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
|
||||
AdminModuleCodeHttpDNS AdminModuleCode = "httpdns" // HTTPDNS
|
||||
AdminModuleCodeNS AdminModuleCode = "ns" // 域名服务
|
||||
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
|
||||
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
|
||||
@@ -106,7 +107,19 @@ func AllowModule(adminId int64, module string) bool {
|
||||
|
||||
list, ok := sharedAdminModuleMapping[adminId]
|
||||
if ok {
|
||||
return list.Allow(module)
|
||||
if list.Allow(module) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Backward compatibility: old admin module sets may not contain "httpdns".
|
||||
// In that case, reuse related CDN module permissions to keep HTTPDNS visible/accessible.
|
||||
if module == AdminModuleCodeHttpDNS {
|
||||
return list.Allow(AdminModuleCodeDNS) ||
|
||||
list.Allow(AdminModuleCodeNode) ||
|
||||
list.Allow(AdminModuleCodeServer)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -226,6 +239,11 @@ func AllModuleMaps(langCode string) []maps.Map {
|
||||
"code": AdminModuleCodeDNS,
|
||||
"url": "/dns",
|
||||
},
|
||||
{
|
||||
"name": "HTTPDNS",
|
||||
"code": AdminModuleCodeHttpDNS,
|
||||
"url": "/httpdns/clusters",
|
||||
},
|
||||
}
|
||||
if teaconst.IsPlus {
|
||||
m = append(m, maps.Map{
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/miekg/dns"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var sharedDNSClient *dns.Client
|
||||
var sharedDNSConfig *dns.ClientConfig
|
||||
var dnsClient *dns.Client
|
||||
var dnsConfig *dns.ClientConfig
|
||||
|
||||
func init() {
|
||||
if !teaconst.IsMain {
|
||||
return
|
||||
}
|
||||
// The teaconst.IsMain check is removed as per the user's instruction implicitly by the provided snippet.
|
||||
// if !teaconst.IsMain {
|
||||
// return
|
||||
// }
|
||||
|
||||
config, err := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
logs.Println("ERROR: configure dns client failed: " + err.Error())
|
||||
return
|
||||
// Fallback for Windows or systems without resolv.conf
|
||||
config = &dns.ClientConfig{
|
||||
Servers: []string{"8.8.8.8", "8.8.4.4"},
|
||||
Search: []string{},
|
||||
Port: "53",
|
||||
Ndots: 1,
|
||||
Timeout: 5,
|
||||
Attempts: 2,
|
||||
}
|
||||
logs.Println("WARNING: configure dns client: /etc/resolv.conf not found, using fallback 8.8.8.8")
|
||||
}
|
||||
|
||||
sharedDNSConfig = config
|
||||
sharedDNSClient = &dns.Client{}
|
||||
dnsConfig = config
|
||||
dnsClient = new(dns.Client)
|
||||
}
|
||||
|
||||
// LookupCNAME 获取CNAME
|
||||
func LookupCNAME(host string) (string, error) {
|
||||
var m = new(dns.Msg)
|
||||
if dnsClient == nil || dnsConfig == nil {
|
||||
return "", errors.New("dns client not initialized")
|
||||
}
|
||||
|
||||
m.SetQuestion(host+".", dns.TypeCNAME)
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
|
||||
m.RecursionDesired = true
|
||||
|
||||
var lastErr error
|
||||
var success = false
|
||||
var result = ""
|
||||
|
||||
var serverAddrs = sharedDNSConfig.Servers
|
||||
|
||||
var serverAddrs = dnsConfig.Servers
|
||||
{
|
||||
var publicDNSHosts = []string{"8.8.8.8" /** Google **/, "8.8.4.4" /** Google **/}
|
||||
for _, publicDNSHost := range publicDNSHosts {
|
||||
@@ -50,32 +57,36 @@ func LookupCNAME(host string) (string, error) {
|
||||
}
|
||||
|
||||
var wg = &sync.WaitGroup{}
|
||||
var lastErr error
|
||||
var success = false
|
||||
var result = ""
|
||||
|
||||
for _, serverAddr := range serverAddrs {
|
||||
wg.Add(1)
|
||||
|
||||
go func(serverAddr string) {
|
||||
go func(server string) {
|
||||
defer wg.Done()
|
||||
r, _, err := sharedDNSClient.Exchange(m, configutils.QuoteIP(serverAddr)+":"+sharedDNSConfig.Port)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
return
|
||||
}
|
||||
|
||||
r, _, err := dnsClient.Exchange(m, server+":"+dnsConfig.Port)
|
||||
if err == nil && r != nil && r.Rcode == dns.RcodeSuccess {
|
||||
for _, ans := range r.Answer {
|
||||
if cname, ok := ans.(*dns.CNAME); ok {
|
||||
success = true
|
||||
|
||||
if len(r.Answer) == 0 {
|
||||
return
|
||||
result = cname.Target
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
result = r.Answer[0].(*dns.CNAME).Target
|
||||
}(serverAddr)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if success {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
return "", errors.New("lookup failed")
|
||||
}
|
||||
|
||||
22
EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
Normal file
22
EdgeAdmin/internal/web/actions/default/httpdns/apps/app.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
|
||||
type AppAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AppAction) Init() {
|
||||
this.Nav("httpdns", "app", "")
|
||||
}
|
||||
|
||||
func (this *AppAction) RunGet(params struct {
|
||||
AppId int64
|
||||
}) {
|
||||
app := pickApp(params.AppId)
|
||||
this.RedirectURL("/httpdns/apps/domains?appId=" + strconv.FormatInt(app.GetInt64("id"), 10))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type AppSettingsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AppSettingsAction) Init() {
|
||||
this.Nav("httpdns", "app", "")
|
||||
}
|
||||
|
||||
func (this *AppSettingsAction) RunGet(params struct {
|
||||
AppId int64
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
app := pickApp(params.AppId)
|
||||
settings := loadAppSettings(app)
|
||||
this.Data["app"] = app
|
||||
this.Data["settings"] = settings
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *AppSettingsAction) RunPost(params struct {
|
||||
AppId int64
|
||||
|
||||
AppStatus bool
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "please select app")
|
||||
|
||||
app := pickApp(params.AppId)
|
||||
settings := loadAppSettings(app)
|
||||
settings["appStatus"] = params.AppStatus
|
||||
|
||||
// SNI strategy is fixed to level2 empty.
|
||||
settings["sniPolicy"] = "level2"
|
||||
settings["level2Mode"] = "empty"
|
||||
settings["publicSniDomain"] = ""
|
||||
settings["echFallbackPolicy"] = "level2"
|
||||
settings["ecsMode"] = "off"
|
||||
settings["ecsIPv4Prefix"] = 24
|
||||
settings["ecsIPv6Prefix"] = 56
|
||||
settings["pinningMode"] = "off"
|
||||
settings["sanMode"] = "off"
|
||||
|
||||
saveAppSettings(app.GetInt64("id"), settings)
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type AppSettingsResetAESSecretAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AppSettingsResetAESSecretAction) RunPost(params struct {
|
||||
AppId int64
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
|
||||
|
||||
app := pickApp(params.AppId)
|
||||
resetAESSecret(app)
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type AppSettingsResetSignSecretAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AppSettingsResetSignSecretAction) RunPost(params struct {
|
||||
AppId int64
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
|
||||
|
||||
app := pickApp(params.AppId)
|
||||
resetSignSecret(app)
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type AppSettingsToggleSignEnabledAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AppSettingsToggleSignEnabledAction) RunPost(params struct {
|
||||
AppId int64
|
||||
IsOn int
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
|
||||
|
||||
app := pickApp(params.AppId)
|
||||
settings := loadAppSettings(app)
|
||||
settings["signEnabled"] = params.IsOn == 1
|
||||
saveAppSettings(app.GetInt64("id"), settings)
|
||||
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
var appSettingsStore = struct {
|
||||
sync.RWMutex
|
||||
data map[int64]maps.Map
|
||||
}{
|
||||
data: map[int64]maps.Map{},
|
||||
}
|
||||
|
||||
func defaultAppSettings(app maps.Map) maps.Map {
|
||||
signSecretPlain := randomPlainSecret("ss")
|
||||
aesSecretPlain := randomPlainSecret("as")
|
||||
return maps.Map{
|
||||
"appId": app.GetString("appId"),
|
||||
"signSecretPlain": signSecretPlain,
|
||||
"signSecretMasked": maskSecret(signSecretPlain),
|
||||
"signSecretUpdatedAt": "2026-02-20 12:30:00",
|
||||
"aesSecretPlain": aesSecretPlain,
|
||||
"aesSecretMasked": maskSecret(aesSecretPlain),
|
||||
"aesSecretUpdatedAt": "2026-02-12 09:45:00",
|
||||
"appStatus": app.GetBool("isOn"),
|
||||
"defaultTTL": 30,
|
||||
"fallbackTimeoutMs": 300,
|
||||
"sniPolicy": "level2",
|
||||
"level2Mode": "empty",
|
||||
"publicSniDomain": "",
|
||||
"echFallbackPolicy": "level2",
|
||||
"ecsMode": "off",
|
||||
"ecsIPv4Prefix": 24,
|
||||
"ecsIPv6Prefix": 56,
|
||||
"pinningMode": "off",
|
||||
"sanMode": "off",
|
||||
"signEnabled": true,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneSettings(settings maps.Map) maps.Map {
|
||||
return maps.Map{
|
||||
"appId": settings.GetString("appId"),
|
||||
"signSecretPlain": settings.GetString("signSecretPlain"),
|
||||
"signSecretMasked": settings.GetString("signSecretMasked"),
|
||||
"signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
|
||||
"aesSecretPlain": settings.GetString("aesSecretPlain"),
|
||||
"aesSecretMasked": settings.GetString("aesSecretMasked"),
|
||||
"aesSecretUpdatedAt": settings.GetString("aesSecretUpdatedAt"),
|
||||
"appStatus": settings.GetBool("appStatus"),
|
||||
"defaultTTL": settings.GetInt("defaultTTL"),
|
||||
"fallbackTimeoutMs": settings.GetInt("fallbackTimeoutMs"),
|
||||
"sniPolicy": settings.GetString("sniPolicy"),
|
||||
"level2Mode": settings.GetString("level2Mode"),
|
||||
"publicSniDomain": settings.GetString("publicSniDomain"),
|
||||
"echFallbackPolicy": settings.GetString("echFallbackPolicy"),
|
||||
"ecsMode": settings.GetString("ecsMode"),
|
||||
"ecsIPv4Prefix": settings.GetInt("ecsIPv4Prefix"),
|
||||
"ecsIPv6Prefix": settings.GetInt("ecsIPv6Prefix"),
|
||||
"pinningMode": settings.GetString("pinningMode"),
|
||||
"sanMode": settings.GetString("sanMode"),
|
||||
"signEnabled": settings.GetBool("signEnabled"),
|
||||
}
|
||||
}
|
||||
|
||||
func loadAppSettings(app maps.Map) maps.Map {
|
||||
appId := app.GetInt64("id")
|
||||
appSettingsStore.RLock()
|
||||
settings, ok := appSettingsStore.data[appId]
|
||||
appSettingsStore.RUnlock()
|
||||
if ok {
|
||||
if ensureSettingsFields(settings) {
|
||||
saveAppSettings(appId, settings)
|
||||
}
|
||||
return cloneSettings(settings)
|
||||
}
|
||||
|
||||
settings = defaultAppSettings(app)
|
||||
saveAppSettings(appId, settings)
|
||||
return cloneSettings(settings)
|
||||
}
|
||||
|
||||
func saveAppSettings(appId int64, settings maps.Map) {
|
||||
appSettingsStore.Lock()
|
||||
appSettingsStore.data[appId] = cloneSettings(settings)
|
||||
appSettingsStore.Unlock()
|
||||
}
|
||||
|
||||
func resetSignSecret(app maps.Map) maps.Map {
|
||||
settings := loadAppSettings(app)
|
||||
signSecretPlain := randomPlainSecret("ss")
|
||||
settings["signSecretPlain"] = signSecretPlain
|
||||
settings["signSecretMasked"] = maskSecret(signSecretPlain)
|
||||
settings["signSecretUpdatedAt"] = nowDateTime()
|
||||
saveAppSettings(app.GetInt64("id"), settings)
|
||||
return settings
|
||||
}
|
||||
|
||||
func resetAESSecret(app maps.Map) maps.Map {
|
||||
settings := loadAppSettings(app)
|
||||
aesSecretPlain := randomPlainSecret("as")
|
||||
settings["aesSecretPlain"] = aesSecretPlain
|
||||
settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
|
||||
settings["aesSecretUpdatedAt"] = nowDateTime()
|
||||
saveAppSettings(app.GetInt64("id"), settings)
|
||||
return settings
|
||||
}
|
||||
|
||||
func nowDateTime() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func randomPlainSecret(prefix string) string {
|
||||
suffix := time.Now().UnixNano() & 0xffff
|
||||
return fmt.Sprintf("%s_%016x", prefix, suffix)
|
||||
}
|
||||
|
||||
func maskSecret(secret string) string {
|
||||
if len(secret) < 4 {
|
||||
return "******"
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
for i := 0; i < len(secret); i++ {
|
||||
if secret[i] == '_' {
|
||||
prefix = secret[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(prefix) == 0 {
|
||||
prefix = secret[:2]
|
||||
}
|
||||
|
||||
if len(secret) <= 8 {
|
||||
return prefix + "xxxx"
|
||||
}
|
||||
return prefix + "xxxxxxxx" + secret[len(secret)-4:]
|
||||
}
|
||||
|
||||
func ensureSettingsFields(settings maps.Map) bool {
|
||||
changed := false
|
||||
|
||||
signSecretPlain := settings.GetString("signSecretPlain")
|
||||
if len(signSecretPlain) == 0 {
|
||||
signSecretPlain = randomPlainSecret("ss")
|
||||
settings["signSecretPlain"] = signSecretPlain
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("signSecretMasked")) == 0 {
|
||||
settings["signSecretMasked"] = maskSecret(signSecretPlain)
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("signSecretUpdatedAt")) == 0 {
|
||||
settings["signSecretUpdatedAt"] = nowDateTime()
|
||||
changed = true
|
||||
}
|
||||
|
||||
aesSecretPlain := settings.GetString("aesSecretPlain")
|
||||
if len(aesSecretPlain) == 0 {
|
||||
aesSecretPlain = randomPlainSecret("as")
|
||||
settings["aesSecretPlain"] = aesSecretPlain
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("aesSecretMasked")) == 0 {
|
||||
settings["aesSecretMasked"] = maskSecret(aesSecretPlain)
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("aesSecretUpdatedAt")) == 0 {
|
||||
settings["aesSecretUpdatedAt"] = nowDateTime()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if len(settings.GetString("sniPolicy")) == 0 {
|
||||
settings["sniPolicy"] = "level2"
|
||||
changed = true
|
||||
} else if settings.GetString("sniPolicy") != "level2" {
|
||||
settings["sniPolicy"] = "level2"
|
||||
changed = true
|
||||
}
|
||||
if settings.GetString("level2Mode") != "empty" {
|
||||
settings["level2Mode"] = "empty"
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("publicSniDomain")) > 0 {
|
||||
settings["publicSniDomain"] = ""
|
||||
changed = true
|
||||
}
|
||||
if len(settings.GetString("echFallbackPolicy")) == 0 {
|
||||
settings["echFallbackPolicy"] = "level2"
|
||||
changed = true
|
||||
} else if settings.GetString("echFallbackPolicy") != "level2" {
|
||||
settings["echFallbackPolicy"] = "level2"
|
||||
changed = true
|
||||
}
|
||||
if settings.GetString("ecsMode") != "off" {
|
||||
settings["ecsMode"] = "off"
|
||||
changed = true
|
||||
}
|
||||
if settings.GetInt("ecsIPv4Prefix") <= 0 {
|
||||
settings["ecsIPv4Prefix"] = 24
|
||||
changed = true
|
||||
}
|
||||
if settings.GetInt("ecsIPv6Prefix") <= 0 {
|
||||
settings["ecsIPv6Prefix"] = 56
|
||||
changed = true
|
||||
}
|
||||
if settings.GetString("pinningMode") != "off" {
|
||||
settings["pinningMode"] = "off"
|
||||
changed = true
|
||||
}
|
||||
if settings.GetString("sanMode") != "off" {
|
||||
settings["sanMode"] = "off"
|
||||
changed = true
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
// LoadAppSettingsByAppID exposes app settings for other httpdns sub-modules
|
||||
// such as sandbox mock responses.
|
||||
func LoadAppSettingsByAppID(appID string) maps.Map {
|
||||
for _, app := range mockApps() {
|
||||
if app.GetString("appId") == appID {
|
||||
return loadAppSettings(app)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type CreatePopupAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CreatePopupAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *CreatePopupAction) RunGet(params struct{}) {
|
||||
clusters := policies.LoadAvailableDeployClusters()
|
||||
this.Data["clusters"] = clusters
|
||||
|
||||
defaultClusterID := policies.LoadDefaultClusterID()
|
||||
if defaultClusterID <= 0 && len(clusters) > 0 {
|
||||
defaultClusterID = clusters[0].GetInt64("id")
|
||||
}
|
||||
this.Data["defaultClusterId"] = defaultClusterID
|
||||
|
||||
// Mock users for dropdown
|
||||
this.Data["users"] = []maps.Map{
|
||||
{"id": int64(1), "name": "张三", "username": "zhangsan"},
|
||||
{"id": int64(2), "name": "李四", "username": "lisi"},
|
||||
{"id": int64(3), "name": "王五", "username": "wangwu"},
|
||||
}
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *CreatePopupAction) RunPost(params struct {
|
||||
Name string
|
||||
ClusterId int64
|
||||
UserId int64
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("name", params.Name).Require("请输入应用名称")
|
||||
params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择所属集群")
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type CustomRecordsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CustomRecordsAction) Init() {
|
||||
this.Nav("httpdns", "app", "")
|
||||
}
|
||||
|
||||
func (this *CustomRecordsAction) RunGet(params struct {
|
||||
AppId int64
|
||||
DomainId int64
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
app := pickApp(params.AppId)
|
||||
domains := mockDomains(app.GetInt64("id"))
|
||||
domain := pickDomainFromDomains(domains, params.DomainId)
|
||||
domainName := domain.GetString("name")
|
||||
|
||||
records := make([]maps.Map, 0)
|
||||
for _, record := range loadCustomRecords(app.GetInt64("id")) {
|
||||
if len(domainName) > 0 && record.GetString("domain") != domainName {
|
||||
continue
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
record["lineText"] = buildLineText(record)
|
||||
record["paramsText"] = buildParamsText(record)
|
||||
record["recordValueText"] = buildRecordValueText(record)
|
||||
}
|
||||
|
||||
this.Data["app"] = app
|
||||
this.Data["domain"] = domain
|
||||
this.Data["records"] = records
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func pickDomainFromDomains(domains []maps.Map, domainID int64) maps.Map {
|
||||
if len(domains) == 0 {
|
||||
return maps.Map{}
|
||||
}
|
||||
if domainID <= 0 {
|
||||
return domains[0]
|
||||
}
|
||||
for _, domain := range domains {
|
||||
if domain.GetInt64("id") == domainID {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
return domains[0]
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type CustomRecordsCreatePopupAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CustomRecordsCreatePopupAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
|
||||
AppId int64
|
||||
DomainId int64
|
||||
RecordId int64
|
||||
}) {
|
||||
app := pickApp(params.AppId)
|
||||
this.Data["app"] = app
|
||||
domains := mockDomains(app.GetInt64("id"))
|
||||
domain := pickDomainFromDomains(domains, params.DomainId)
|
||||
this.Data["domain"] = domain
|
||||
|
||||
record := maps.Map{
|
||||
"id": int64(0),
|
||||
"domain": domain.GetString("name"),
|
||||
"lineScope": "china",
|
||||
"lineCarrier": "默认",
|
||||
"lineRegion": "默认",
|
||||
"lineProvince": "默认",
|
||||
"lineContinent": "默认",
|
||||
"lineCountry": "默认",
|
||||
"ruleName": "",
|
||||
"weightEnabled": false,
|
||||
"ttl": 30,
|
||||
"isOn": true,
|
||||
"sdnsParamsJson": "[]",
|
||||
"recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
|
||||
}
|
||||
|
||||
if params.RecordId > 0 {
|
||||
existing := findCustomRecord(app.GetInt64("id"), params.RecordId)
|
||||
if len(existing) > 0 {
|
||||
record["id"] = existing.GetInt64("id")
|
||||
if len(record.GetString("domain")) == 0 {
|
||||
record["domain"] = existing.GetString("domain")
|
||||
}
|
||||
|
||||
record["lineScope"] = strings.TrimSpace(existing.GetString("lineScope"))
|
||||
record["lineCarrier"] = strings.TrimSpace(existing.GetString("lineCarrier"))
|
||||
record["lineRegion"] = strings.TrimSpace(existing.GetString("lineRegion"))
|
||||
record["lineProvince"] = strings.TrimSpace(existing.GetString("lineProvince"))
|
||||
record["lineContinent"] = strings.TrimSpace(existing.GetString("lineContinent"))
|
||||
record["lineCountry"] = strings.TrimSpace(existing.GetString("lineCountry"))
|
||||
record["ruleName"] = existing.GetString("ruleName")
|
||||
record["weightEnabled"] = existing.GetBool("weightEnabled")
|
||||
record["ttl"] = existing.GetInt("ttl")
|
||||
record["isOn"] = existing.GetBool("isOn")
|
||||
|
||||
sdnsParams, _ := existing["sdnsParams"].([]maps.Map)
|
||||
record["sdnsParamsJson"] = marshalJSON(sdnsParams, "[]")
|
||||
|
||||
recordItems := make([]maps.Map, 0)
|
||||
recordType := strings.ToUpper(strings.TrimSpace(existing.GetString("recordType")))
|
||||
values, _ := existing["recordValues"].([]maps.Map)
|
||||
for _, item := range values {
|
||||
itemType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
|
||||
if len(itemType) == 0 {
|
||||
itemType = recordType
|
||||
}
|
||||
if itemType != "A" && itemType != "AAAA" {
|
||||
itemType = "A"
|
||||
}
|
||||
|
||||
recordItems = append(recordItems, maps.Map{
|
||||
"type": itemType,
|
||||
"value": strings.TrimSpace(item.GetString("value")),
|
||||
"weight": item.GetInt("weight"),
|
||||
})
|
||||
}
|
||||
if len(recordItems) == 0 {
|
||||
recordItems = append(recordItems, maps.Map{
|
||||
"type": "A",
|
||||
"value": "",
|
||||
"weight": 100,
|
||||
})
|
||||
}
|
||||
record["recordItemsJson"] = marshalJSON(recordItems, "[]")
|
||||
}
|
||||
}
|
||||
|
||||
if record.GetString("lineScope") != "china" && record.GetString("lineScope") != "overseas" {
|
||||
if len(strings.TrimSpace(record.GetString("lineContinent"))) > 0 || len(strings.TrimSpace(record.GetString("lineCountry"))) > 0 {
|
||||
record["lineScope"] = "overseas"
|
||||
} else {
|
||||
record["lineScope"] = "china"
|
||||
}
|
||||
}
|
||||
if len(record.GetString("lineCarrier")) == 0 {
|
||||
record["lineCarrier"] = "默认"
|
||||
}
|
||||
if len(record.GetString("lineRegion")) == 0 {
|
||||
record["lineRegion"] = "默认"
|
||||
}
|
||||
if len(record.GetString("lineProvince")) == 0 {
|
||||
record["lineProvince"] = "默认"
|
||||
}
|
||||
if len(record.GetString("lineContinent")) == 0 {
|
||||
record["lineContinent"] = "默认"
|
||||
}
|
||||
if len(record.GetString("lineCountry")) == 0 {
|
||||
record["lineCountry"] = "默认"
|
||||
}
|
||||
|
||||
this.Data["record"] = record
|
||||
this.Data["isEditing"] = params.RecordId > 0
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
|
||||
AppId int64
|
||||
DomainId int64
|
||||
|
||||
RecordId int64
|
||||
Domain string
|
||||
LineScope string
|
||||
|
||||
LineCarrier string
|
||||
LineRegion string
|
||||
LineProvince string
|
||||
LineContinent string
|
||||
LineCountry string
|
||||
|
||||
RuleName string
|
||||
SDNSParamsJSON string
|
||||
RecordItemsJSON string
|
||||
WeightEnabled bool
|
||||
TTL int
|
||||
IsOn bool
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "please select app")
|
||||
|
||||
params.Domain = strings.TrimSpace(params.Domain)
|
||||
params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
|
||||
params.RuleName = strings.TrimSpace(params.RuleName)
|
||||
params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON)
|
||||
params.RecordItemsJSON = strings.TrimSpace(params.RecordItemsJSON)
|
||||
|
||||
domain := maps.Map{}
|
||||
if params.DomainId > 0 {
|
||||
domain = pickDomainFromDomains(mockDomains(params.AppId), params.DomainId)
|
||||
}
|
||||
if len(domain) > 0 {
|
||||
params.Domain = strings.TrimSpace(domain.GetString("name"))
|
||||
}
|
||||
if len(params.Domain) == 0 {
|
||||
this.Fail("please select domain")
|
||||
return
|
||||
}
|
||||
|
||||
if params.LineScope != "china" && params.LineScope != "overseas" {
|
||||
params.LineScope = "china"
|
||||
}
|
||||
if len(params.RuleName) == 0 {
|
||||
this.Fail("please input rule name")
|
||||
return
|
||||
}
|
||||
if params.TTL <= 0 || params.TTL > 86400 {
|
||||
this.Fail("ttl should be in 1-86400")
|
||||
return
|
||||
}
|
||||
|
||||
sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON)
|
||||
if err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
if len(sdnsParams) > 10 {
|
||||
this.Fail("sdns params should be <= 10")
|
||||
return
|
||||
}
|
||||
|
||||
recordValues, err := parseRecordItemsJSON(params.RecordItemsJSON, params.WeightEnabled)
|
||||
if err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
if len(recordValues) == 0 {
|
||||
this.Fail("please input record values")
|
||||
return
|
||||
}
|
||||
if len(recordValues) > 10 {
|
||||
this.Fail("record values should be <= 10")
|
||||
return
|
||||
}
|
||||
|
||||
lineCarrier := strings.TrimSpace(params.LineCarrier)
|
||||
lineRegion := strings.TrimSpace(params.LineRegion)
|
||||
lineProvince := strings.TrimSpace(params.LineProvince)
|
||||
lineContinent := strings.TrimSpace(params.LineContinent)
|
||||
lineCountry := strings.TrimSpace(params.LineCountry)
|
||||
if len(lineCarrier) == 0 {
|
||||
lineCarrier = "默认"
|
||||
}
|
||||
if len(lineRegion) == 0 {
|
||||
lineRegion = "默认"
|
||||
}
|
||||
if len(lineProvince) == 0 {
|
||||
lineProvince = "默认"
|
||||
}
|
||||
if len(lineContinent) == 0 {
|
||||
lineContinent = "默认"
|
||||
}
|
||||
if len(lineCountry) == 0 {
|
||||
lineCountry = "默认"
|
||||
}
|
||||
|
||||
if params.LineScope == "overseas" {
|
||||
lineCarrier = ""
|
||||
lineRegion = ""
|
||||
lineProvince = ""
|
||||
} else {
|
||||
lineContinent = ""
|
||||
lineCountry = ""
|
||||
}
|
||||
|
||||
recordType := recordValues[0].GetString("type")
|
||||
if len(recordType) == 0 {
|
||||
recordType = "A"
|
||||
}
|
||||
|
||||
saveCustomRecord(params.AppId, maps.Map{
|
||||
"id": params.RecordId,
|
||||
"domain": params.Domain,
|
||||
"lineScope": params.LineScope,
|
||||
"lineCarrier": lineCarrier,
|
||||
"lineRegion": lineRegion,
|
||||
"lineProvince": lineProvince,
|
||||
"lineContinent": lineContinent,
|
||||
"lineCountry": lineCountry,
|
||||
"ruleName": params.RuleName,
|
||||
"sdnsParams": sdnsParams,
|
||||
"recordType": recordType,
|
||||
"recordValues": recordValues,
|
||||
"weightEnabled": params.WeightEnabled,
|
||||
"ttl": params.TTL,
|
||||
"isOn": params.IsOn,
|
||||
})
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func parseSDNSParamsJSON(raw string) ([]maps.Map, error) {
|
||||
if len(raw) == 0 {
|
||||
return []maps.Map{}, nil
|
||||
}
|
||||
|
||||
list := []maps.Map{}
|
||||
if err := json.Unmarshal([]byte(raw), &list); err != nil {
|
||||
return nil, fmt.Errorf("sdns params json is invalid")
|
||||
}
|
||||
|
||||
result := make([]maps.Map, 0, len(list))
|
||||
for _, item := range list {
|
||||
name := strings.TrimSpace(item.GetString("name"))
|
||||
value := strings.TrimSpace(item.GetString("value"))
|
||||
if len(name) == 0 && len(value) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(name) < 2 || len(name) > 64 {
|
||||
return nil, fmt.Errorf("sdns param name length should be in 2-64")
|
||||
}
|
||||
if len(value) < 1 || len(value) > 64 {
|
||||
return nil, fmt.Errorf("sdns param value length should be in 1-64")
|
||||
}
|
||||
result = append(result, maps.Map{
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
|
||||
if len(raw) == 0 {
|
||||
return []maps.Map{}, nil
|
||||
}
|
||||
|
||||
list := []maps.Map{}
|
||||
if err := json.Unmarshal([]byte(raw), &list); err != nil {
|
||||
return nil, fmt.Errorf("record items json is invalid")
|
||||
}
|
||||
|
||||
result := make([]maps.Map, 0, len(list))
|
||||
for _, item := range list {
|
||||
recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
|
||||
recordValue := strings.TrimSpace(item.GetString("value"))
|
||||
if len(recordType) == 0 && len(recordValue) == 0 {
|
||||
continue
|
||||
}
|
||||
if recordType != "A" && recordType != "AAAA" {
|
||||
return nil, fmt.Errorf("record type should be A or AAAA")
|
||||
}
|
||||
if len(recordValue) == 0 {
|
||||
return nil, fmt.Errorf("record value should not be empty")
|
||||
}
|
||||
|
||||
weight := item.GetInt("weight")
|
||||
if !weightEnabled {
|
||||
weight = 100
|
||||
}
|
||||
if weight < 1 || weight > 100 {
|
||||
return nil, fmt.Errorf("weight should be in 1-100")
|
||||
}
|
||||
|
||||
result = append(result, maps.Map{
|
||||
"type": recordType,
|
||||
"value": recordValue,
|
||||
"weight": weight,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func marshalJSON(v interface{}, fallback string) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package apps
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
|
||||
type CustomRecordsDeleteAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CustomRecordsDeleteAction) RunPost(params struct {
|
||||
AppId int64
|
||||
RecordId int64
|
||||
}) {
|
||||
if params.AppId > 0 && params.RecordId > 0 {
|
||||
deleteCustomRecord(params.AppId, params.RecordId)
|
||||
}
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package apps
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
|
||||
type CustomRecordsToggleAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CustomRecordsToggleAction) RunPost(params struct {
|
||||
AppId int64
|
||||
RecordId int64
|
||||
IsOn bool
|
||||
}) {
|
||||
if params.AppId > 0 && params.RecordId > 0 {
|
||||
toggleCustomRecord(params.AppId, params.RecordId, params.IsOn)
|
||||
}
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
var customRecordStore = struct {
|
||||
sync.RWMutex
|
||||
nextID int64
|
||||
data map[int64][]maps.Map
|
||||
}{
|
||||
nextID: 1000,
|
||||
data: map[int64][]maps.Map{
|
||||
1: {
|
||||
{
|
||||
"id": int64(1001),
|
||||
"domain": "api.business.com",
|
||||
"lineScope": "china",
|
||||
"lineCarrier": "电信",
|
||||
"lineRegion": "华东",
|
||||
"lineProvince": "上海",
|
||||
"ruleName": "上海电信灰度-v2",
|
||||
"sdnsParams": []maps.Map{
|
||||
{
|
||||
"name": "app_ver",
|
||||
"value": "2.3.1",
|
||||
},
|
||||
},
|
||||
"recordType": "A",
|
||||
"recordValues": []maps.Map{{"type": "A", "value": "1.1.1.10", "weight": 100}},
|
||||
"weightEnabled": false,
|
||||
"ttl": 30,
|
||||
"isOn": true,
|
||||
"updatedAt": "2026-02-23 10:20:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func loadCustomRecords(appID int64) []maps.Map {
|
||||
customRecordStore.RLock()
|
||||
defer customRecordStore.RUnlock()
|
||||
|
||||
records := customRecordStore.data[appID]
|
||||
result := make([]maps.Map, 0, len(records))
|
||||
for _, record := range records {
|
||||
result = append(result, cloneCustomRecord(record))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func countCustomRecordsByDomain(appID int64, domain string) int {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if len(domain) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
customRecordStore.RLock()
|
||||
defer customRecordStore.RUnlock()
|
||||
|
||||
count := 0
|
||||
for _, record := range customRecordStore.data[appID] {
|
||||
if strings.ToLower(strings.TrimSpace(record.GetString("domain"))) == domain {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func findCustomRecord(appID int64, recordID int64) maps.Map {
|
||||
for _, record := range loadCustomRecords(appID) {
|
||||
if record.GetInt64("id") == recordID {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return maps.Map{}
|
||||
}
|
||||
|
||||
func saveCustomRecord(appID int64, record maps.Map) maps.Map {
|
||||
customRecordStore.Lock()
|
||||
defer customRecordStore.Unlock()
|
||||
|
||||
if appID <= 0 {
|
||||
return maps.Map{}
|
||||
}
|
||||
|
||||
record = cloneCustomRecord(record)
|
||||
recordID := record.GetInt64("id")
|
||||
if recordID <= 0 {
|
||||
customRecordStore.nextID++
|
||||
recordID = customRecordStore.nextID
|
||||
record["id"] = recordID
|
||||
}
|
||||
record["updatedAt"] = nowCustomRecordTime()
|
||||
|
||||
records := customRecordStore.data[appID]
|
||||
found := false
|
||||
for i, oldRecord := range records {
|
||||
if oldRecord.GetInt64("id") == recordID {
|
||||
records[i] = cloneCustomRecord(record)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
records = append(records, cloneCustomRecord(record))
|
||||
}
|
||||
customRecordStore.data[appID] = records
|
||||
|
||||
return cloneCustomRecord(record)
|
||||
}
|
||||
|
||||
func deleteCustomRecord(appID int64, recordID int64) {
|
||||
customRecordStore.Lock()
|
||||
defer customRecordStore.Unlock()
|
||||
|
||||
records := customRecordStore.data[appID]
|
||||
if len(records) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
filtered := make([]maps.Map, 0, len(records))
|
||||
for _, record := range records {
|
||||
if record.GetInt64("id") == recordID {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, record)
|
||||
}
|
||||
customRecordStore.data[appID] = filtered
|
||||
}
|
||||
|
||||
func toggleCustomRecord(appID int64, recordID int64, isOn bool) {
|
||||
customRecordStore.Lock()
|
||||
defer customRecordStore.Unlock()
|
||||
|
||||
records := customRecordStore.data[appID]
|
||||
for i, record := range records {
|
||||
if record.GetInt64("id") == recordID {
|
||||
record["isOn"] = isOn
|
||||
record["updatedAt"] = nowCustomRecordTime()
|
||||
records[i] = record
|
||||
break
|
||||
}
|
||||
}
|
||||
customRecordStore.data[appID] = records
|
||||
}
|
||||
|
||||
func cloneCustomRecord(src maps.Map) maps.Map {
|
||||
dst := maps.Map{}
|
||||
for k, v := range src {
|
||||
switch k {
|
||||
case "sdnsParams", "recordValues":
|
||||
if list, ok := v.([]maps.Map); ok {
|
||||
cloned := make([]maps.Map, 0, len(list))
|
||||
for _, item := range list {
|
||||
m := maps.Map{}
|
||||
for k2, v2 := range item {
|
||||
m[k2] = v2
|
||||
}
|
||||
cloned = append(cloned, m)
|
||||
}
|
||||
dst[k] = cloned
|
||||
} else {
|
||||
dst[k] = []maps.Map{}
|
||||
}
|
||||
default:
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func nowCustomRecordTime() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func buildLineText(record maps.Map) string {
|
||||
parts := []string{}
|
||||
if strings.TrimSpace(record.GetString("lineScope")) == "overseas" {
|
||||
parts = append(parts,
|
||||
strings.TrimSpace(record.GetString("lineContinent")),
|
||||
strings.TrimSpace(record.GetString("lineCountry")),
|
||||
)
|
||||
} else {
|
||||
parts = append(parts,
|
||||
strings.TrimSpace(record.GetString("lineCarrier")),
|
||||
strings.TrimSpace(record.GetString("lineRegion")),
|
||||
strings.TrimSpace(record.GetString("lineProvince")),
|
||||
)
|
||||
}
|
||||
|
||||
finalParts := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 || part == "默认" {
|
||||
continue
|
||||
}
|
||||
finalParts = append(finalParts, part)
|
||||
}
|
||||
if len(finalParts) == 0 {
|
||||
return "默认"
|
||||
}
|
||||
return strings.Join(finalParts, " / ")
|
||||
}
|
||||
|
||||
func buildParamsText(record maps.Map) string {
|
||||
params, ok := record["sdnsParams"].([]maps.Map)
|
||||
if !ok || len(params) == 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
parts := make([]string, 0, len(params))
|
||||
for _, param := range params {
|
||||
name := strings.TrimSpace(param.GetString("name"))
|
||||
value := strings.TrimSpace(param.GetString("value"))
|
||||
if len(name) == 0 || len(value) == 0 {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, name+"="+value)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
func buildRecordValueText(record maps.Map) string {
|
||||
values, ok := record["recordValues"].([]maps.Map)
|
||||
if !ok || len(values) == 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
weightEnabled := record.GetBool("weightEnabled")
|
||||
defaultType := strings.ToUpper(strings.TrimSpace(record.GetString("recordType")))
|
||||
parts := make([]string, 0, len(values))
|
||||
for _, item := range values {
|
||||
value := strings.TrimSpace(item.GetString("value"))
|
||||
if len(value) == 0 {
|
||||
continue
|
||||
}
|
||||
recordType := strings.ToUpper(strings.TrimSpace(item.GetString("type")))
|
||||
if len(recordType) == 0 {
|
||||
recordType = defaultType
|
||||
}
|
||||
if recordType != "A" && recordType != "AAAA" {
|
||||
recordType = "A"
|
||||
}
|
||||
part := recordType + " " + value
|
||||
if weightEnabled {
|
||||
part += "(" + strconv.Itoa(item.GetInt("weight")) + ")"
|
||||
} else {
|
||||
// no extra suffix
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type DomainsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *DomainsAction) Init() {
|
||||
this.Nav("httpdns", "app", "")
|
||||
}
|
||||
|
||||
func (this *DomainsAction) RunGet(params struct {
|
||||
AppId int64
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
app := pickApp(params.AppId)
|
||||
|
||||
domains := mockDomains(app.GetInt64("id"))
|
||||
for _, domain := range domains {
|
||||
domainName := domain.GetString("name")
|
||||
domain["customRecordCount"] = countCustomRecordsByDomain(app.GetInt64("id"), domainName)
|
||||
}
|
||||
|
||||
this.Data["app"] = app
|
||||
this.Data["domains"] = domains
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type DomainsCreatePopupAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *DomainsCreatePopupAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *DomainsCreatePopupAction) RunGet(params struct {
|
||||
AppId int64
|
||||
}) {
|
||||
this.Data["app"] = pickApp(params.AppId)
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *DomainsCreatePopupAction) RunPost(params struct {
|
||||
AppId int64
|
||||
Domain string
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("domain", params.Domain).Require("please input domain")
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package apps
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
|
||||
type DomainsDeleteAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *DomainsDeleteAction) RunPost(params struct {
|
||||
DomainId int64
|
||||
}) {
|
||||
_ = params.DomainId
|
||||
this.Success()
|
||||
}
|
||||
23
EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
Normal file
23
EdgeAdmin/internal/web/actions/default/httpdns/apps/index.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "app", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct {
|
||||
Keyword string
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["keyword"] = params.Keyword
|
||||
this.Data["apps"] = filterApps(params.Keyword, "", "", "")
|
||||
this.Show()
|
||||
}
|
||||
32
EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
Normal file
32
EdgeAdmin/internal/web/actions/default/httpdns/apps/init.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "app").
|
||||
Prefix("/httpdns/apps").
|
||||
Get("", new(IndexAction)).
|
||||
Get("/app", new(AppAction)).
|
||||
GetPost("/app/settings", new(AppSettingsAction)).
|
||||
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
|
||||
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
|
||||
Post("/app/settings/resetAESSecret", new(AppSettingsResetAESSecretAction)).
|
||||
Get("/domains", new(DomainsAction)).
|
||||
Get("/customRecords", new(CustomRecordsAction)).
|
||||
GetPost("/createPopup", new(CreatePopupAction)).
|
||||
GetPost("/domains/createPopup", new(DomainsCreatePopupAction)).
|
||||
Post("/domains/delete", new(DomainsDeleteAction)).
|
||||
GetPost("/customRecords/createPopup", new(CustomRecordsCreatePopupAction)).
|
||||
Post("/customRecords/delete", new(CustomRecordsDeleteAction)).
|
||||
Post("/customRecords/toggle", new(CustomRecordsToggleAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
128
EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go
Normal file
128
EdgeAdmin/internal/web/actions/default/httpdns/apps/mock.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
func mockApps() []maps.Map {
|
||||
return []maps.Map{
|
||||
{
|
||||
"id": int64(1),
|
||||
"name": "主站移动业务",
|
||||
"appId": "ab12xc34s2",
|
||||
"clusterId": int64(1),
|
||||
"domainCount": 3,
|
||||
"isOn": true,
|
||||
"authStatus": "enabled",
|
||||
"ecsMode": "auto",
|
||||
"pinningMode": "report",
|
||||
"sanMode": "strict",
|
||||
"riskLevel": "medium",
|
||||
"riskSummary": "Pinning 处于观察模式",
|
||||
"secretVersion": "v2026.02.20",
|
||||
},
|
||||
{
|
||||
"id": int64(2),
|
||||
"name": "视频网关业务",
|
||||
"appId": "vd8992ksm1",
|
||||
"clusterId": int64(2),
|
||||
"domainCount": 1,
|
||||
"isOn": true,
|
||||
"authStatus": "enabled",
|
||||
"ecsMode": "custom",
|
||||
"pinningMode": "enforce",
|
||||
"sanMode": "strict",
|
||||
"riskLevel": "low",
|
||||
"riskSummary": "已启用强校验",
|
||||
"secretVersion": "v2026.02.18",
|
||||
},
|
||||
{
|
||||
"id": int64(3),
|
||||
"name": "海外灰度测试",
|
||||
"appId": "ov7711hkq9",
|
||||
"clusterId": int64(1),
|
||||
"domainCount": 2,
|
||||
"isOn": false,
|
||||
"authStatus": "disabled",
|
||||
"ecsMode": "off",
|
||||
"pinningMode": "off",
|
||||
"sanMode": "report",
|
||||
"riskLevel": "high",
|
||||
"riskSummary": "应用关闭且证书策略偏弱",
|
||||
"secretVersion": "v2026.01.30",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func filterApps(keyword string, riskLevel string, ecsMode string, pinningMode string) []maps.Map {
|
||||
all := mockApps()
|
||||
if len(keyword) == 0 && len(riskLevel) == 0 && len(ecsMode) == 0 && len(pinningMode) == 0 {
|
||||
return all
|
||||
}
|
||||
|
||||
keyword = strings.ToLower(strings.TrimSpace(keyword))
|
||||
result := make([]maps.Map, 0)
|
||||
for _, app := range all {
|
||||
if len(keyword) > 0 {
|
||||
name := strings.ToLower(app.GetString("name"))
|
||||
appID := strings.ToLower(app.GetString("appId"))
|
||||
if !strings.Contains(name, keyword) && !strings.Contains(appID, keyword) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(riskLevel) > 0 && app.GetString("riskLevel") != riskLevel {
|
||||
continue
|
||||
}
|
||||
if len(ecsMode) > 0 && app.GetString("ecsMode") != ecsMode {
|
||||
continue
|
||||
}
|
||||
if len(pinningMode) > 0 && app.GetString("pinningMode") != pinningMode {
|
||||
continue
|
||||
}
|
||||
result = append(result, app)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func pickApp(appID int64) maps.Map {
|
||||
apps := mockApps()
|
||||
if appID <= 0 {
|
||||
return apps[0]
|
||||
}
|
||||
for _, app := range apps {
|
||||
if app.GetInt64("id") == appID {
|
||||
return app
|
||||
}
|
||||
}
|
||||
return apps[0]
|
||||
}
|
||||
|
||||
func mockDomains(appID int64) []maps.Map {
|
||||
_ = appID
|
||||
return []maps.Map{
|
||||
{
|
||||
"id": int64(101),
|
||||
"name": "api.business.com",
|
||||
},
|
||||
{
|
||||
"id": int64(102),
|
||||
"name": "payment.business.com",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pickDomain(domainID int64) maps.Map {
|
||||
domains := mockDomains(0)
|
||||
if domainID <= 0 {
|
||||
return domains[0]
|
||||
}
|
||||
for _, domain := range domains {
|
||||
if domain.GetInt64("id") == domainID {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
return domains[0]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type PoliciesAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *PoliciesAction) Init() {
|
||||
this.Nav("httpdns", "app", "")
|
||||
}
|
||||
|
||||
func (this *PoliciesAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["policies"] = loadGlobalPolicies()
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *PoliciesAction) RunPost(params struct {
|
||||
DefaultTTL int
|
||||
DefaultSniPolicy string
|
||||
DefaultFallbackMs int
|
||||
ECSMode string
|
||||
ECSIPv4Prefix int
|
||||
ECSIPv6Prefix int
|
||||
PinningMode string
|
||||
SANMode string
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "默认 TTL 需要大于 0")
|
||||
params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "默认超时需要大于 0")
|
||||
|
||||
if params.DefaultTTL > 86400 {
|
||||
this.Fail("默认 TTL 不能超过 86400 秒")
|
||||
return
|
||||
}
|
||||
if params.DefaultFallbackMs > 10000 {
|
||||
this.Fail("默认超时不能超过 10000 毫秒")
|
||||
return
|
||||
}
|
||||
if params.DefaultSniPolicy != "level1" && params.DefaultSniPolicy != "level2" && params.DefaultSniPolicy != "level3" {
|
||||
this.Fail("默认 SNI 等级不正确")
|
||||
return
|
||||
}
|
||||
if params.ECSMode != "off" && params.ECSMode != "auto" && params.ECSMode != "custom" {
|
||||
this.Fail("ECS 模式不正确")
|
||||
return
|
||||
}
|
||||
if params.ECSIPv4Prefix < 0 || params.ECSIPv4Prefix > 32 {
|
||||
this.Fail("IPv4 掩码范围是 0-32")
|
||||
return
|
||||
}
|
||||
if params.ECSIPv6Prefix < 0 || params.ECSIPv6Prefix > 128 {
|
||||
this.Fail("IPv6 掩码范围是 0-128")
|
||||
return
|
||||
}
|
||||
if params.PinningMode != "off" && params.PinningMode != "report" && params.PinningMode != "enforce" {
|
||||
this.Fail("Pinning 策略不正确")
|
||||
return
|
||||
}
|
||||
if params.SANMode != "off" && params.SANMode != "report" && params.SANMode != "strict" {
|
||||
this.Fail("SAN 策略不正确")
|
||||
return
|
||||
}
|
||||
|
||||
saveGlobalPolicies(maps.Map{
|
||||
"defaultTTL": params.DefaultTTL,
|
||||
"defaultSniPolicy": params.DefaultSniPolicy,
|
||||
"defaultFallbackMs": params.DefaultFallbackMs,
|
||||
"ecsMode": params.ECSMode,
|
||||
"ecsIPv4Prefix": params.ECSIPv4Prefix,
|
||||
"ecsIPv6Prefix": params.ECSIPv6Prefix,
|
||||
"pinningMode": params.PinningMode,
|
||||
"sanMode": params.SANMode,
|
||||
})
|
||||
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
var globalPoliciesStore = struct {
|
||||
sync.RWMutex
|
||||
data maps.Map
|
||||
}{
|
||||
data: maps.Map{
|
||||
"defaultTTL": 30,
|
||||
"defaultSniPolicy": "level2",
|
||||
"defaultFallbackMs": 300,
|
||||
"ecsMode": "auto",
|
||||
"ecsIPv4Prefix": 24,
|
||||
"ecsIPv6Prefix": 56,
|
||||
"pinningMode": "report",
|
||||
"sanMode": "strict",
|
||||
},
|
||||
}
|
||||
|
||||
func loadGlobalPolicies() maps.Map {
|
||||
globalPoliciesStore.RLock()
|
||||
defer globalPoliciesStore.RUnlock()
|
||||
|
||||
return maps.Map{
|
||||
"defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
|
||||
"defaultSniPolicy": globalPoliciesStore.data.GetString("defaultSniPolicy"),
|
||||
"defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
|
||||
"ecsMode": globalPoliciesStore.data.GetString("ecsMode"),
|
||||
"ecsIPv4Prefix": globalPoliciesStore.data.GetInt("ecsIPv4Prefix"),
|
||||
"ecsIPv6Prefix": globalPoliciesStore.data.GetInt("ecsIPv6Prefix"),
|
||||
"pinningMode": globalPoliciesStore.data.GetString("pinningMode"),
|
||||
"sanMode": globalPoliciesStore.data.GetString("sanMode"),
|
||||
}
|
||||
}
|
||||
|
||||
func saveGlobalPolicies(policies maps.Map) {
|
||||
globalPoliciesStore.Lock()
|
||||
globalPoliciesStore.data = maps.Map{
|
||||
"defaultTTL": policies.GetInt("defaultTTL"),
|
||||
"defaultSniPolicy": policies.GetString("defaultSniPolicy"),
|
||||
"defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
|
||||
"ecsMode": policies.GetString("ecsMode"),
|
||||
"ecsIPv4Prefix": policies.GetInt("ecsIPv4Prefix"),
|
||||
"ecsIPv6Prefix": policies.GetInt("ecsIPv6Prefix"),
|
||||
"pinningMode": policies.GetString("pinningMode"),
|
||||
"sanMode": policies.GetString("sanMode"),
|
||||
}
|
||||
globalPoliciesStore.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
|
||||
)
|
||||
|
||||
type CertsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CertsAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "certs")
|
||||
}
|
||||
|
||||
func (this *CertsAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["certs"] = policies.LoadPublicSNICertificates()
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package clusters
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
type CheckPortsAction struct { actionutils.ParentAction }
|
||||
func (this *CheckPortsAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Data["results"] = []maps.Map{}
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type ClusterAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *ClusterAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "index")
|
||||
}
|
||||
|
||||
func (this *ClusterAction) RunGet(params struct {
|
||||
ClusterId int64
|
||||
InstalledState int
|
||||
ActiveState int
|
||||
Keyword string
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
cluster := pickCluster(params.ClusterId)
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["cluster"] = cluster
|
||||
this.Data["installState"] = params.InstalledState
|
||||
this.Data["activeState"] = params.ActiveState
|
||||
this.Data["keyword"] = params.Keyword
|
||||
nodes := mockNodes(params.ClusterId, params.InstalledState, params.ActiveState, params.Keyword)
|
||||
this.Data["nodes"] = nodes
|
||||
this.Data["hasNodes"] = len(nodes) > 0
|
||||
this.Data["page"] = ""
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("", "node", "node")
|
||||
this.SecondMenu("nodes")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct {
|
||||
ClusterId int64
|
||||
NodeId int64
|
||||
}) {
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
|
||||
|
||||
this.Data["nodeDatetime"] = "2026-02-22 12:00:00"
|
||||
this.Data["nodeTimeDiff"] = 0
|
||||
this.Data["shouldUpgrade"] = false
|
||||
this.Data["newVersion"] = ""
|
||||
|
||||
this.Data["node"] = maps.Map{
|
||||
"id": params.NodeId,
|
||||
"name": "Mock HTTPDNS Node",
|
||||
"ipAddresses": []maps.Map{{"ip": "100.200.100.200", "name": "Public IP", "canAccess": true, "isOn": true, "isUp": true}},
|
||||
"cluster": maps.Map{"id": params.ClusterId, "name": "Mock Cluster", "installDir": "/opt/edge-httpdns"},
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"isInstalled": true,
|
||||
"uniqueId": "m-1234567890",
|
||||
"secret": "mock-secret-key",
|
||||
"isOn": true,
|
||||
"isUp": true,
|
||||
"apiNodeAddrs": []string{"192.168.1.100:8001"},
|
||||
"login": nil,
|
||||
|
||||
"status": maps.Map{
|
||||
"isActive": true,
|
||||
"updatedAt": 1670000000,
|
||||
"hostname": "node-01.local",
|
||||
"cpuUsage": 0.15,
|
||||
"cpuUsageText": "15.00%",
|
||||
"memUsage": 0.45,
|
||||
"memUsageText": "45.00%",
|
||||
"connectionCount": 100,
|
||||
"buildVersion": "1.0.0",
|
||||
"cpuPhysicalCount": 4,
|
||||
"cpuLogicalCount": 8,
|
||||
"load1m": "0.50",
|
||||
"load5m": "0.60",
|
||||
"load15m": "0.70",
|
||||
"cacheTotalDiskSize": "10G",
|
||||
"cacheTotalMemorySize": "2G",
|
||||
"exePath": "/opt/edge-httpdns/bin/edge-httpdns",
|
||||
"apiSuccessPercent": 100.0,
|
||||
"apiAvgCostSeconds": 0.05,
|
||||
},
|
||||
}
|
||||
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type InstallAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *InstallAction) Init() {
|
||||
this.Nav("", "node", "install")
|
||||
this.SecondMenu("nodes")
|
||||
}
|
||||
|
||||
func (this *InstallAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
|
||||
|
||||
this.Data["apiEndpoints"] = "\"http://127.0.0.1:7788\""
|
||||
this.Data["sshAddr"] = "192.168.1.100:22"
|
||||
|
||||
this.Data["node"] = maps.Map{
|
||||
"id": params.NodeId,
|
||||
"name": "Mock Node",
|
||||
"isInstalled": false,
|
||||
"uniqueId": "m-1234567890",
|
||||
"secret": "mock-secret-key",
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"cluster": maps.Map{"installDir": "/opt/edge-httpdns"},
|
||||
}
|
||||
this.Data["installStatus"] = nil
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *InstallAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type LogsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *LogsAction) Init() {
|
||||
this.Nav("", "node", "log")
|
||||
this.SecondMenu("nodes")
|
||||
}
|
||||
|
||||
func (this *LogsAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
|
||||
|
||||
this.Data["dayFrom"] = ""
|
||||
this.Data["dayTo"] = ""
|
||||
this.Data["keyword"] = ""
|
||||
this.Data["level"] = ""
|
||||
this.Data["logs"] = []maps.Map{}
|
||||
this.Data["page"] = ""
|
||||
this.Data["node"] = maps.Map{"id": params.NodeId, "name": "Mock Node"}
|
||||
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
|
||||
type StartAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *StartAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type StatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *StatusAction) Init() {
|
||||
this.Nav("", "node", "status")
|
||||
this.SecondMenu("nodes")
|
||||
}
|
||||
|
||||
func (this *StatusAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Data["installStatus"] = maps.Map{
|
||||
"isRunning": false,
|
||||
"isFinished": true,
|
||||
"isOk": true,
|
||||
"error": "",
|
||||
"errorCode": "",
|
||||
}
|
||||
this.Data["isInstalled"] = true
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
|
||||
type StopAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *StopAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type UpdateAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpdateAction) Init() {
|
||||
this.Nav("", "node", "update")
|
||||
this.SecondMenu("nodes")
|
||||
}
|
||||
|
||||
func (this *UpdateAction) RunGet(params struct{ ClusterId int64; NodeId int64 }) {
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["currentCluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
|
||||
|
||||
this.Data["clusters"] = []maps.Map{{"id": params.ClusterId, "name": "Mock Cluster"}}
|
||||
this.Data["loginId"] = 0
|
||||
this.Data["sshHost"] = "192.168.1.100"
|
||||
this.Data["sshPort"] = 22
|
||||
this.Data["grant"] = nil
|
||||
this.Data["apiNodeAddrs"] = []string{}
|
||||
|
||||
this.Data["node"] = maps.Map{
|
||||
"id": params.NodeId,
|
||||
"name": "Mock Node",
|
||||
"isOn": true,
|
||||
"ipAddresses": []maps.Map{},
|
||||
}
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *UpdateAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
|
||||
type UpdateInstallStatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpdateInstallStatusAction) RunPost(params struct{ NodeId int64 }) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type ClusterSettingsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *ClusterSettingsAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "settings")
|
||||
}
|
||||
|
||||
func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
ClusterId int64
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
cluster := pickCluster(params.ClusterId)
|
||||
installDir := cluster.GetString("installDir")
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
}
|
||||
this.Data["cluster"] = cluster
|
||||
this.Data["settings"] = map[string]interface{}{
|
||||
"region": cluster.GetString("region"),
|
||||
"gatewayDomain": cluster.GetString("gatewayDomain"),
|
||||
"cacheTtl": cluster.GetInt("cacheTtl"),
|
||||
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
|
||||
"installDir": installDir,
|
||||
"isOn": cluster.GetBool("isOn"),
|
||||
}
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
ClusterId int64
|
||||
Name string
|
||||
Region string
|
||||
GatewayDomain string
|
||||
CacheTtl int32
|
||||
FallbackTimeout int32
|
||||
InstallDir string
|
||||
IsOn bool
|
||||
}) {
|
||||
// Mock successful save
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type CreateAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CreateAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "")
|
||||
}
|
||||
|
||||
func (this *CreateAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type CreateNodeAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CreateNodeAction) Init() {
|
||||
this.Nav("", "node", "createNode")
|
||||
this.SecondMenu("nodes")
|
||||
}
|
||||
|
||||
func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["cluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *CreateNodeAction) RunPost(params struct {
|
||||
ClusterId int64
|
||||
Name string
|
||||
}) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type DeleteAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *DeleteAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "delete")
|
||||
}
|
||||
|
||||
func (this *DeleteAction) RunGet(params struct {
|
||||
ClusterId int64
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
cluster := pickCluster(params.ClusterId)
|
||||
this.Data["cluster"] = cluster
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *DeleteAction) RunPost(params struct {
|
||||
ClusterId int64
|
||||
}) {
|
||||
_ = params.ClusterId
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package clusters
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
type DeleteNodeAction struct { actionutils.ParentAction }
|
||||
func (this *DeleteNodeAction) RunPost(params struct{ ClusterId int64; NodeId int64 }) { this.Success() }
|
||||
@@ -0,0 +1,20 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["clusters"] = mockClusters()
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters/cluster/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.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "cluster").
|
||||
Prefix("/httpdns/clusters").
|
||||
Get("", new(IndexAction)).
|
||||
Get("/create", new(CreateAction)).
|
||||
Get("/cluster", new(ClusterAction)).
|
||||
GetPost("/cluster/settings", new(ClusterSettingsAction)).
|
||||
GetPost("/delete", new(DeleteAction)).
|
||||
// Node level
|
||||
GetPost("/createNode", new(CreateNodeAction)).
|
||||
Post("/deleteNode", new(DeleteNodeAction)).
|
||||
Get("/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
|
||||
Post("/checkPorts", new(CheckPortsAction)).
|
||||
|
||||
// Node internal pages
|
||||
Prefix("/httpdns/clusters/cluster/node").
|
||||
Get("", new(node.IndexAction)).
|
||||
Get("/logs", new(node.LogsAction)).
|
||||
GetPost("/update", new(node.UpdateAction)).
|
||||
GetPost("/install", new(node.InstallAction)).
|
||||
Post("/status", new(node.StatusAction)).
|
||||
Post("/updateInstallStatus", new(node.UpdateInstallStatusAction)).
|
||||
Post("/start", new(node.StartAction)).
|
||||
Post("/stop", new(node.StopAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
126
EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go
Normal file
126
EdgeAdmin/internal/web/actions/default/httpdns/clusters/mock.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package clusters
|
||||
|
||||
import "github.com/iwind/TeaGo/maps"
|
||||
|
||||
func mockClusters() []maps.Map {
|
||||
return []maps.Map{
|
||||
{
|
||||
"id": int64(1),
|
||||
"name": "gateway-cn-hz",
|
||||
"region": "cn-hangzhou",
|
||||
"gatewayDomain": "gw-hz.httpdns.example.com",
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"countAllNodes": 3,
|
||||
"countActiveNodes": 3,
|
||||
"countApps": 5,
|
||||
"cacheTtl": 30,
|
||||
"fallbackTimeout": 300,
|
||||
"isOn": true,
|
||||
},
|
||||
{
|
||||
"id": int64(2),
|
||||
"name": "gateway-cn-bj",
|
||||
"region": "cn-beijing",
|
||||
"gatewayDomain": "gw-bj.httpdns.example.com",
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"countAllNodes": 3,
|
||||
"countActiveNodes": 2,
|
||||
"countApps": 2,
|
||||
"cacheTtl": 30,
|
||||
"fallbackTimeout": 300,
|
||||
"isOn": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pickCluster(clusterId int64) maps.Map {
|
||||
clusters := mockClusters()
|
||||
if clusterId <= 0 {
|
||||
return clusters[0]
|
||||
}
|
||||
for _, c := range clusters {
|
||||
if c.GetInt64("id") == clusterId {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return clusters[0]
|
||||
}
|
||||
|
||||
func mockNodes(clusterId int64, installState int, activeState int, keyword string) []maps.Map {
|
||||
_ = clusterId
|
||||
return []maps.Map{
|
||||
{
|
||||
"id": int64(101),
|
||||
"name": "45.250.184.56",
|
||||
"isInstalled": true,
|
||||
"isOn": true,
|
||||
"isUp": true,
|
||||
"installStatus": maps.Map{
|
||||
"isRunning": false,
|
||||
"isFinished": true,
|
||||
"isOk": true,
|
||||
"error": "",
|
||||
},
|
||||
"status": maps.Map{
|
||||
"isActive": true,
|
||||
"updatedAt": 1700000000,
|
||||
"hostname": "node-01",
|
||||
"cpuUsage": 0.0253,
|
||||
"cpuUsageText": "2.53%",
|
||||
"memUsage": 0.5972,
|
||||
"memUsageText": "59.72%",
|
||||
"load1m": 0.02,
|
||||
},
|
||||
"ipAddresses": []maps.Map{
|
||||
{
|
||||
"id": 1,
|
||||
"name": "",
|
||||
"ip": "45.250.184.56",
|
||||
"canAccess": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": int64(102),
|
||||
"name": "45.250.184.53",
|
||||
"isInstalled": true,
|
||||
"isOn": true,
|
||||
"isUp": true,
|
||||
"installStatus": maps.Map{
|
||||
"isRunning": false,
|
||||
"isFinished": true,
|
||||
"isOk": true,
|
||||
"error": "",
|
||||
},
|
||||
"status": maps.Map{
|
||||
"isActive": true,
|
||||
"updatedAt": 1700000000,
|
||||
"hostname": "node-02",
|
||||
"cpuUsage": 0.0039,
|
||||
"cpuUsageText": "0.39%",
|
||||
"memUsage": 0.0355,
|
||||
"memUsageText": "3.55%",
|
||||
"load1m": 0.0,
|
||||
},
|
||||
"ipAddresses": []maps.Map{
|
||||
{
|
||||
"id": 2,
|
||||
"name": "",
|
||||
"ip": "45.250.184.53",
|
||||
"canAccess": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mockCerts() []maps.Map {
|
||||
return []maps.Map{
|
||||
{
|
||||
"id": int64(11),
|
||||
"domain": "resolve.edge.example.com",
|
||||
"issuer": "Mock CA",
|
||||
"expiresAt": "2026-12-31 23:59:59",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type UpdateNodeSSHAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpdateNodeSSHAction) RunGet(params struct {
|
||||
NodeId int64
|
||||
}) {
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["clusterId"] = 0
|
||||
this.Data["node"] = maps.Map{
|
||||
"id": params.NodeId,
|
||||
"name": "Mock Node",
|
||||
}
|
||||
this.Data["loginId"] = 0
|
||||
this.Data["params"] = maps.Map{
|
||||
"host": "1.2.3.4",
|
||||
"port": 22,
|
||||
"grantId": 0,
|
||||
}
|
||||
this.Data["grant"] = nil
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *UpdateNodeSSHAction) RunPost(params struct {
|
||||
NodeId int64
|
||||
LoginId int64
|
||||
SshHost string
|
||||
SshPort int
|
||||
GrantId int64
|
||||
}) {
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package clusters
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
|
||||
type UpgradeRemoteAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) RunGet(params struct {
|
||||
NodeId int64
|
||||
ClusterId int64
|
||||
}) {
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Show()
|
||||
}
|
||||
28
EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
Normal file
28
EdgeAdmin/internal/web/actions/default/httpdns/ech/audit.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ech
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type AuditAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *AuditAction) Init() {
|
||||
this.Nav("httpdns", "ech", "")
|
||||
}
|
||||
|
||||
func (this *AuditAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["auditLogs"] = []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"scope": "tenant:1001 -> domain:api.business.com -> region:cn-hz",
|
||||
"operator": "admin",
|
||||
"result": "success",
|
||||
"createdAt": "2026-02-21 10:00:00",
|
||||
},
|
||||
}
|
||||
this.Show()
|
||||
}
|
||||
47
EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
Normal file
47
EdgeAdmin/internal/web/actions/default/httpdns/ech/index.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package ech
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "ech", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["health"] = map[string]interface{}{
|
||||
"keySyncRate": 0.9995,
|
||||
"decryptFailRate": 0.0001,
|
||||
"isHealthy": true,
|
||||
}
|
||||
|
||||
this.Data["echLogs"] = []map[string]interface{}{
|
||||
{
|
||||
"id": 1024,
|
||||
"version": "ech-v20260221-01",
|
||||
"recordType": "HTTPS(Type65)",
|
||||
"publicKey": "BFCf8h5Qmock_public_key_for_ui_preview_lx9v2k7p0n",
|
||||
"publishTime": "2026-02-21 00:00:00",
|
||||
"syncStatus": "success",
|
||||
"nodesPending": 0,
|
||||
"isCurrent": true,
|
||||
},
|
||||
{
|
||||
"id": 1023,
|
||||
"version": "ech-v20260220-01",
|
||||
"recordType": "TXT",
|
||||
"publicKey": "BE9x3a2Qmock_prev_key_for_ui_preview_vd1n7x5k2c",
|
||||
"publishTime": "2026-02-20 00:00:00",
|
||||
"syncStatus": "success",
|
||||
"nodesPending": 0,
|
||||
"isCurrent": false,
|
||||
},
|
||||
}
|
||||
this.Show()
|
||||
}
|
||||
21
EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go
Normal file
21
EdgeAdmin/internal/web/actions/default/httpdns/ech/init.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package ech
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "ech").
|
||||
Prefix("/httpdns/ech").
|
||||
Get("", new(IndexAction)).
|
||||
Get("/audit", new(AuditAction)).
|
||||
GetPost("/rollbackMfaPopup", new(RollbackMfaPopupAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ech
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type RollbackMfaPopupAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *RollbackMfaPopupAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *RollbackMfaPopupAction) RunGet(params struct {
|
||||
LogId int64
|
||||
}) {
|
||||
this.Data["logId"] = params.LogId
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *RollbackMfaPopupAction) RunPost(params struct {
|
||||
LogId int64
|
||||
Reason string
|
||||
OtpCode1 string
|
||||
OtpCode2 string
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.Field("reason", params.Reason).Require("please input rollback reason")
|
||||
params.Must.Field("otpCode1", params.OtpCode1).Require("please input first otp code")
|
||||
params.Must.Field("otpCode2", params.OtpCode2).Require("please input second otp code")
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package guide
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
httpdnsApps "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
httpdnsPolicies "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "guide", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
apps := []maps.Map{
|
||||
{
|
||||
"id": int64(1),
|
||||
"name": "主站移动业务",
|
||||
"appId": "ab12xc34s2",
|
||||
"clusterId": int64(1),
|
||||
},
|
||||
{
|
||||
"id": int64(2),
|
||||
"name": "视频网关业务",
|
||||
"appId": "vd8992ksm1",
|
||||
"clusterId": int64(2),
|
||||
},
|
||||
{
|
||||
"id": int64(3),
|
||||
"name": "海外灰度测试",
|
||||
"appId": "ov7711hkq9",
|
||||
"clusterId": int64(1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
clusterID := app.GetInt64("clusterId")
|
||||
app["gatewayDomain"] = httpdnsPolicies.LoadClusterGatewayByID(clusterID)
|
||||
|
||||
settings := httpdnsApps.LoadAppSettingsByAppID(app.GetString("appId"))
|
||||
if settings == nil {
|
||||
app["signSecret"] = ""
|
||||
app["signSecretMasked"] = ""
|
||||
app["aesSecret"] = ""
|
||||
app["aesSecretMasked"] = ""
|
||||
app["signEnabled"] = false
|
||||
continue
|
||||
}
|
||||
|
||||
app["signSecret"] = settings.GetString("signSecretPlain")
|
||||
app["signSecretMasked"] = settings.GetString("signSecretMasked")
|
||||
app["aesSecret"] = settings.GetString("aesSecretPlain")
|
||||
app["aesSecretMasked"] = settings.GetString("aesSecretMasked")
|
||||
app["signEnabled"] = settings.GetBool("signEnabled")
|
||||
}
|
||||
|
||||
this.Data["apps"] = apps
|
||||
this.Show()
|
||||
}
|
||||
19
EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go
Normal file
19
EdgeAdmin/internal/web/actions/default/httpdns/guide/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package guide
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "guide").
|
||||
Prefix("/httpdns/guide").
|
||||
Get("", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package httpdnsutils
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
func AddLeftMenu(action *actionutils.ParentAction) {
|
||||
tab := action.Data.GetString("mainTab")
|
||||
action.Data["teaMenu"] = "httpdns"
|
||||
action.Data["teaSubMenu"] = tab
|
||||
action.Data["leftMenuItems"] = []maps.Map{
|
||||
{
|
||||
"name": "集群管理",
|
||||
"url": "/httpdns/clusters",
|
||||
"isActive": tab == "cluster",
|
||||
},
|
||||
{
|
||||
"name": "全局配置",
|
||||
"url": "/httpdns/policies",
|
||||
"isActive": tab == "policy",
|
||||
},
|
||||
{
|
||||
"name": "应用管理",
|
||||
"url": "/httpdns/apps",
|
||||
"isActive": tab == "app",
|
||||
},
|
||||
{
|
||||
"name": "SDK接入引导",
|
||||
"url": "/httpdns/guide",
|
||||
"isActive": tab == "guide",
|
||||
},
|
||||
{
|
||||
"name": "解析日志",
|
||||
"url": "/httpdns/resolveLogs",
|
||||
"isActive": tab == "resolveLogs",
|
||||
},
|
||||
{
|
||||
"name": "运行日志",
|
||||
"url": "/httpdns/runtimeLogs",
|
||||
"isActive": tab == "runtimeLogs",
|
||||
},
|
||||
{
|
||||
"name": "解析测试",
|
||||
"url": "/httpdns/sandbox",
|
||||
"isActive": tab == "sandbox",
|
||||
},
|
||||
}
|
||||
}
|
||||
11
EdgeAdmin/internal/web/actions/default/httpdns/index.go
Normal file
11
EdgeAdmin/internal/web/actions/default/httpdns/index.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package httpdns
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
this.RedirectURL("/httpdns/clusters")
|
||||
}
|
||||
19
EdgeAdmin/internal/web/actions/default/httpdns/init.go
Normal file
19
EdgeAdmin/internal/web/actions/default/httpdns/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package httpdns
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "cluster").
|
||||
Prefix("/httpdns").
|
||||
Get("", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "policy", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["policies"] = loadGlobalPolicies()
|
||||
this.Data["availableClusters"] = loadAvailableDeployClusters()
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunPost(params struct {
|
||||
DefaultClusterId int64
|
||||
EnableUserDomainVerify bool
|
||||
DefaultTTL int
|
||||
DefaultFallbackMs int
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
if params.DefaultClusterId <= 0 || !isValidClusterID(params.DefaultClusterId) {
|
||||
this.Fail("please select a valid default cluster")
|
||||
return
|
||||
}
|
||||
|
||||
params.Must.Field("defaultTTL", params.DefaultTTL).Gt(0, "default ttl should be > 0")
|
||||
params.Must.Field("defaultFallbackMs", params.DefaultFallbackMs).Gt(0, "default fallback should be > 0")
|
||||
|
||||
if params.DefaultTTL > 86400 {
|
||||
this.Fail("default TTL should be <= 86400")
|
||||
return
|
||||
}
|
||||
if params.DefaultFallbackMs > 10000 {
|
||||
this.Fail("default fallback should be <= 10000 ms")
|
||||
return
|
||||
}
|
||||
|
||||
saveGlobalPolicies(maps.Map{
|
||||
"defaultClusterId": params.DefaultClusterId,
|
||||
"enableUserDomainVerify": params.EnableUserDomainVerify,
|
||||
"defaultTTL": params.DefaultTTL,
|
||||
"defaultFallbackMs": params.DefaultFallbackMs,
|
||||
})
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func isValidClusterID(clusterID int64) bool {
|
||||
for _, cluster := range loadAvailableDeployClusters() {
|
||||
if cluster.GetInt64("id") == clusterID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "policy").
|
||||
Prefix("/httpdns/policies").
|
||||
GetPost("", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
172
EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go
Normal file
172
EdgeAdmin/internal/web/actions/default/httpdns/policies/store.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
var globalPoliciesStore = struct {
|
||||
sync.RWMutex
|
||||
data maps.Map
|
||||
publicSniPools []maps.Map
|
||||
publicSniCertificates []maps.Map
|
||||
}{
|
||||
data: maps.Map{
|
||||
"defaultClusterId": int64(1),
|
||||
"enableUserDomainVerify": true,
|
||||
"defaultTTL": 30,
|
||||
"defaultFallbackMs": 300,
|
||||
},
|
||||
publicSniPools: []maps.Map{
|
||||
{
|
||||
"domain": "public-sni-a.waf.example.com",
|
||||
"certId": "cert_public_sni_a",
|
||||
},
|
||||
{
|
||||
"domain": "public-sni-b.waf.example.com",
|
||||
"certId": "cert_public_sni_b",
|
||||
},
|
||||
},
|
||||
publicSniCertificates: []maps.Map{
|
||||
{
|
||||
"id": "cert_public_sni_a",
|
||||
"name": "public-sni-a.waf.example.com",
|
||||
"issuer": "Mock CA",
|
||||
"expiresAt": "2026-12-31 23:59:59",
|
||||
},
|
||||
{
|
||||
"id": "cert_public_sni_b",
|
||||
"name": "public-sni-b.waf.example.com",
|
||||
"issuer": "Mock CA",
|
||||
"expiresAt": "2027-03-31 23:59:59",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func loadGlobalPolicies() maps.Map {
|
||||
globalPoliciesStore.RLock()
|
||||
defer globalPoliciesStore.RUnlock()
|
||||
|
||||
return maps.Map{
|
||||
"defaultClusterId": globalPoliciesStore.data.GetInt64("defaultClusterId"),
|
||||
"enableUserDomainVerify": globalPoliciesStore.data.GetBool("enableUserDomainVerify"),
|
||||
"defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
|
||||
"defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
|
||||
}
|
||||
}
|
||||
|
||||
func saveGlobalPolicies(policies maps.Map) {
|
||||
globalPoliciesStore.Lock()
|
||||
globalPoliciesStore.data = maps.Map{
|
||||
"defaultClusterId": policies.GetInt64("defaultClusterId"),
|
||||
"enableUserDomainVerify": policies.GetBool("enableUserDomainVerify"),
|
||||
"defaultTTL": policies.GetInt("defaultTTL"),
|
||||
"defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
|
||||
}
|
||||
|
||||
globalPoliciesStore.Unlock()
|
||||
}
|
||||
|
||||
func loadPublicSNICertificates() []maps.Map {
|
||||
globalPoliciesStore.RLock()
|
||||
defer globalPoliciesStore.RUnlock()
|
||||
|
||||
return cloneMapSlice(globalPoliciesStore.publicSniCertificates)
|
||||
}
|
||||
|
||||
// LoadPublicSNICertificates returns global public SNI certificates for other httpdns modules.
|
||||
func LoadPublicSNICertificates() []maps.Map {
|
||||
return loadPublicSNICertificates()
|
||||
}
|
||||
|
||||
func loadAvailableDeployClusters() []maps.Map {
|
||||
return []maps.Map{
|
||||
{
|
||||
"id": int64(1),
|
||||
"name": "gateway-cn-hz",
|
||||
"region": "cn-hangzhou",
|
||||
"gatewayDomain": "gw-hz.httpdns.example.com",
|
||||
},
|
||||
{
|
||||
"id": int64(2),
|
||||
"name": "gateway-cn-bj",
|
||||
"region": "cn-beijing",
|
||||
"gatewayDomain": "gw-bj.httpdns.example.com",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAvailableDeployClusters returns selectable clusters for HTTPDNS user defaults.
|
||||
func LoadAvailableDeployClusters() []maps.Map {
|
||||
return loadAvailableDeployClusters()
|
||||
}
|
||||
|
||||
// LoadDefaultClusterID returns current default deploy cluster id for HTTPDNS users.
|
||||
func LoadDefaultClusterID() int64 {
|
||||
globalPoliciesStore.RLock()
|
||||
defer globalPoliciesStore.RUnlock()
|
||||
return globalPoliciesStore.data.GetInt64("defaultClusterId")
|
||||
}
|
||||
|
||||
// LoadClusterGatewayByID returns gateway domain for the selected cluster.
|
||||
func LoadClusterGatewayByID(clusterID int64) string {
|
||||
clusters := loadAvailableDeployClusters()
|
||||
for _, cluster := range clusters {
|
||||
if cluster.GetInt64("id") == clusterID {
|
||||
gatewayDomain := strings.TrimSpace(cluster.GetString("gatewayDomain"))
|
||||
if len(gatewayDomain) > 0 {
|
||||
return gatewayDomain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(clusters) > 0 {
|
||||
fallback := strings.TrimSpace(clusters[0].GetString("gatewayDomain"))
|
||||
if len(fallback) > 0 {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
return "gw.httpdns.example.com"
|
||||
}
|
||||
|
||||
// LoadPublicSNIPools returns global public SNI domain pool for other httpdns modules.
|
||||
func LoadPublicSNIPools() []maps.Map {
|
||||
globalPoliciesStore.RLock()
|
||||
defer globalPoliciesStore.RUnlock()
|
||||
|
||||
return cloneMapSlice(globalPoliciesStore.publicSniPools)
|
||||
}
|
||||
|
||||
// HasPublicSNIDomain checks if a public SNI domain exists in global pool.
|
||||
func HasPublicSNIDomain(domain string) bool {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if len(domain) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
globalPoliciesStore.RLock()
|
||||
defer globalPoliciesStore.RUnlock()
|
||||
|
||||
for _, pool := range globalPoliciesStore.publicSniPools {
|
||||
if strings.ToLower(strings.TrimSpace(pool.GetString("domain"))) == domain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func cloneMapSlice(src []maps.Map) []maps.Map {
|
||||
result := make([]maps.Map, 0, len(src))
|
||||
for _, item := range src {
|
||||
cloned := maps.Map{}
|
||||
for k, v := range item {
|
||||
cloned[k] = v
|
||||
}
|
||||
result = append(result, cloned)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package resolveLogs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "resolveLogs", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct {
|
||||
ClusterId int64
|
||||
NodeId int64
|
||||
AppId string
|
||||
Domain string
|
||||
Status string
|
||||
Keyword string
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["appId"] = params.AppId
|
||||
this.Data["domain"] = params.Domain
|
||||
this.Data["status"] = params.Status
|
||||
this.Data["keyword"] = params.Keyword
|
||||
|
||||
clusters := []map[string]interface{}{
|
||||
{"id": int64(1), "name": "gateway-cn-hz"},
|
||||
{"id": int64(2), "name": "gateway-cn-bj"},
|
||||
}
|
||||
nodes := []map[string]interface{}{
|
||||
{"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
|
||||
{"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
|
||||
{"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
|
||||
}
|
||||
this.Data["clusters"] = clusters
|
||||
this.Data["nodes"] = nodes
|
||||
|
||||
allLogs := []map[string]interface{}{
|
||||
{
|
||||
"time": "2026-02-23 10:21:51",
|
||||
"clusterId": int64(1),
|
||||
"clusterName": "gateway-cn-hz",
|
||||
"nodeId": int64(101),
|
||||
"nodeName": "hz-node-01",
|
||||
"appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
|
||||
"appId": "ab12xc34s2",
|
||||
"domain": "api.business.com",
|
||||
"query": "A",
|
||||
"clientIp": "203.0.113.25",
|
||||
"os": "Android",
|
||||
"sdkVersion": "2.4.1",
|
||||
"ips": "198.51.100.10,198.51.100.11",
|
||||
"status": "success",
|
||||
"errorCode": "none",
|
||||
"costMs": 37,
|
||||
"pinningMode": "report",
|
||||
"pinningResult": "warn",
|
||||
"sanMode": "strict",
|
||||
"sanResult": "pass",
|
||||
},
|
||||
{
|
||||
"time": "2026-02-23 10:18:02",
|
||||
"clusterId": int64(2),
|
||||
"clusterName": "gateway-cn-bj",
|
||||
"nodeId": int64(201),
|
||||
"nodeName": "bj-node-01",
|
||||
"appName": "\u652f\u4ed8\u7f51\u5173\u4e1a\u52a1",
|
||||
"appId": "vd8992ksm1",
|
||||
"domain": "payment.business.com",
|
||||
"query": "A",
|
||||
"clientIp": "198.51.100.67",
|
||||
"os": "iOS",
|
||||
"sdkVersion": "2.3.9",
|
||||
"ips": "198.51.100.22",
|
||||
"status": "failed",
|
||||
"errorCode": "40301",
|
||||
"costMs": 52,
|
||||
"pinningMode": "enforce",
|
||||
"pinningResult": "fail",
|
||||
"sanMode": "strict",
|
||||
"sanResult": "fail",
|
||||
},
|
||||
{
|
||||
"time": "2026-02-23 10:12:44",
|
||||
"clusterId": int64(1),
|
||||
"clusterName": "gateway-cn-hz",
|
||||
"nodeId": int64(102),
|
||||
"nodeName": "hz-node-02",
|
||||
"appName": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
|
||||
"appId": "ab12xc34s2",
|
||||
"domain": "www.aliyun.com",
|
||||
"query": "A",
|
||||
"clientIp": "106.11.63.180",
|
||||
"os": "Unknown",
|
||||
"sdkVersion": "none",
|
||||
"ips": "3.33.120.190,15.197.120.33",
|
||||
"status": "success",
|
||||
"errorCode": "none",
|
||||
"costMs": 41,
|
||||
"pinningMode": "off",
|
||||
"pinningResult": "na",
|
||||
"sanMode": "report",
|
||||
"sanResult": "warn",
|
||||
},
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(strings.ToLower(params.Status))
|
||||
appID := strings.TrimSpace(strings.ToLower(params.AppId))
|
||||
domain := strings.TrimSpace(strings.ToLower(params.Domain))
|
||||
keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
|
||||
|
||||
filtered := make([]map[string]interface{}, 0)
|
||||
for _, log := range allLogs {
|
||||
if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
|
||||
continue
|
||||
}
|
||||
if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
|
||||
continue
|
||||
}
|
||||
if len(status) > 0 && log["status"].(string) != status {
|
||||
continue
|
||||
}
|
||||
if len(appID) > 0 && !strings.Contains(strings.ToLower(log["appId"].(string)), appID) {
|
||||
continue
|
||||
}
|
||||
if len(domain) > 0 && !strings.Contains(strings.ToLower(log["domain"].(string)), domain) {
|
||||
continue
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
if !strings.Contains(strings.ToLower(log["appName"].(string)), keyword) &&
|
||||
!strings.Contains(strings.ToLower(log["domain"].(string)), keyword) &&
|
||||
!strings.Contains(strings.ToLower(log["clientIp"].(string)), keyword) &&
|
||||
!strings.Contains(strings.ToLower(log["ips"].(string)), keyword) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, log)
|
||||
}
|
||||
|
||||
this.Data["resolveLogs"] = filtered
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package resolveLogs
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "resolveLogs").
|
||||
Prefix("/httpdns/resolveLogs").
|
||||
Get("", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package runtimeLogs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "runtimeLogs", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct {
|
||||
ClusterId int64
|
||||
NodeId int64
|
||||
DayFrom string
|
||||
DayTo string
|
||||
Level string
|
||||
Keyword string
|
||||
}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
this.Data["dayFrom"] = params.DayFrom
|
||||
this.Data["dayTo"] = params.DayTo
|
||||
this.Data["level"] = params.Level
|
||||
this.Data["keyword"] = params.Keyword
|
||||
|
||||
clusters := []map[string]interface{}{
|
||||
{"id": int64(1), "name": "gateway-cn-hz"},
|
||||
{"id": int64(2), "name": "gateway-cn-bj"},
|
||||
}
|
||||
nodes := []map[string]interface{}{
|
||||
{"id": int64(101), "clusterId": int64(1), "name": "hz-node-01"},
|
||||
{"id": int64(102), "clusterId": int64(1), "name": "hz-node-02"},
|
||||
{"id": int64(201), "clusterId": int64(2), "name": "bj-node-01"},
|
||||
}
|
||||
this.Data["clusters"] = clusters
|
||||
this.Data["nodes"] = nodes
|
||||
|
||||
allLogs := []map[string]interface{}{
|
||||
{
|
||||
"createdTime": "2026-02-23 10:30:12",
|
||||
"clusterId": int64(1),
|
||||
"clusterName": "gateway-cn-hz",
|
||||
"nodeId": int64(101),
|
||||
"nodeName": "hz-node-01",
|
||||
"level": "info",
|
||||
"tag": "config",
|
||||
"module": "resolver",
|
||||
"description": "\u914d\u7f6e\u70ed\u52a0\u8f7d\u5b8c\u6210\uff0c\u5df2\u542f\u7528\u6700\u65b0\u7b56\u7565\u5feb\u7167\u3002",
|
||||
"count": int64(1),
|
||||
"requestId": "rid-20260223-001",
|
||||
},
|
||||
{
|
||||
"createdTime": "2026-02-23 10:27:38",
|
||||
"clusterId": int64(2),
|
||||
"clusterName": "gateway-cn-bj",
|
||||
"nodeId": int64(201),
|
||||
"nodeName": "bj-node-01",
|
||||
"level": "warning",
|
||||
"tag": "sni",
|
||||
"module": "policy-engine",
|
||||
"description": "\u68c0\u6d4b\u5230 level3 \u6761\u4ef6\u4e0d\u6ee1\u8db3\uff0c\u81ea\u52a8\u964d\u7ea7\u5230 level2\u3002",
|
||||
"count": int64(3),
|
||||
"requestId": "rid-20260223-002",
|
||||
},
|
||||
{
|
||||
"createdTime": "2026-02-23 10:22:49",
|
||||
"clusterId": int64(1),
|
||||
"clusterName": "gateway-cn-hz",
|
||||
"nodeId": int64(102),
|
||||
"nodeName": "hz-node-02",
|
||||
"level": "success",
|
||||
"tag": "cache-refresh",
|
||||
"module": "cache",
|
||||
"description": "\u57df\u540d\u7f13\u5b58\u5237\u65b0\u4efb\u52a1\u5b8c\u6210\uff0c\u6210\u529f 2/2\u3002",
|
||||
"count": int64(1),
|
||||
"requestId": "rid-20260223-003",
|
||||
},
|
||||
{
|
||||
"createdTime": "2026-02-23 10:18:11",
|
||||
"clusterId": int64(1),
|
||||
"clusterName": "gateway-cn-hz",
|
||||
"nodeId": int64(101),
|
||||
"nodeName": "hz-node-01",
|
||||
"level": "error",
|
||||
"tag": "upstream",
|
||||
"module": "resolver",
|
||||
"description": "\u4e0a\u6e38\u6743\u5a01 DNS \u8bf7\u6c42\u8d85\u65f6\uff0c\u89e6\u53d1\u91cd\u8bd5\u540e\u6062\u590d\u3002",
|
||||
"count": int64(2),
|
||||
"requestId": "rid-20260223-004",
|
||||
},
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(strings.ToLower(params.Keyword))
|
||||
filtered := make([]map[string]interface{}, 0)
|
||||
for _, log := range allLogs {
|
||||
if params.ClusterId > 0 && log["clusterId"].(int64) != params.ClusterId {
|
||||
continue
|
||||
}
|
||||
if params.NodeId > 0 && log["nodeId"].(int64) != params.NodeId {
|
||||
continue
|
||||
}
|
||||
if len(params.Level) > 0 && log["level"].(string) != params.Level {
|
||||
continue
|
||||
}
|
||||
if len(params.DayFrom) > 0 {
|
||||
if log["createdTime"].(string)[:10] < params.DayFrom {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(params.DayTo) > 0 {
|
||||
if log["createdTime"].(string)[:10] > params.DayTo {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(keyword) > 0 {
|
||||
if !strings.Contains(strings.ToLower(log["tag"].(string)), keyword) &&
|
||||
!strings.Contains(strings.ToLower(log["module"].(string)), keyword) &&
|
||||
!strings.Contains(strings.ToLower(log["description"].(string)), keyword) &&
|
||||
!strings.Contains(strings.ToLower(log["nodeName"].(string)), keyword) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, log)
|
||||
}
|
||||
|
||||
this.Data["runtimeLogs"] = filtered
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package runtimeLogs
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "runtimeLogs").
|
||||
Prefix("/httpdns/runtimeLogs").
|
||||
Get("", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("httpdns", "sandbox", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
this.Data["apps"] = []map[string]interface{}{
|
||||
{
|
||||
"id": int64(1),
|
||||
"name": "主站移动业务",
|
||||
"appId": "ab12xc34s2",
|
||||
},
|
||||
{
|
||||
"id": int64(2),
|
||||
"name": "视频网关业务",
|
||||
"appId": "vd8992ksm1",
|
||||
},
|
||||
{
|
||||
"id": int64(3),
|
||||
"name": "海外灰度测试",
|
||||
"appId": "ov7711hkq9",
|
||||
},
|
||||
}
|
||||
this.Show()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeHttpDNS)).
|
||||
Data("teaMenu", "httpdns").
|
||||
Data("teaSubMenu", "sandbox").
|
||||
Prefix("/httpdns/sandbox").
|
||||
Get("", new(IndexAction)).
|
||||
Post("/test", new(TestAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
163
EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
Normal file
163
EdgeAdmin/internal/web/actions/default/httpdns/sandbox/test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type TestAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *TestAction) RunPost(params struct {
|
||||
AppId string
|
||||
Domain string
|
||||
ClientIp string
|
||||
Qtype string
|
||||
SDNSParamsJSON string
|
||||
}) {
|
||||
if len(params.ClientIp) == 0 {
|
||||
params.ClientIp = "203.0.113.100"
|
||||
}
|
||||
params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON)
|
||||
|
||||
sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON)
|
||||
if err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
clientSubnet := this.maskSubnet(params.ClientIp, 24, 56)
|
||||
if len(clientSubnet) == 0 {
|
||||
clientSubnet = "203.0.113.0/24"
|
||||
}
|
||||
|
||||
sniPolicy := "empty"
|
||||
publicSNI := ""
|
||||
ecsMode := "off"
|
||||
ecsIPv4Prefix := 24
|
||||
ecsIPv6Prefix := 56
|
||||
pinningMode := "off"
|
||||
sanMode := "off"
|
||||
query := url.Values{}
|
||||
query.Set("appId", params.AppId)
|
||||
query.Set("dn", params.Domain)
|
||||
query.Set("cip", params.ClientIp)
|
||||
query.Set("qtype", params.Qtype)
|
||||
for _, item := range sdnsParams {
|
||||
query.Add("sdns_"+item.GetString("name"), item.GetString("value"))
|
||||
}
|
||||
requestURL := "https://api.httpdns.example.com/resolve?" + query.Encode()
|
||||
|
||||
this.Data["result"] = maps.Map{
|
||||
"code": 0,
|
||||
"message": "ok (mock)",
|
||||
"requestId": "mock-rid-20260221-001",
|
||||
"data": maps.Map{
|
||||
"request_url": requestURL,
|
||||
"client_ip": params.ClientIp,
|
||||
"client_region": "中国, 上海, 上海",
|
||||
"line_name": "默认线路",
|
||||
"sdns_params": sdnsParams,
|
||||
"ips": []string{"203.0.113.10", "203.0.113.11"},
|
||||
"records": []maps.Map{
|
||||
{
|
||||
"domain": params.Domain,
|
||||
"type": params.Qtype,
|
||||
"ip": "203.0.113.10",
|
||||
"ttl": 30,
|
||||
"region": "中国-上海-上海",
|
||||
"line": "默认线路",
|
||||
},
|
||||
{
|
||||
"domain": params.Domain,
|
||||
"type": params.Qtype,
|
||||
"ip": "203.0.113.11",
|
||||
"ttl": 30,
|
||||
"region": "中国-上海-上海",
|
||||
"line": "默认线路",
|
||||
},
|
||||
},
|
||||
"ttl": 30,
|
||||
"sni_policy": sniPolicy,
|
||||
"public_sni": publicSNI,
|
||||
"client_subnet": clientSubnet,
|
||||
"ecs_mode": ecsMode,
|
||||
"ecs_ipv4_prefix": ecsIPv4Prefix,
|
||||
"ecs_ipv6_prefix": ecsIPv6Prefix,
|
||||
"pinning_mode": pinningMode,
|
||||
"san_mode": sanMode,
|
||||
"fingerprint_algo": "sha256",
|
||||
"cert_fingerprints": []string{"8f41ab8d32fca7f9a2b0fdd09a0e2ccf31e5579cd3a0b65f1a9f2f2ec8f38d4a"},
|
||||
"verify_headers": maps.Map{
|
||||
"X-Edge-Resolve-AppId": params.AppId,
|
||||
"X-Edge-Resolve-Expire": "1768953600",
|
||||
"X-Edge-Resolve-Nonce": "mock-nonce-123456",
|
||||
"X-Edge-Resolve-Target": params.Domain,
|
||||
"X-Edge-Resolve-Sign": "mock-signature-base64url",
|
||||
},
|
||||
},
|
||||
}
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func parseSDNSParamsJSON(raw string) ([]maps.Map, error) {
|
||||
if len(raw) == 0 {
|
||||
return []maps.Map{}, nil
|
||||
}
|
||||
|
||||
list := []maps.Map{}
|
||||
if err := json.Unmarshal([]byte(raw), &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) > 10 {
|
||||
return nil, fmt.Errorf("sdns params should be <= 10")
|
||||
}
|
||||
|
||||
result := make([]maps.Map, 0, len(list))
|
||||
for _, item := range list {
|
||||
name := strings.TrimSpace(item.GetString("name"))
|
||||
value := strings.TrimSpace(item.GetString("value"))
|
||||
if len(name) == 0 && len(value) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(name) == 0 || len(name) > 64 {
|
||||
return nil, fmt.Errorf("sdns param name should be in 1-64")
|
||||
}
|
||||
if len(value) == 0 || len(value) > 64 {
|
||||
return nil, fmt.Errorf("sdns param value should be in 1-64")
|
||||
}
|
||||
result = append(result, maps.Map{
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
if len(result) > 10 {
|
||||
return nil, fmt.Errorf("sdns params should be <= 10")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (this *TestAction) maskSubnet(ip string, ipv4Prefix int, ipv6Prefix int) string {
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ipv4 := parsed.To4()
|
||||
if ipv4 != nil {
|
||||
mask := net.CIDRMask(ipv4Prefix, 32)
|
||||
return ipv4.Mask(mask).String() + "/" + strconv.Itoa(ipv4Prefix)
|
||||
}
|
||||
|
||||
mask := net.CIDRMask(ipv6Prefix, 128)
|
||||
return parsed.Mask(mask).String() + "/" + strconv.Itoa(ipv6Prefix)
|
||||
}
|
||||
@@ -113,6 +113,50 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"code": "httpdns",
|
||||
"module": configloaders.AdminModuleCodeHttpDNS,
|
||||
"name": "HTTPDNS",
|
||||
"subtitle": "",
|
||||
"icon": "shield alternate",
|
||||
"subItems": []maps.Map{
|
||||
{
|
||||
"name": "集群管理",
|
||||
"url": "/httpdns/clusters",
|
||||
"code": "cluster",
|
||||
},
|
||||
{
|
||||
"name": "全局配置",
|
||||
"url": "/httpdns/policies",
|
||||
"code": "policy",
|
||||
},
|
||||
{
|
||||
"name": "应用管理",
|
||||
"url": "/httpdns/apps",
|
||||
"code": "app",
|
||||
},
|
||||
{
|
||||
"name": "SDK接入引导",
|
||||
"url": "/httpdns/guide",
|
||||
"code": "guide",
|
||||
},
|
||||
{
|
||||
"name": "解析日志",
|
||||
"url": "/httpdns/resolveLogs",
|
||||
"code": "resolveLogs",
|
||||
},
|
||||
{
|
||||
"name": "运行日志",
|
||||
"url": "/httpdns/runtimeLogs",
|
||||
"code": "runtimeLogs",
|
||||
},
|
||||
{
|
||||
"name": "解析测试",
|
||||
"url": "/httpdns/sandbox",
|
||||
"code": "sandbox",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"code": "dns",
|
||||
"module": configloaders.AdminModuleCodeDNS,
|
||||
|
||||
@@ -178,6 +178,50 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"code": "httpdns",
|
||||
"module": configloaders.AdminModuleCodeHttpDNS,
|
||||
"name": "HTTPDNS",
|
||||
"subtitle": "",
|
||||
"icon": "shield alternate",
|
||||
"subItems": []maps.Map{
|
||||
{
|
||||
"name": "集群管理",
|
||||
"url": "/httpdns/clusters",
|
||||
"code": "cluster",
|
||||
},
|
||||
{
|
||||
"name": "全局配置",
|
||||
"url": "/httpdns/policies",
|
||||
"code": "policy",
|
||||
},
|
||||
{
|
||||
"name": "应用管理",
|
||||
"url": "/httpdns/apps",
|
||||
"code": "app",
|
||||
},
|
||||
{
|
||||
"name": "SDK接入引导",
|
||||
"url": "/httpdns/guide",
|
||||
"code": "guide",
|
||||
},
|
||||
{
|
||||
"name": "解析日志",
|
||||
"url": "/httpdns/resolveLogs",
|
||||
"code": "resolveLogs",
|
||||
},
|
||||
{
|
||||
"name": "运行日志",
|
||||
"url": "/httpdns/runtimeLogs",
|
||||
"code": "runtimeLogs",
|
||||
},
|
||||
{
|
||||
"name": "解析测试",
|
||||
"url": "/httpdns/sandbox",
|
||||
"code": "sandbox",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"code": "dns",
|
||||
"module": configloaders.AdminModuleCodeDNS,
|
||||
|
||||
@@ -136,4 +136,15 @@ import (
|
||||
|
||||
// 平台用户
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/users"
|
||||
|
||||
// HTTPDNS体系
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/clusters"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/ech"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/guide"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/resolveLogs"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/runtimeLogs"
|
||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/sandbox"
|
||||
)
|
||||
|
||||
4
EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
Normal file
4
EdgeAdmin/web/views/@default/httpdns/apps/@menu.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<first-menu>
|
||||
<menu-item href="/httpdns/apps" code="index">应用列表</menu-item>
|
||||
<a href="" class="item" @click.prevent="createApp()">[添加应用]</a>
|
||||
</first-menu>
|
||||
116
EdgeAdmin/web/views/@default/httpdns/apps/appSettings.html
Normal file
116
EdgeAdmin/web/views/@default/httpdns/apps/appSettings.html
Normal file
@@ -0,0 +1,116 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<style>
|
||||
.httpdns-settings-grid {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.httpdns-settings-grid .left-nav-column {
|
||||
max-width: 220px;
|
||||
}
|
||||
.httpdns-settings-grid .right-form-column {
|
||||
padding-left: .3em !important;
|
||||
}
|
||||
.httpdns-side-menu .item {
|
||||
padding-top: .8em !important;
|
||||
padding-bottom: .8em !important;
|
||||
}
|
||||
.httpdns-mini-action {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-left: .55em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.httpdns-mini-action:hover {
|
||||
color: #1e70bf;
|
||||
}
|
||||
.httpdns-mini-action .icon {
|
||||
margin-right: 0 !important;
|
||||
font-size: .92em !important;
|
||||
}
|
||||
.httpdns-note.comment {
|
||||
color: #8f9aa6 !important;
|
||||
font-size: 12px;
|
||||
margin-top: .45em !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<div class="ui menu text blue">
|
||||
<div class="item"><strong>{{app.name}}</strong> (<code>{{settings.appId}}</code>)</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
|
||||
<div class="ui stackable grid httpdns-settings-grid">
|
||||
<div class="three wide computer four wide tablet sixteen wide mobile column left-nav-column">
|
||||
<div class="ui fluid vertical pointing menu httpdns-side-menu">
|
||||
<a href="" class="item" :class="{active: activeSection == 'basic'}" @click.prevent="activeSection='basic'">基础配置</a>
|
||||
<a href="" class="item" :class="{active: activeSection == 'auth'}" @click.prevent="activeSection='auth'">认证与密钥</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="thirteen wide computer twelve wide tablet sixteen wide mobile column right-form-column">
|
||||
<table class="ui table selectable definition" v-show="activeSection == 'basic'">
|
||||
<tr>
|
||||
<td class="title">App ID</td>
|
||||
<td><code>{{settings.appId}}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">应用启用</td>
|
||||
<td><checkbox name="appStatus" value="1" v-model="settings.appStatus"></checkbox></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="title">SNI 防护配置</td>
|
||||
<td>
|
||||
<span class="green">隐匿 SNI</span>
|
||||
<p class="comment httpdns-note">当前默认采用隐匿 SNI 策略,避免在 TLS 握手阶段暴露业务域名。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="ui table selectable definition" v-show="activeSection == 'auth'">
|
||||
<tr>
|
||||
<td class="title">请求验签</td>
|
||||
<td>
|
||||
<span class="ui label tiny green" v-if="settings.signEnabled">已开启</span>
|
||||
<span class="ui label tiny basic" v-else>已关闭</span>
|
||||
<a href="" class="ui mini button" :class="settings.signEnabled ? 'basic' : 'primary'" style="margin-left: .8em;" @click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
|
||||
<p class="comment httpdns-note">打开后,服务端会对请求进行签名校验。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">加签 Secret</td>
|
||||
<td>
|
||||
<code>{{signSecretVisible ? settings.signSecretPlain : settings.signSecretMasked}}</code>
|
||||
<a href="" class="httpdns-mini-action" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
|
||||
<a href="" class="httpdns-mini-action" title="复制加签 Secret" @click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i class="copy outline icon"></i></a>
|
||||
<a href="" class="httpdns-mini-action" @click.prevent="resetSignSecret">[重置]</a>
|
||||
<p class="comment httpdns-note">最近更新:{{settings.signSecretUpdatedAt}}</p>
|
||||
<p class="comment httpdns-note" v-if="!settings.signEnabled">请求验签已关闭,当前不使用加签 Secret。</p>
|
||||
<p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">AES 数据加密 Secret</td>
|
||||
<td>
|
||||
<code>{{aesSecretVisible ? settings.aesSecretPlain : settings.aesSecretMasked}}</code>
|
||||
<a href="" class="httpdns-mini-action" @click.prevent="aesSecretVisible = !aesSecretVisible" :title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
|
||||
<a href="" class="httpdns-mini-action" title="复制 AES 数据加密 Secret" @click.prevent="copySecret(settings.aesSecretPlain, 'AES Secret')"><i class="copy outline icon"></i></a>
|
||||
<a href="" class="httpdns-mini-action" @click.prevent="resetAESSecret">[重置]</a>
|
||||
<p class="comment httpdns-note">最近更新:{{settings.aesSecretUpdatedAt}}</p>
|
||||
<p class="comment httpdns-note">用于解析接口数据加密的密钥。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
111
EdgeAdmin/web/views/@default/httpdns/apps/appSettings.js
Normal file
111
EdgeAdmin/web/views/@default/httpdns/apps/appSettings.js
Normal file
@@ -0,0 +1,111 @@
|
||||
Tea.context(function () {
|
||||
this.activeSection = "basic";
|
||||
this.success = NotifyReloadSuccess("保存成功");
|
||||
this.signSecretVisible = false;
|
||||
this.aesSecretVisible = false;
|
||||
|
||||
this.toggleSignEnabled = function () {
|
||||
let that = this;
|
||||
let targetIsOn = !this.settings.signEnabled;
|
||||
|
||||
if (targetIsOn) {
|
||||
teaweb.confirm("html:开启后,服务端将会对解析请求进行验签鉴权,<span class='red'>未签名、签名无效或过期的请求都解析失败</span>,确认开启吗?", function () {
|
||||
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
|
||||
.params({
|
||||
appId: that.app.id,
|
||||
isOn: 1
|
||||
})
|
||||
.success(function () {
|
||||
that.settings.signEnabled = true;
|
||||
teaweb.success("请求验签已开启");
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
teaweb.confirm("html:关闭后,服务端将不会对解析请求进行验签鉴权,可能<span class='red'>存在被刷风险</span>,确认关闭吗?", function () {
|
||||
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
|
||||
.params({
|
||||
appId: that.app.id,
|
||||
isOn: 0
|
||||
})
|
||||
.success(function () {
|
||||
that.settings.signEnabled = false;
|
||||
teaweb.success("请求验签已关闭");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.copySecret = function (text, name) {
|
||||
if (typeof text != "string" || text.length == 0) {
|
||||
teaweb.warn("没有可复制的内容");
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
teaweb.success(name + "已复制");
|
||||
}).catch(function () {
|
||||
this.copyByTextarea(text, name);
|
||||
}.bind(this));
|
||||
return;
|
||||
}
|
||||
|
||||
this.copyByTextarea(text, name);
|
||||
};
|
||||
|
||||
this.copyByTextarea = function (text, name) {
|
||||
let input = document.createElement("textarea");
|
||||
input.value = text;
|
||||
input.setAttribute("readonly", "readonly");
|
||||
input.style.position = "fixed";
|
||||
input.style.left = "-10000px";
|
||||
input.style.top = "-10000px";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
|
||||
let ok = false;
|
||||
try {
|
||||
ok = document.execCommand("copy");
|
||||
} catch (e) {
|
||||
ok = false;
|
||||
}
|
||||
document.body.removeChild(input);
|
||||
|
||||
if (ok) {
|
||||
teaweb.success(name + "已复制");
|
||||
} else {
|
||||
teaweb.warn("复制失败,请手动复制");
|
||||
}
|
||||
};
|
||||
|
||||
this.resetSignSecret = function () {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要重置加签 Secret 吗?", function () {
|
||||
that.$post("/httpdns/apps/app/settings/resetSignSecret")
|
||||
.params({
|
||||
appId: that.app.id
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success("加签 Secret 已重置", function () {
|
||||
teaweb.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.resetAESSecret = function () {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要重置 AES Secret 吗?", function () {
|
||||
that.$post("/httpdns/apps/app/settings/resetAESSecret")
|
||||
.params({
|
||||
appId: that.app.id
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success("AES Secret 已重置", function () {
|
||||
teaweb.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
37
EdgeAdmin/web/views/@default/httpdns/apps/createPopup.html
Normal file
37
EdgeAdmin/web/views/@default/httpdns/apps/createPopup.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{$layout "layout_popup"}
|
||||
|
||||
<h3>添加应用</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$">
|
||||
<csrf-token></csrf-token>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">应用名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="name" maxlength="64" ref="focus" />
|
||||
<p class="comment">为该 HTTPDNS 应用设置一个便于识别的名称。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>所属集群 *</td>
|
||||
<td>
|
||||
<select class="ui dropdown" name="clusterId" v-model="defaultClusterId">
|
||||
<option value="">[请选择集群]</option>
|
||||
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
|
||||
</select>
|
||||
<p class="comment">应用的解析请求将由所选集群下的网关节点处理。默认值来自“HTTPDNS 用户设置”。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>所属用户</td>
|
||||
<td>
|
||||
<select class="ui dropdown" name="userId">
|
||||
<option value="0">[平台自用 / 不指定]</option>
|
||||
<option v-for="user in users" :value="user.id">{{user.name}} ({{user.username}})</option>
|
||||
</select>
|
||||
<p class="comment">可分配给指定租户用户;留空表示平台管理员自用。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
66
EdgeAdmin/web/views/@default/httpdns/apps/customRecords.html
Normal file
66
EdgeAdmin/web/views/@default/httpdns/apps/customRecords.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<style>
|
||||
.httpdns-col-ttl {
|
||||
width: 72px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.httpdns-col-actions {
|
||||
width: 130px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<div class="ui menu text blue">
|
||||
<div class="item"><strong>{{app.name}}</strong> (<code>{{app.appId}}</code>)</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<div class="ui small message">
|
||||
当前域名:<strong>{{domain.name}}</strong>
|
||||
</div>
|
||||
|
||||
<a href="" @click.prevent="createRecord" class="ui button primary small" :class="{disabled: !domain || !domain.id}">
|
||||
<i class="icon plus"></i> 新增自定义解析规则
|
||||
</a>
|
||||
<a :href="'/httpdns/apps/domains?appId=' + app.id" class="ui button small">返回域名管理</a>
|
||||
|
||||
<table class="ui table selectable celled" v-if="records.length > 0" style="margin-top: 1em;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>线路</th>
|
||||
<th>规则名称</th>
|
||||
<th>SDNS 参数</th>
|
||||
<th>解析记录</th>
|
||||
<th class="httpdns-col-ttl">TTL</th>
|
||||
<th class="width10">状态</th>
|
||||
<th class="httpdns-col-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="record in records">
|
||||
<td>{{record.lineText}}</td>
|
||||
<td>{{record.ruleName}}</td>
|
||||
<td>{{record.paramsText}}</td>
|
||||
<td>
|
||||
{{record.recordValueText}}
|
||||
</td>
|
||||
<td class="httpdns-col-ttl">{{record.ttl}}s</td>
|
||||
<td>
|
||||
<span class="green" v-if="record.isOn">已启用</span>
|
||||
<span class="grey" v-else>已停用</span>
|
||||
</td>
|
||||
<td class="httpdns-col-actions">
|
||||
<a href="" @click.prevent="editRecord(record.id)">编辑</a> |
|
||||
<a href="" @click.prevent="toggleRecord(record)">{{record.isOn ? "停用" : "启用"}}</a> |
|
||||
<a href="" @click.prevent="deleteRecord(record.id)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="ui warning message" v-if="!domain || !domain.id">当前应用暂无可用域名,请先到域名管理中添加域名。</p>
|
||||
<p class="ui message" v-else-if="records.length == 0">当前域名还没有自定义解析规则。</p>
|
||||
</div>
|
||||
50
EdgeAdmin/web/views/@default/httpdns/apps/customRecords.js
Normal file
50
EdgeAdmin/web/views/@default/httpdns/apps/customRecords.js
Normal file
@@ -0,0 +1,50 @@
|
||||
Tea.context(function () {
|
||||
if (typeof this.records == "undefined") {
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
this.createRecord = function () {
|
||||
if (!this.domain || !this.domain.id) {
|
||||
return;
|
||||
}
|
||||
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id, {
|
||||
width: "56em",
|
||||
height: "40em",
|
||||
title: "新增自定义解析规则"
|
||||
});
|
||||
};
|
||||
|
||||
this.editRecord = function (recordId) {
|
||||
if (!this.domain || !this.domain.id) {
|
||||
return;
|
||||
}
|
||||
teaweb.popup("/httpdns/apps/customRecords/createPopup?appId=" + this.app.id + "&domainId=" + this.domain.id + "&recordId=" + recordId, {
|
||||
width: "56em",
|
||||
height: "40em",
|
||||
title: "编辑自定义解析规则"
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteRecord = function (recordId) {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要删除这条自定义解析规则吗?", function () {
|
||||
that.$post("/httpdns/apps/customRecords/delete")
|
||||
.params({
|
||||
appId: that.app.id,
|
||||
recordId: recordId
|
||||
})
|
||||
.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
this.toggleRecord = function (record) {
|
||||
let that = this;
|
||||
that.$post("/httpdns/apps/customRecords/toggle")
|
||||
.params({
|
||||
appId: that.app.id,
|
||||
recordId: record.id,
|
||||
isOn: record.isOn ? 0 : 1
|
||||
})
|
||||
.refresh();
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
{$layout "layout_popup"}
|
||||
|
||||
<style>
|
||||
.httpdns-inline-actions {
|
||||
margin-top: .6em;
|
||||
}
|
||||
.httpdns-inline-actions .count {
|
||||
color: #8f9aa6;
|
||||
margin-left: .4em;
|
||||
}
|
||||
.httpdns-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
margin-bottom: .45em;
|
||||
}
|
||||
.httpdns-row .field {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.httpdns-row .field.flex {
|
||||
flex: 1;
|
||||
}
|
||||
.httpdns-line-row {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: .5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3 v-if="isEditing">编辑自定义解析规则</h3>
|
||||
<h3 v-else>新增自定义解析规则</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
<input type="hidden" name="domainId" :value="domain.id" />
|
||||
<input type="hidden" name="domain" :value="record.domain" />
|
||||
<input type="hidden" name="recordId" :value="record.id" />
|
||||
<input type="hidden" name="sdnsParamsJSON" :value="JSON.stringify(sdnsParams)" />
|
||||
<input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" />
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">域名 *</td>
|
||||
<td><strong>{{record.domain}}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">规则名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="ruleName" maxlength="50" v-model="record.ruleName" ref="focus" />
|
||||
<p class="comment">例如:上海电信灰度-v2。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">线路</td>
|
||||
<td>
|
||||
<div class="httpdns-line-row">
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineScope" v-model="record.lineScope" @change="onLineScopeChange">
|
||||
<option value="china">中国大陆</option>
|
||||
<option value="overseas">港澳台及境外</option>
|
||||
</select>
|
||||
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineCarrier" v-if="record.lineScope == 'china'" v-model="record.lineCarrier">
|
||||
<option v-for="carrier in chinaCarriers" :value="carrier">{{carrier}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineRegion" v-if="record.lineScope == 'china'" v-model="record.lineRegion" @change="onChinaRegionChange">
|
||||
<option v-for="region in chinaRegions" :value="region">{{region}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineProvince" v-if="record.lineScope == 'china'" v-model="record.lineProvince">
|
||||
<option v-for="province in provinceOptions" :value="province">{{province}}</option>
|
||||
</select>
|
||||
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineContinent" v-if="record.lineScope == 'overseas'" v-model="record.lineContinent" @change="onContinentChange">
|
||||
<option v-for="continent in continents" :value="continent">{{continent}}</option>
|
||||
</select>
|
||||
<select class="ui dropdown auto-width" style="display:inline-block !important;width:auto !important;margin:0 !important;" name="lineCountry" v-if="record.lineScope == 'overseas'" v-model="record.lineCountry">
|
||||
<option v-for="country in countryOptions" :value="country">{{country}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">SDNS 参数配置</td>
|
||||
<td>
|
||||
<div class="httpdns-row" v-for="(param, index) in sdnsParams">
|
||||
<div class="field flex">
|
||||
<input type="text" maxlength="64" placeholder="参数名称" v-model="param.name" />
|
||||
</div>
|
||||
<div class="field flex">
|
||||
<input type="text" maxlength="64" placeholder="参数值" v-model="param.value" />
|
||||
</div>
|
||||
<a href="" @click.prevent="removeSDNSParam(index)" title="删除"><i class="icon trash alternate outline"></i></a>
|
||||
</div>
|
||||
<div class="httpdns-inline-actions">
|
||||
<a href="" @click.prevent="addSDNSParam" :class="{disabled: sdnsParams.length >= 10}">
|
||||
<i class="icon plus circle"></i>添加参数
|
||||
</a>
|
||||
<span class="count">{{sdnsParams.length}}/10</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">解析记录值 *</td>
|
||||
<td>
|
||||
<div style="float:right;">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="weightEnabled" value="1" v-model="record.weightEnabled" />
|
||||
<label>按权重调度</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="httpdns-row" v-for="(item, index) in recordItems">
|
||||
<div class="field">
|
||||
<select class="ui dropdown auto-width" v-model="item.type">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field flex">
|
||||
<input type="text" placeholder="记录值(与记录类型匹配)" v-model="item.value" />
|
||||
</div>
|
||||
<div class="field" v-if="record.weightEnabled">
|
||||
<input type="text" style="width:8em;" placeholder="权重(1-100)" v-model="item.weight" />
|
||||
</div>
|
||||
<a href="" @click.prevent="removeRecordItem(index)" title="删除"><i class="icon trash alternate outline"></i></a>
|
||||
</div>
|
||||
<div class="httpdns-inline-actions">
|
||||
<a href="" @click.prevent="addRecordItem" :class="{disabled: recordItems.length >= 10}">
|
||||
<i class="icon plus circle"></i>添加记录值
|
||||
</a>
|
||||
<span class="count">{{recordItems.length}}/10</span>
|
||||
</div>
|
||||
<p class="comment" v-if="record.weightEnabled">开启后每条记录可配置权重,范围 1-100。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">TTL *</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" name="ttl" maxlength="5" style="width:8em;" v-model="record.ttl" />
|
||||
<span class="ui basic label">秒</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">规则状态</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="isOn" value="1" v-model="record.isOn" />
|
||||
<label>启用</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
@@ -0,0 +1,177 @@
|
||||
Tea.context(function () {
|
||||
var vm = this;
|
||||
|
||||
if (typeof vm.record == "undefined" || vm.record == null) {
|
||||
vm.record = {};
|
||||
}
|
||||
|
||||
vm.chinaCarriers = ["默认", "电信", "联通", "移动", "教育网", "鹏博士", "广电"];
|
||||
vm.chinaRegions = ["默认", "东北", "华北", "华东", "华南", "华中", "西北", "西南"];
|
||||
vm.chinaRegionProvinces = {
|
||||
"默认": ["默认"],
|
||||
"东北": ["默认", "辽宁", "吉林", "黑龙江"],
|
||||
"华北": ["默认", "北京", "天津", "河北", "山西", "内蒙古"],
|
||||
"华东": ["默认", "上海", "江苏", "浙江", "安徽", "福建", "江西", "山东"],
|
||||
"华南": ["默认", "广东", "广西", "海南"],
|
||||
"华中": ["默认", "河南", "湖北", "湖南"],
|
||||
"西北": ["默认", "陕西", "甘肃", "青海", "宁夏", "新疆"],
|
||||
"西南": ["默认", "重庆", "四川", "贵州", "云南", "西藏"]
|
||||
};
|
||||
|
||||
vm.continents = ["默认", "非洲", "南极洲", "亚洲", "欧洲", "北美洲", "南美洲", "大洋洲"];
|
||||
vm.continentCountries = {
|
||||
"默认": ["默认"],
|
||||
"非洲": ["默认", "南非", "埃及", "尼日利亚", "肯尼亚", "摩洛哥"],
|
||||
"南极洲": ["默认"],
|
||||
"亚洲": ["默认", "中国香港", "中国澳门", "中国台湾", "日本", "韩国", "新加坡", "印度", "泰国", "越南"],
|
||||
"欧洲": ["默认", "德国", "英国", "法国", "荷兰", "西班牙", "意大利", "俄罗斯"],
|
||||
"北美洲": ["默认", "美国", "加拿大", "墨西哥"],
|
||||
"南美洲": ["默认", "巴西", "阿根廷", "智利", "哥伦比亚"],
|
||||
"大洋洲": ["默认", "澳大利亚", "新西兰"]
|
||||
};
|
||||
|
||||
vm.parseJSONList = function (raw) {
|
||||
if (typeof raw == "undefined" || raw == null || raw == "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
try {
|
||||
var list = JSON.parse(raw);
|
||||
if (Array.isArray(list)) {
|
||||
return list;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
vm.normalizeBoolean = function (value, defaultValue) {
|
||||
if (typeof value == "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value == "number") {
|
||||
return value > 0;
|
||||
}
|
||||
if (typeof value == "string") {
|
||||
value = value.toLowerCase();
|
||||
return value == "1" || value == "true" || value == "yes" || value == "on";
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
vm.record.lineScope = vm.record.lineScope == "overseas" ? "overseas" : "china";
|
||||
vm.record.lineCarrier = vm.record.lineCarrier || "默认";
|
||||
vm.record.lineRegion = vm.record.lineRegion || "默认";
|
||||
vm.record.lineProvince = vm.record.lineProvince || "默认";
|
||||
vm.record.lineContinent = vm.record.lineContinent || "默认";
|
||||
vm.record.lineCountry = vm.record.lineCountry || "默认";
|
||||
vm.record.ruleName = vm.record.ruleName || "";
|
||||
vm.record.ttl = vm.record.ttl || 30;
|
||||
vm.record.weightEnabled = vm.normalizeBoolean(vm.record.weightEnabled, false);
|
||||
vm.record.isOn = vm.normalizeBoolean(vm.record.isOn, true);
|
||||
|
||||
vm.sdnsParams = vm.parseJSONList(vm.record.sdnsParamsJson);
|
||||
if (vm.sdnsParams.length == 0) {
|
||||
vm.sdnsParams.push({name: "", value: ""});
|
||||
}
|
||||
|
||||
vm.recordItems = vm.parseJSONList(vm.record.recordItemsJson);
|
||||
if (vm.recordItems.length == 0) {
|
||||
vm.recordItems.push({type: "A", value: "", weight: 100});
|
||||
} else {
|
||||
for (var i = 0; i < vm.recordItems.length; i++) {
|
||||
var item = vm.recordItems[i];
|
||||
if (item.type != "A" && item.type != "AAAA") {
|
||||
item.type = "A";
|
||||
}
|
||||
if (typeof item.weight == "undefined" || item.weight == null || item.weight === "") {
|
||||
item.weight = 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.provinceOptions = ["默认"];
|
||||
vm.countryOptions = ["默认"];
|
||||
|
||||
vm.refreshProvinceOptions = function () {
|
||||
var provinces = vm.chinaRegionProvinces[vm.record.lineRegion];
|
||||
if (!Array.isArray(provinces) || provinces.length == 0) {
|
||||
provinces = ["默认"];
|
||||
}
|
||||
vm.provinceOptions = provinces;
|
||||
if (vm.provinceOptions.indexOf(vm.record.lineProvince) < 0) {
|
||||
vm.record.lineProvince = vm.provinceOptions[0];
|
||||
}
|
||||
};
|
||||
|
||||
vm.refreshCountryOptions = function () {
|
||||
var countries = vm.continentCountries[vm.record.lineContinent];
|
||||
if (!Array.isArray(countries) || countries.length == 0) {
|
||||
countries = ["默认"];
|
||||
}
|
||||
vm.countryOptions = countries;
|
||||
if (vm.countryOptions.indexOf(vm.record.lineCountry) < 0) {
|
||||
vm.record.lineCountry = vm.countryOptions[0];
|
||||
}
|
||||
};
|
||||
|
||||
vm.onChinaRegionChange = function () {
|
||||
vm.refreshProvinceOptions();
|
||||
};
|
||||
|
||||
vm.onContinentChange = function () {
|
||||
vm.refreshCountryOptions();
|
||||
};
|
||||
|
||||
vm.onLineScopeChange = function () {
|
||||
if (vm.record.lineScope == "overseas") {
|
||||
vm.record.lineContinent = vm.record.lineContinent || "默认";
|
||||
vm.refreshCountryOptions();
|
||||
} else {
|
||||
vm.record.lineCarrier = vm.record.lineCarrier || "默认";
|
||||
vm.record.lineRegion = vm.record.lineRegion || "默认";
|
||||
vm.refreshProvinceOptions();
|
||||
}
|
||||
};
|
||||
|
||||
vm.addSDNSParam = function () {
|
||||
if (vm.sdnsParams.length >= 10) {
|
||||
return;
|
||||
}
|
||||
vm.sdnsParams.push({name: "", value: ""});
|
||||
};
|
||||
|
||||
vm.removeSDNSParam = function (index) {
|
||||
if (index < 0 || index >= vm.sdnsParams.length) {
|
||||
return;
|
||||
}
|
||||
vm.sdnsParams.splice(index, 1);
|
||||
if (vm.sdnsParams.length == 0) {
|
||||
vm.sdnsParams.push({name: "", value: ""});
|
||||
}
|
||||
};
|
||||
|
||||
vm.addRecordItem = function () {
|
||||
if (vm.recordItems.length >= 10) {
|
||||
return;
|
||||
}
|
||||
vm.recordItems.push({type: "A", value: "", weight: 100});
|
||||
};
|
||||
|
||||
vm.removeRecordItem = function (index) {
|
||||
if (index < 0 || index >= vm.recordItems.length) {
|
||||
return;
|
||||
}
|
||||
vm.recordItems.splice(index, 1);
|
||||
if (vm.recordItems.length == 0) {
|
||||
vm.recordItems.push({type: "A", value: "", weight: 100});
|
||||
}
|
||||
};
|
||||
|
||||
vm.onLineScopeChange();
|
||||
});
|
||||
35
EdgeAdmin/web/views/@default/httpdns/apps/domains.html
Normal file
35
EdgeAdmin/web/views/@default/httpdns/apps/domains.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<div>
|
||||
<div class="ui menu text blue">
|
||||
<div class="item"><strong>{{app.name}}</strong> (<code>{{app.appId}}</code>)</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<a href="" @click.prevent="bindDomain" class="ui button primary small"><i class="icon plus"></i>添加域名</a>
|
||||
|
||||
<table class="ui table selectable celled" v-if="domains.length > 0" style="margin-top: 1em;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>域名列表</th>
|
||||
<th class="two wide">规则策略</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="domain in domains">
|
||||
<td><strong>{{domain.name}}</strong></td>
|
||||
<td>
|
||||
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">{{domain.customRecordCount}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">自定义解析</a> |
|
||||
<a href="" @click.prevent="deleteDomain(domain.id)">解绑</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="ui message" v-if="domains.length == 0">该应用尚未绑定域名。</p>
|
||||
</div>
|
||||
24
EdgeAdmin/web/views/@default/httpdns/apps/domains.js
Normal file
24
EdgeAdmin/web/views/@default/httpdns/apps/domains.js
Normal file
@@ -0,0 +1,24 @@
|
||||
Tea.context(function () {
|
||||
if (typeof this.domains == "undefined") {
|
||||
this.domains = [];
|
||||
}
|
||||
|
||||
this.bindDomain = function () {
|
||||
teaweb.popup("/httpdns/apps/domains/createPopup?appId=" + this.app.id, {
|
||||
height: "24em",
|
||||
width: "46em",
|
||||
title: "添加域名"
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteDomain = function (domainId) {
|
||||
let that = this;
|
||||
teaweb.confirm("确定要解绑这个域名吗?", function () {
|
||||
that.$post("/httpdns/apps/domains/delete")
|
||||
.params({
|
||||
domainId: domainId
|
||||
})
|
||||
.refresh();
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{$layout "layout_popup"}
|
||||
|
||||
<h3>添加域名</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$">
|
||||
<csrf-token></csrf-token>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">域名 *</td>
|
||||
<td>
|
||||
<input type="text" name="domain" maxlength="255" placeholder="api.example.com" ref="focus" />
|
||||
<p class="comment">请输入完整 FQDN,例如 <code>api.example.com</code>。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
53
EdgeAdmin/web/views/@default/httpdns/apps/index.html
Normal file
53
EdgeAdmin/web/views/@default/httpdns/apps/index.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<div>
|
||||
<form method="get" class="ui form" action="/httpdns/apps">
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="keyword" style="width:16em" placeholder="AppID 或应用名称..." :value="keyword" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button type="submit" class="ui button">查询</button>
|
||||
<a href="/httpdns/apps" v-if="keyword.length > 0">[清除条件]</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="ui message" v-if="apps.length == 0">暂时没有符合条件的 HTTPDNS 应用。</p>
|
||||
|
||||
<table class="ui table selectable celled" v-if="apps.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>应用名称</th>
|
||||
<th>AppID</th>
|
||||
<th>绑定域名数</th>
|
||||
<th class="one wide center">状态</th>
|
||||
<th class="tz op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="app in apps">
|
||||
<td>
|
||||
<a :href="'/httpdns/apps/app?appId=' + app.id">
|
||||
<strong><keyword :v-word="keyword">{{app.name}}</keyword></strong>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{app.appId}}</code>
|
||||
<copy-icon :text="app.appId"></copy-icon>
|
||||
</td>
|
||||
<td><a :href="'/httpdns/apps/domains?appId=' + app.id">{{app.domainCount}}</a></td>
|
||||
<td class="center">
|
||||
<label-on :v-is-on="app.isOn"></label-on>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="'/httpdns/apps/app?appId=' + app.id">域名管理</a> |
|
||||
<a :href="'/httpdns/apps/app/settings?appId=' + app.id">应用设置</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
</div>
|
||||
13
EdgeAdmin/web/views/@default/httpdns/apps/index.js
Normal file
13
EdgeAdmin/web/views/@default/httpdns/apps/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Tea.context(function () {
|
||||
if (typeof this.apps == "undefined") {
|
||||
this.apps = [];
|
||||
}
|
||||
|
||||
this.createApp = function () {
|
||||
teaweb.popup("/httpdns/apps/createPopup", {
|
||||
height: "26em",
|
||||
width: "48em",
|
||||
title: "添加应用"
|
||||
});
|
||||
};
|
||||
});
|
||||
78
EdgeAdmin/web/views/@default/httpdns/apps/policies.html
Normal file
78
EdgeAdmin/web/views/@default/httpdns/apps/policies.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
|
||||
<div class="ui margin"></div>
|
||||
|
||||
<h3 class="ui header">HTTPDNS 鍏ㄥ眬绛栫暐</h3>
|
||||
|
||||
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">鍏ㄥ眬榛樿瑙f瀽 TTL</td>
|
||||
<td><input type="text" name="defaultTTL" maxlength="8" v-model="policies.defaultTTL" /> 绉?/td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">鍏ㄥ眬榛樿 SNI 绛夌骇</td>
|
||||
<td>
|
||||
<select name="defaultSniPolicy" class="ui dropdown auto-width" v-model="policies.defaultSniPolicy">
|
||||
<option value="level1">level1锛堝浐瀹?SNI锛?/option>
|
||||
<option value="level2">level2锛堥殣鍖?SNI锛?/option>
|
||||
<option value="level3">level3锛圗CH锛?/option>
|
||||
</select>
|
||||
<div class="grey small" v-if="policies.defaultSniPolicy == 'level1'" style="margin-top:.5em;">
|
||||
level1 浠呬娇鐢ㄥ浐瀹?SNI锛屼笉鍚敤 ECS 鍜岃瘉涔︽牎楠岀瓥鐣ャ€? </div>
|
||||
<div class="grey small" v-else-if="policies.defaultSniPolicy == 'level2'" style="margin-top:.5em;">
|
||||
level2 浠呭惎鐢ㄩ殣鍖?SNI锛屼笉瑕佹眰閰嶇疆 ECS 涓庤瘉涔︾瓥鐣ャ€? </div>
|
||||
<div class="grey small" v-else style="margin-top:.5em;">
|
||||
level3 鍚敤 ECH锛屽缓璁悓鏃堕厤缃?ECS 涓庤瘉涔︽牎楠岀瓥鐣ャ€? </div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">鍏ㄥ眬闄嶇骇瓒呮椂</td>
|
||||
<td><input type="text" name="defaultFallbackMs" maxlength="8" v-model="policies.defaultFallbackMs" /> 姣</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="policies.defaultSniPolicy == 'level3'">
|
||||
<td class="title">全局 ECS 掩码策略</td>
|
||||
<td>
|
||||
<select name="ecsMode" class="ui dropdown auto-width" v-model="policies.ecsMode">
|
||||
<option value="off">鍏抽棴</option>
|
||||
<option value="auto">鑷姩</option>
|
||||
<option value="custom">鑷畾涔?/option>
|
||||
</select>
|
||||
<span v-if="policies.ecsMode == 'custom'">
|
||||
<span class="grey" style="margin-left:.8em;">IPv4 /</span>
|
||||
<input type="text" name="ecsIPv4Prefix" maxlength="3" v-model="policies.ecsIPv4Prefix" style="width:4.5em;" />
|
||||
<span class="grey" style="margin-left:.8em;">IPv6 /</span>
|
||||
<input type="text" name="ecsIPv6Prefix" maxlength="3" v-model="policies.ecsIPv6Prefix" style="width:4.5em;" />
|
||||
</span>
|
||||
<span v-else class="grey" style="margin-left:.8em;">仅在“自定义”模式下配置掩码。</span>
|
||||
|
||||
<input type="hidden" name="ecsIPv4Prefix" v-model="policies.ecsIPv4Prefix" v-if="policies.ecsMode != 'custom'" />
|
||||
<input type="hidden" name="ecsIPv6Prefix" v-model="policies.ecsIPv6Prefix" v-if="policies.ecsMode != 'custom'" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="policies.defaultSniPolicy == 'level3'">
|
||||
<td class="title">鍏ㄥ眬璇佷功鏍¢獙绛栫暐</td>
|
||||
<td>
|
||||
<span class="grey">证书指纹校验(Pinning)</span>
|
||||
<select name="pinningMode" class="ui dropdown auto-width" v-model="policies.pinningMode" style="margin-left:.4em;">
|
||||
<option value="off">鍏抽棴</option>
|
||||
<option value="report">瑙傚療妯″紡</option>
|
||||
<option value="enforce">寮哄埗鏍¢獙</option>
|
||||
</select>
|
||||
<span class="grey" style="margin-left:1.2em;">证书 SAN 域名校验</span>
|
||||
<select name="sanMode" class="ui dropdown auto-width" v-model="policies.sanMode" style="margin-left:.4em;">
|
||||
<option value="off">鍏抽棴</option>
|
||||
<option value="report">瑙傚療妯″紡</option>
|
||||
<option value="strict">涓ユ牸鏍¢獙</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
|
||||
40
EdgeAdmin/web/views/@default/httpdns/apps/policies.js
Normal file
40
EdgeAdmin/web/views/@default/httpdns/apps/policies.js
Normal file
@@ -0,0 +1,40 @@
|
||||
Tea.context(function () {
|
||||
this.success = NotifyReloadSuccess("保存成功");
|
||||
|
||||
this.$delay(function () {
|
||||
this.$watch("policies.defaultSniPolicy", function (level) {
|
||||
if (level == "level1" || level == "level2") {
|
||||
this.policies.ecsMode = "off";
|
||||
this.policies.pinningMode = "off";
|
||||
this.policies.sanMode = "off";
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == "level3") {
|
||||
if (this.policies.ecsMode == "off") {
|
||||
this.policies.ecsMode = "auto";
|
||||
}
|
||||
if (this.policies.pinningMode == "off") {
|
||||
this.policies.pinningMode = "report";
|
||||
}
|
||||
if (this.policies.sanMode == "off") {
|
||||
this.policies.sanMode = "strict";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$watch("policies.ecsMode", function (mode) {
|
||||
if (this.policies.defaultSniPolicy != "level3") {
|
||||
return;
|
||||
}
|
||||
if (mode == "custom") {
|
||||
if (!this.policies.ecsIPv4Prefix || this.policies.ecsIPv4Prefix <= 0) {
|
||||
this.policies.ecsIPv4Prefix = 24;
|
||||
}
|
||||
if (!this.policies.ecsIPv6Prefix || this.policies.ecsIPv6Prefix <= 0) {
|
||||
this.policies.ecsIPv6Prefix = 56;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<second-menu>
|
||||
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</menu-item>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id" code="index">节点列表</menu-item>
|
||||
<menu-item :href="'/httpdns/clusters/createNode?clusterId=' + cluster.id" code="createNode">创建节点</menu-item>
|
||||
<menu-item :href="'/httpdns/clusters/cluster/settings?clusterId=' + cluster.id" code="settings">集群设置</menu-item>
|
||||
<menu-item :href="'/httpdns/clusters/delete?clusterId=' + cluster.id" code="delete">删除集群</menu-item>
|
||||
</second-menu>
|
||||
6
EdgeAdmin/web/views/@default/httpdns/clusters/@menu.html
Normal file
6
EdgeAdmin/web/views/@default/httpdns/clusters/@menu.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- 左侧菜单由 Go Backend 自动生成注入,此处加首行子菜单使其符合标准平台样式 -->
|
||||
<first-menu>
|
||||
<menu-item href="/httpdns/clusters" code="index">集群列表</menu-item>
|
||||
<span class="item disabled">|</span>
|
||||
<menu-item href="/httpdns/clusters/create" code="create">[创建新集群]</menu-item>
|
||||
</first-menu>
|
||||
30
EdgeAdmin/web/views/@default/httpdns/clusters/certs.html
Normal file
30
EdgeAdmin/web/views/@default/httpdns/clusters/certs.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{$layout}
|
||||
{$template "menu"}
|
||||
{$template "/left_menu_with_menu"}
|
||||
|
||||
<div class="right-box with-menu">
|
||||
<h3 class="ui header">HTTPS 接口证书</h3>
|
||||
<p class="comment">用于 HTTPDNS 接口证书管理与展示。</p>
|
||||
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45%;">证书名称</th>
|
||||
<th style="width:25%;">颁发机构</th>
|
||||
<th style="width:20%;">到期时间</th>
|
||||
<th style="width:10%;">证书ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="cert in certs">
|
||||
<td>{{cert.name}}</td>
|
||||
<td>{{cert.issuer}}</td>
|
||||
<td>{{cert.expiresAt}}</td>
|
||||
<td><code>{{cert.id}}</code></td>
|
||||
</tr>
|
||||
<tr v-if="!certs || certs.length == 0">
|
||||
<td colspan="4" class="grey">暂无证书。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
91
EdgeAdmin/web/views/@default/httpdns/clusters/cluster.html
Normal file
91
EdgeAdmin/web/views/@default/httpdns/clusters/cluster.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{$layout}
|
||||
{$template "cluster_menu"}
|
||||
<div class="ui margin"></div>
|
||||
|
||||
<div class="right-menu">
|
||||
<a :href="'/httpdns/clusters/createNode?clusterId=' + clusterId" class="item">[创建节点]</a>
|
||||
</div>
|
||||
|
||||
<form class="ui form" action="/httpdns/clusters/cluster">
|
||||
<input type="hidden" name="clusterId" :value="clusterId" />
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="installedState" v-model="installState">
|
||||
<option value="0">[安装状态]</option>
|
||||
<option value="1">已安装</option>
|
||||
<option value="2">未安装</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="activeState" v-model="activeState">
|
||||
<option value="0">[在线状态]</option>
|
||||
<option value="1">在线</option>
|
||||
<option value="2">离线</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" name="keyword" placeholder="节点名称、IP..." :value="keyword" />
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button class="ui button" type="submit">搜索</button>
|
||||
|
||||
<a :href="'/httpdns/clusters/cluster?clusterId=' + clusterId"
|
||||
v-if="installState > 0 || activeState > 0 || keyword.length > 0">[清除条件]</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="ui margin" v-if="!hasNodes"></div>
|
||||
<p class="comment" v-if="!hasNodes">当前集群下暂时还没有节点。</p>
|
||||
|
||||
<table class="ui table selectable celled" v-if="hasNodes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>节点名称</th>
|
||||
<th>IP地址</th>
|
||||
<th class="width-5 center">状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="node in nodes">
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
:href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id">{{node.name}}</a>
|
||||
<a :href="'/httpdns/clusters/cluster/node/update?clusterId=' + clusterId + '&nodeId=' + node.id"
|
||||
title="设置"><i class="icon setting grey"></i></a>
|
||||
<div v-if="node.region != null">
|
||||
<span class="grey small">{{node.region.name}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.ipAddresses.length == 0" class="disabled">-</span>
|
||||
<div v-for="addr in node.ipAddresses">
|
||||
<div class="ui label tiny basic">{{addr.ip}}
|
||||
<span class="small" v-if="addr.name.length > 0">({{addr.name}})</span>
|
||||
<span class="small red" v-if="!addr.canAccess" title="不公开访问">[内]</span>
|
||||
<span class="small red" v-if="!addr.isOn">[下线]</span>
|
||||
<span class="small red" v-if="!addr.isUp" title="健康检查失败">[宕机]</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="center">
|
||||
<div v-if="!node.isUp">
|
||||
<span class="red">宕机</span>
|
||||
</div>
|
||||
<div v-else-if="!node.isOn">
|
||||
<span class="disabled">未启用</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span v-if="node.status.isActive" class="green">在线</span>
|
||||
<span v-else class="disabled">离线</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id">详情</a>
|
||||
<a href="" @click.prevent="deleteNode(node.id)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="page" v-html="page"></div>
|
||||
17
EdgeAdmin/web/views/@default/httpdns/clusters/cluster.js
Normal file
17
EdgeAdmin/web/views/@default/httpdns/clusters/cluster.js
Normal file
@@ -0,0 +1,17 @@
|
||||
Tea.context(function () {
|
||||
this.deleteNode = function (nodeId) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此节点吗?", function () {
|
||||
that.$post("/httpdns/clusters/deleteNode")
|
||||
.params({
|
||||
clusterId: that.clusterId,
|
||||
nodeId: nodeId
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success("删除成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
<second-menu>
|
||||
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + currentCluster.id">{{currentCluster.name}}</menu-item>
|
||||
<span class="item disabled" style="padding-left: 0; padding-right: 0">»</span>
|
||||
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + clusterId">节点列表</menu-item>
|
||||
<span class="item disabled">|</span>
|
||||
<menu-item :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + nodeId" code="node">节点详情</menu-item>
|
||||
<menu-item :href="'/httpdns/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + nodeId" code="log">运行日志</menu-item>
|
||||
<menu-item :href="'/httpdns/clusters/cluster/node/update?clusterId=' + clusterId + '&nodeId=' + nodeId" code="update">修改设置</menu-item>
|
||||
<menu-item :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + nodeId" code="install">安装节点</menu-item>
|
||||
</second-menu>
|
||||
@@ -0,0 +1,4 @@
|
||||
a.underline {
|
||||
border-bottom: 1px #db2828 dashed;
|
||||
}
|
||||
/*# sourceMappingURL=index.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,CAAC;EACA,iCAAA","file":"index.css"}
|
||||
@@ -0,0 +1,191 @@
|
||||
{$layout}
|
||||
|
||||
{$template "node_menu"}
|
||||
|
||||
<h3>节点详情</h3>
|
||||
<table class="ui table definition selectable">
|
||||
<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>IP地址</td>
|
||||
<td>
|
||||
<div v-if="node.ipAddresses.length > 0">
|
||||
<div>
|
||||
<div v-for="(address, index) in node.ipAddresses" class="ui label tiny basic">
|
||||
{{address.ip}}
|
||||
<span class="small" v-if="address.name.length > 0">({{address.name}}<span v-if="!address.canAccess">,不公开访问</span>)</span>
|
||||
<span class="small red" v-if="!address.isOn">[off]</span>
|
||||
<span class="small red" v-if="!address.isUp">[down]</span>
|
||||
<span class="small" v-if="address.name.length == 0 && !address.canAccess">(不公开访问)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="disabled">暂时还没有填写 IP 地址。</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><more-options-indicator></more-options-indicator></td>
|
||||
</tr>
|
||||
<tbody v-show="moreOptionsVisible">
|
||||
<tr>
|
||||
<td>SSH主机地址</td>
|
||||
<td>
|
||||
<div v-if="node.login != null && node.login.params != null && node.login.params.host != null">
|
||||
<span v-if="node.login.params.host.length > 0">{{node.login.params.host}}</span>
|
||||
<span v-else class="disabled">尚未设置</span>
|
||||
</div>
|
||||
<div v-else class="disabled">尚未设置</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSH主机端口</td>
|
||||
<td>
|
||||
<div v-if="node.login != null && node.login.params != null && node.login.params.host != null">
|
||||
<span v-if="node.login.params.port > 0">{{node.login.params.port}}</span>
|
||||
<span v-else class="disabled">尚未设置</span>
|
||||
</div>
|
||||
<span v-else class="disabled">尚未设置</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSH登录认证</td>
|
||||
<td>
|
||||
<div v-if="node.login != null && node.login.grant != null && node.login.grant.id > 0">
|
||||
<a :href="'/clusters/grants/grant?grantId=' + node.login.grant.id">
|
||||
{{node.login.grant.name}}
|
||||
<span class="small grey">({{node.login.grant.methodName}})</span>
|
||||
<span class="small grey" v-if="node.login.grant.username.length > 0">({{node.login.grant.username}})</span>
|
||||
</a>
|
||||
</div>
|
||||
<span v-else class="disabled">尚未设置</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>API节点地址</td>
|
||||
<td>
|
||||
<div v-if="node.apiNodeAddrs != null && node.apiNodeAddrs.length > 0">
|
||||
<span v-for="addr in node.apiNodeAddrs" class="ui label basic small">{{addr}}</span>
|
||||
</div>
|
||||
<span v-else class="disabled">使用全局设置</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<h3>运行状态</h3>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">运行状态</td>
|
||||
<td>
|
||||
<div v-if="node.status.isActive">
|
||||
<span class="green">运行中</span>
|
||||
<a href="" @click.prevent="stopNode()" v-if="!isStopping"><span>[通过SSH停止]</span></a>
|
||||
<span v-if="isStopping">[停止中...]</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="red">已断开</span>
|
||||
<a href="" @click.prevent="startNode()" v-if="node.isInstalled && !isStarting"><span>[通过SSH启动]</span></a>
|
||||
<span v-if="node.isInstalled && isStarting">[启动中...]</span>
|
||||
<a v-if="!node.isInstalled" :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + node.id"><span>去安装 ></span></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody v-show="node.status.isActive">
|
||||
<tr>
|
||||
<td>CPU用量</td>
|
||||
<td>
|
||||
{{node.status.cpuUsageText}}
|
||||
|
||||
<span v-if="node.status.cpuPhysicalCount > 0" class="small grey">({{node.status.cpuPhysicalCount}}核心/{{node.status.cpuLogicalCount}}线程)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>内存用量</td>
|
||||
<td>{{node.status.memUsageText}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>负载</td>
|
||||
<td>
|
||||
{{node.status.load1m}} {{node.status.load5m}} {{node.status.load15m}}
|
||||
|
||||
<tip-icon content="三个数字分别代表1分钟、5分钟、15分钟平均负载"></tip-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr v-if="node.status.buildVersion != null && node.status.buildVersion.length > 0">
|
||||
<td>版本</td>
|
||||
<td>
|
||||
v{{node.status.buildVersion}}
|
||||
|
||||
<a :href="'/httpdns/clusters/upgradeRemote?clusterId=' + clusterId + '&nodeId=' + node.id" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} »</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>
|
||||
<td>最近API连接状况</td>
|
||||
<td>
|
||||
<span v-if="node.status.apiSuccessPercent > 0 && node.status.apiAvgCostSeconds > 0">
|
||||
<span v-if="node.status.apiSuccessPercent <= 50" class="red">连接错误异常严重({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯</span>
|
||||
<span v-else-if="node.status.apiSuccessPercent <= 80" class="red">连接错误较多({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯</span>
|
||||
<span v-else-if="node.status.apiSuccessPercent < 100" class="orange">有连接错误发生({{round(100 - node.status.apiSuccessPercent)}}%失败),请改善当前节点和API节点之间通讯</span>
|
||||
<span v-else>
|
||||
<span v-if="node.status.apiAvgCostSeconds <= 1" class="green">连接良好</span>
|
||||
<span v-else-if="node.status.apiAvgCostSeconds <= 5" class="orange">连接基本稳定(平均{{round(node.status.apiAvgCostSeconds)}}秒)</span>
|
||||
<span v-else-if="node.status.apiAvgCostSeconds <= 10" class="orange">连接速度较慢(平均{{round(node.status.apiAvgCostSeconds)}}秒)</span>
|
||||
<span v-else class="red">连接非常慢(平均{{round(node.status.apiAvgCostSeconds)}}秒),请改善当前节点和API节点之间通讯</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="disabled">尚未上报数据</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="nodeDatetime.length > 0">
|
||||
<td>上次更新时间</td>
|
||||
<td>
|
||||
{{nodeDatetime}}
|
||||
<p class="comment" v-if="nodeTimeDiff > 30"><span class="red">当前节点时间与API节点时间相差 {{nodeTimeDiff}} 秒,请同步节点时间。</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="comment" v-if="node.status.isActive">每隔30秒钟更新一次运行状态。</p>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<h3>安装信息</h3>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td>节点ID <em>(id)</em></td>
|
||||
<td>{{node.uniqueId}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密钥 <em>(secret)</em></td>
|
||||
<td>{{node.secret}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">安装目录</td>
|
||||
<td>
|
||||
<div v-if="node.installDir.length == 0">使用集群设置<span v-if="node.cluster != null && node.cluster.installDir.length > 0">({{node.cluster.installDir}})</span></div>
|
||||
<span v-else>{{node.installDir}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>已安装</td>
|
||||
<td>
|
||||
<span v-if="node.isInstalled" class="green">已安装</span>
|
||||
<a v-else :href="'/httpdns/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + nodeId" class="underline" title="点击进入安装界面"><span class="red">未安装</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
Tea.context(function () {
|
||||
this.isStarting = false
|
||||
this.startNode = function () {
|
||||
this.isStarting = true
|
||||
this.$post('/httpdns/clusters/cluster/node/start')
|
||||
.params({
|
||||
nodeId: this.node.id
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success('启动成功', function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
})
|
||||
.done(function () {
|
||||
this.isStarting = false
|
||||
})
|
||||
}
|
||||
|
||||
this.isStopping = false
|
||||
this.stopNode = function () {
|
||||
this.isStopping = true
|
||||
this.$post('/httpdns/clusters/cluster/node/stop')
|
||||
.params({
|
||||
nodeId: this.node.id
|
||||
})
|
||||
.success(function () {
|
||||
teaweb.success('执行成功', function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
})
|
||||
.done(function () {
|
||||
this.isStopping = false
|
||||
})
|
||||
}
|
||||
|
||||
this.round = function (f) {
|
||||
return Math.round(f * 100) / 100
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
a.underline {
|
||||
border-bottom: 1px #db2828 dashed;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.installing-box {
|
||||
line-height: 1.8;
|
||||
}
|
||||
.installing-box .blue {
|
||||
color: #2185d0;
|
||||
}
|
||||
/*# sourceMappingURL=install.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["install.less"],"names":[],"mappings":"AAAA;EACC,gBAAA;;AADD,eAGC;EACC,cAAA","file":"install.css"}
|
||||
@@ -0,0 +1,100 @@
|
||||
{$layout}
|
||||
{$template "node_menu"}
|
||||
{$template "/code_editor"}
|
||||
|
||||
<!-- 已安装 -->
|
||||
<div v-if="node.isInstalled">
|
||||
<div class="ui message green">当前节点为已安装状态。</div>
|
||||
<a href="" @click.prevent="updateNodeIsInstalled(false)">[重新安装]</a>
|
||||
|
||||
<h4>配置文件</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">配置文件</td>
|
||||
<td>
|
||||
configs/api_httpdns.yaml
|
||||
<download-link :v-element="'rpc-code'" :v-file="'api_httpdns.yaml'">[下载]</download-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>配置内容</td>
|
||||
<td>
|
||||
<source-code-box id="rpc-code" type="text/yaml">rpc.endpoints: [ {{apiEndpoints}} ]
|
||||
nodeId: "{{node.uniqueId}}"
|
||||
secret: "{{node.secret}}"</source-code-box>
|
||||
<p class="comment">每个节点的配置文件内容均不相同,不能混用。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">安装目录</td>
|
||||
<td>
|
||||
<div v-if="node.installDir.length == 0">使用集群设置<span
|
||||
v-if="node.cluster != null && node.cluster.installDir.length > 0">({{node.cluster.installDir}})</span>
|
||||
</div>
|
||||
<span v-else>{{node.installDir}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 未安装 -->
|
||||
<div v-if="!node.isInstalled">
|
||||
<h4>方法1:通过SSH自动安装</h4>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">SSH地址</td>
|
||||
<td>
|
||||
<span v-if="sshAddr.length > 0">{{sshAddr}} <a href=""
|
||||
@click.prevent="showSSHPopup(nodeId)">[修改]</a></span>
|
||||
<span v-else><span class="red">尚未设置</span> <a href=""
|
||||
@click.prevent="showSSHPopup(nodeId)">[设置]</a></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div v-if="installStatus != null && (installStatus.isRunning || installStatus.isFinished)"
|
||||
class="ui segment installing-box">
|
||||
<div v-if="installStatus.isRunning" class="blue">安装中...</div>
|
||||
<div v-if="installStatus.isFinished">
|
||||
<span v-if="installStatus.isOk" class="green">已安装成功</span>
|
||||
<span v-if="!installStatus.isOk" class="red">安装过程中发生错误:{{installStatus.error}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installStatus != null && installStatus.isFinished">
|
||||
<button class="ui button small" type="button" @click.prevent="install()">重新安装</button>
|
||||
</div>
|
||||
<div v-if="installStatus == null || (!installStatus.isFinished && !installStatus.isRunning)">
|
||||
<button class="ui button small primary" type="button" @click.prevent="install()">开始安装</button>
|
||||
</div>
|
||||
|
||||
<h4>方法2:手动安装</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">配置文件</td>
|
||||
<td>
|
||||
configs/api_httpdns.yaml
|
||||
<download-link :v-element="'rpc-code'" :v-file="'api_httpdns.yaml'">[下载]</download-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>配置内容</td>
|
||||
<td>
|
||||
<source-code-box id="rpc-code" type="text/yaml">rpc.endpoints: [ {{apiEndpoints}} ]
|
||||
nodeId: "{{node.uniqueId}}"
|
||||
secret: "{{node.secret}}"</source-code-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">安装目录</td>
|
||||
<td>
|
||||
<div v-if="node.installDir.length == 0">使用集群设置<span
|
||||
v-if="node.cluster != null && node.cluster.installDir.length > 0">({{node.cluster.installDir}})</span>
|
||||
</div>
|
||||
<span v-else>{{node.installDir}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a href="" @click.prevent="updateNodeIsInstalled(true)">[修改为已安装状态]</a>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
Tea.context(function () {
|
||||
let isInstalling = false
|
||||
|
||||
this.$delay(function () {
|
||||
this.reloadStatus(this.nodeId)
|
||||
})
|
||||
|
||||
this.install = function () {
|
||||
isInstalling = true
|
||||
|
||||
this.$post("$")
|
||||
.params({
|
||||
nodeId: this.nodeId
|
||||
})
|
||||
.success(function () {})
|
||||
}
|
||||
|
||||
this.updateNodeIsInstalled = function (isInstalled) {
|
||||
let msg = isInstalled
|
||||
? "html:确认要将当前节点修改为 <strong>已安装</strong> 状态?"
|
||||
: "html:确认要将当前节点修改为 <strong>未安装</strong> 状态?"
|
||||
teaweb.confirm(msg, function () {
|
||||
this.$post("/httpdns/clusters/cluster/node/updateInstallStatus")
|
||||
.params({
|
||||
nodeId: this.nodeId,
|
||||
isInstalled: isInstalled ? 1 : 0
|
||||
})
|
||||
.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.reloadStatus = function (nodeId) {
|
||||
let that = this
|
||||
|
||||
this.$post("/httpdns/clusters/cluster/node/status")
|
||||
.params({
|
||||
nodeId: nodeId
|
||||
})
|
||||
.success(function (resp) {
|
||||
this.installStatus = resp.data.installStatus
|
||||
this.node.isInstalled = resp.data.isInstalled
|
||||
|
||||
if (!isInstalling) {
|
||||
return
|
||||
}
|
||||
|
||||
let currentNodeId = this.node.id
|
||||
let installStatus = this.installStatus || {}
|
||||
let errMsg = installStatus.error || ""
|
||||
let errorCode = installStatus.errorCode || ""
|
||||
|
||||
if (errorCode.length > 0) {
|
||||
isInstalling = false
|
||||
}
|
||||
|
||||
switch (errorCode) {
|
||||
case "EMPTY_LOGIN":
|
||||
case "EMPTY_SSH_HOST":
|
||||
case "EMPTY_SSH_PORT":
|
||||
case "EMPTY_GRANT":
|
||||
teaweb.warn("需要补充 SSH 登录信息", function () {
|
||||
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + currentNodeId, {
|
||||
height: "30em",
|
||||
callback: function () {
|
||||
that.install()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
case "SSH_LOGIN_FAILED":
|
||||
teaweb.warn("SSH 登录失败,请检查设置", function () {
|
||||
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + currentNodeId, {
|
||||
height: "30em",
|
||||
callback: function () {
|
||||
that.install()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
case "CREATE_ROOT_DIRECTORY_FAILED":
|
||||
teaweb.warn("创建根目录失败:" + errMsg)
|
||||
return
|
||||
case "INSTALL_HELPER_FAILED":
|
||||
teaweb.warn("安装助手失败:" + errMsg)
|
||||
return
|
||||
case "TEST_FAILED":
|
||||
teaweb.warn("环境测试失败:" + errMsg)
|
||||
return
|
||||
case "RPC_TEST_FAILED":
|
||||
teaweb.confirm("html:节点到 API 的 RPC 连通性测试失败:" + errMsg + "<br/>现在去修改 API 信息?", function () {
|
||||
window.location = "/settings/api"
|
||||
})
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
.done(function () {
|
||||
this.$delay(function () {
|
||||
this.reloadStatus(nodeId)
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
this.showSSHPopup = function (nodeId) {
|
||||
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + nodeId, {
|
||||
height: "30em",
|
||||
callback: function () {
|
||||
teaweb.success("保存成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
.installing-box {
|
||||
line-height: 1.8;
|
||||
|
||||
.blue {
|
||||
color: #2185d0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pre.log-box {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/*# sourceMappingURL=logs.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["logs.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA","file":"logs.css"}
|
||||
@@ -0,0 +1,51 @@
|
||||
{$layout}
|
||||
{$template "node_menu"}
|
||||
{$template "/datepicker"}
|
||||
|
||||
<form method="get" action="/httpdns/clusters/cluster/node/logs" class="ui form" autocomplete="off">
|
||||
<input type="hidden" name="clusterId" :value="clusterId"/>
|
||||
<input type="hidden" name="nodeId" :value="nodeId"/>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" value="" style="width:8em" id="day-to-picker"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="level" v-model="level">
|
||||
<option value="">[级别]</option>
|
||||
<option value="error">错误</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
<option value="success">成功</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" name="keyword" style="width:10em" v-model="keyword" placeholder="关键词"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<button type="submit" class="ui button">查询</button>
|
||||
</div>
|
||||
<div class="ui field" v-if="dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
|
||||
<a :href="'/httpdns/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + nodeId">[清除条件]</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
|
||||
|
||||
<table class="ui table selectable" v-if="logs.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="log in logs">
|
||||
<td>
|
||||
<node-log-row :v-log="log" :v-keyword="keyword"></node-log-row>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
@@ -0,0 +1,6 @@
|
||||
Tea.context(function () {
|
||||
this.$delay(function () {
|
||||
teaweb.datepicker("day-from-picker")
|
||||
teaweb.datepicker("day-to-picker")
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user