前端页面

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