前端页面

This commit is contained in:
robin
2026-02-24 11:33:44 +08:00
parent f3af234308
commit 60dc87e0f2
141 changed files with 6845 additions and 133 deletions

View 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))
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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]
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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, ", ")
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View 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()
}

View 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()
})
}

View 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]
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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() }

View File

@@ -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()
}

View File

@@ -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()
})
}

View 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",
},
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View 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()
}

View 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()
}

View 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()
})
}

View File

@@ -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()
}

View File

@@ -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()
}

View 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()
})
}

View File

@@ -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",
},
}
}

View 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")
}

View 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()
})
}

View File

@@ -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
}

View File

@@ -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()
})
}

View 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
}

View File

@@ -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()
}

View File

@@ -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()
})
}

View File

@@ -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()
}

View File

@@ -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()
})
}

View File

@@ -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()
}

View File

@@ -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()
})
}

View 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)
}