带阿里标识的版本

This commit is contained in:
robin
2026-02-28 18:55:33 +08:00
parent 150799f41d
commit 5d0b7c7e91
477 changed files with 10813 additions and 4044 deletions

View File

@@ -6,8 +6,10 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
)
type AppSettingsAction struct {
@@ -52,30 +54,83 @@ func (this *AppSettingsAction) RunGet(params struct {
},
}
clusterResp, err := this.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(this.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
clusters := make([]maps.Map, 0, len(clusterResp.GetClusters()))
clusterDomainMap := map[int64]string{}
clusterNameMap := map[int64]string{}
defaultPrimaryClusterId := int64(0)
for _, cluster := range clusterResp.GetClusters() {
clusterId := cluster.GetId()
clusterName := cluster.GetName()
clusters = append(clusters, maps.Map{
"id": clusterId,
"name": clusterName,
"serviceDomain": cluster.GetServiceDomain(),
"isDefault": cluster.GetIsDefault(),
})
clusterDomainMap[clusterId] = cluster.GetServiceDomain()
clusterNameMap[clusterId] = clusterName
if defaultPrimaryClusterId <= 0 && cluster.GetIsDefault() {
defaultPrimaryClusterId = clusterId
}
}
defaultBackupClusterId := int64(0)
defaultBackupResp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{
Code: string(systemconfigs.SettingCodeHTTPDNSDefaultBackupClusterId),
})
if err != nil {
this.ErrorPage(err)
return
}
if defaultBackupResp != nil && len(defaultBackupResp.GetValueJSON()) > 0 {
defaultBackupClusterId = types.Int64(string(defaultBackupResp.GetValueJSON()))
}
primaryClusterId := app.GetInt64("primaryClusterId")
backupClusterId := app.GetInt64("backupClusterId")
settings := maps.Map{
"appId": app.GetString("appId"),
"appStatus": app.GetBool("isOn"),
"primaryClusterId": app.GetInt64("primaryClusterId"),
"backupClusterId": app.GetInt64("backupClusterId"),
"signEnabled": app.GetBool("signEnabled"),
"signSecretPlain": app.GetString("signSecretPlain"),
"signSecretMasked": app.GetString("signSecretMasked"),
"signSecretUpdatedAt": app.GetString("signSecretUpdated"),
"appId": app.GetString("appId"),
"appStatus": app.GetBool("isOn"),
"primaryClusterId": primaryClusterId,
"backupClusterId": backupClusterId,
"defaultPrimaryClusterId": defaultPrimaryClusterId,
"defaultPrimaryClusterName": clusterNameMap[defaultPrimaryClusterId],
"defaultBackupClusterId": defaultBackupClusterId,
"defaultBackupClusterName": clusterNameMap[defaultBackupClusterId],
"primaryServiceDomain": clusterDomainMap[primaryClusterId],
"backupServiceDomain": clusterDomainMap[backupClusterId],
"signEnabled": app.GetBool("signEnabled"),
"signSecretPlain": app.GetString("signSecretPlain"),
"signSecretMasked": app.GetString("signSecretMasked"),
"signSecretUpdatedAt": app.GetString("signSecretUpdated"),
}
this.Data["app"] = app
this.Data["settings"] = settings
this.Data["clusters"] = clusters
this.Show()
}
func (this *AppSettingsAction) RunPost(params struct {
AppId int64
AppStatus bool
AppStatus bool
PrimaryClusterId int64
BackupClusterId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
if params.PrimaryClusterId > 0 && params.BackupClusterId > 0 && params.PrimaryClusterId == params.BackupClusterId {
this.FailField("backupClusterId", "备用集群不能与主集群相同")
return
}
appResp, err := this.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(this.AdminContext(), &pb.FindHTTPDNSAppRequest{
AppDbId: params.AppId,
@@ -92,8 +147,8 @@ func (this *AppSettingsAction) RunPost(params struct {
_, err = this.RPC().HTTPDNSAppRPC().UpdateHTTPDNSApp(this.AdminContext(), &pb.UpdateHTTPDNSAppRequest{
AppDbId: params.AppId,
Name: appResp.GetApp().GetName(),
PrimaryClusterId: appResp.GetApp().GetPrimaryClusterId(),
BackupClusterId: appResp.GetApp().GetBackupClusterId(),
PrimaryClusterId: params.PrimaryClusterId,
BackupClusterId: params.BackupClusterId,
IsOn: params.AppStatus,
UserId: appResp.GetApp().GetUserId(),
})

View File

@@ -55,6 +55,24 @@ func (this *CreateAction) RunGet(params struct{}) {
}
this.Data["defaultBackupClusterId"] = defaultBackupClusterId
usersResp, err := this.RPC().UserRPC().ListEnabledUsers(this.AdminContext(), &pb.ListEnabledUsersRequest{
Offset: 0,
Size: 10_000,
})
if err != nil {
this.ErrorPage(err)
return
}
users := make([]maps.Map, 0, len(usersResp.GetUsers()))
for _, user := range usersResp.GetUsers() {
users = append(users, maps.Map{
"id": user.GetId(),
"fullname": user.GetFullname(),
"username": user.GetUsername(),
})
}
this.Data["users"] = users
this.Show()
}

View File

@@ -18,6 +18,7 @@ func init() {
Get("/sdk", new(SdkAction)).
GetPost("/sdk/upload", new(SdkUploadAction)).
Post("/sdk/upload/delete", new(SdkUploadDeleteAction)).
Get("/sdk/check", new(SdkCheckAction)).
Get("/sdk/download", new(SdkDownloadAction)).
Get("/sdk/doc", new(SdkDocAction)).
GetPost("/app/settings", new(AppSettingsAction)).

View File

@@ -7,11 +7,20 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
func listAppMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
clusterNameMap, err := loadHTTPDNSClusterNameMap(parent)
if err != nil {
return nil, err
}
userMapByID, err := loadHTTPDNSUserMap(parent)
if err != nil {
return nil, err
}
resp, err := parent.RPC().HTTPDNSAppRPC().ListHTTPDNSApps(parent.AdminContext(), &pb.ListHTTPDNSAppsRequest{
Offset: 0,
Size: 10_000,
@@ -30,13 +39,22 @@ func listAppMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map,
return nil, err
}
result = append(result, appPBToMap(app, int64(len(domainResp.GetDomains()))))
result = append(result, appPBToMap(app, int64(len(domainResp.GetDomains())), clusterNameMap, userMapByID))
}
return result, nil
}
func findAppMap(parent *actionutils.ParentAction, appDbId int64) (maps.Map, error) {
clusterNameMap, err := loadHTTPDNSClusterNameMap(parent)
if err != nil {
return nil, err
}
userMapByID, err := loadHTTPDNSUserMap(parent)
if err != nil {
return nil, err
}
if appDbId > 0 {
resp, err := parent.RPC().HTTPDNSAppRPC().FindHTTPDNSApp(parent.AdminContext(), &pb.FindHTTPDNSAppRequest{
AppDbId: appDbId,
@@ -51,7 +69,7 @@ func findAppMap(parent *actionutils.ParentAction, appDbId int64) (maps.Map, erro
if err != nil {
return nil, err
}
return appPBToMap(resp.GetApp(), int64(len(domainResp.GetDomains()))), nil
return appPBToMap(resp.GetApp(), int64(len(domainResp.GetDomains())), clusterNameMap, userMapByID), nil
}
}
@@ -259,16 +277,37 @@ func toggleCustomRule(parent *actionutils.ParentAction, ruleId int64, isOn bool)
return err
}
func appPBToMap(app *pb.HTTPDNSApp, domainCount int64) maps.Map {
func appPBToMap(app *pb.HTTPDNSApp, domainCount int64, clusterNameMap map[int64]string, userMapByID map[int64]maps.Map) maps.Map {
signSecret := app.GetSignSecret()
primaryClusterID := app.GetPrimaryClusterId()
backupClusterID := app.GetBackupClusterId()
primaryClusterMap := maps.Map{"id": primaryClusterID, "name": clusterNameMap[primaryClusterID]}
backupClusterMap := maps.Map{"id": backupClusterID, "name": clusterNameMap[backupClusterID]}
var userMap maps.Map
if app.GetUserId() > 0 {
userMap = userMapByID[app.GetUserId()]
if userMap == nil {
userMap = maps.Map{
"id": app.GetUserId(),
"fullname": "用户#" + strconv.FormatInt(app.GetUserId(), 10),
"username": "-",
}
}
}
return maps.Map{
"id": app.GetId(),
"name": app.GetName(),
"appId": app.GetAppId(),
"clusterId": app.GetPrimaryClusterId(),
"primaryClusterId": app.GetPrimaryClusterId(),
"backupClusterId": app.GetBackupClusterId(),
"clusterId": primaryClusterID,
"primaryClusterId": primaryClusterID,
"backupClusterId": backupClusterID,
"primaryCluster": primaryClusterMap,
"backupCluster": backupClusterMap,
"userId": app.GetUserId(),
"user": userMap,
"isOn": app.GetIsOn(),
"domainCount": domainCount,
"sniPolicyText": "隐匿 SNI",
@@ -279,6 +318,39 @@ func appPBToMap(app *pb.HTTPDNSApp, domainCount int64) maps.Map {
}
}
func loadHTTPDNSClusterNameMap(parent *actionutils.ParentAction) (map[int64]string, error) {
resp, err := parent.RPC().HTTPDNSClusterRPC().FindAllHTTPDNSClusters(parent.AdminContext(), &pb.FindAllHTTPDNSClustersRequest{})
if err != nil {
return nil, err
}
result := map[int64]string{}
for _, cluster := range resp.GetClusters() {
result[cluster.GetId()] = cluster.GetName()
}
return result, nil
}
func loadHTTPDNSUserMap(parent *actionutils.ParentAction) (map[int64]maps.Map, error) {
resp, err := parent.RPC().UserRPC().ListEnabledUsers(parent.AdminContext(), &pb.ListEnabledUsersRequest{
Offset: 0,
Size: 10_000,
})
if err != nil {
return nil, err
}
result := map[int64]maps.Map{}
for _, user := range resp.GetUsers() {
result[user.GetId()] = maps.Map{
"id": user.GetId(),
"fullname": user.GetFullname(),
"username": user.GetUsername(),
}
}
return result, nil
}
func defaultLineField(value string) string {
value = strings.TrimSpace(value)
if len(value) == 0 {

View File

@@ -0,0 +1,67 @@
package apps
import (
"net/url"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type SdkCheckAction struct {
actionutils.ParentAction
}
func (this *SdkCheckAction) Init() {
this.Nav("", "", "")
}
func (this *SdkCheckAction) RunGet(params struct {
Platform string
Version string
Type string
}) {
platform, _, _, filename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
t := strings.ToLower(strings.TrimSpace(params.Type))
if t == "doc" {
docPath := findUploadedSDKDocPath(platform, params.Version)
if len(docPath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "Documentation is unavailable, please upload first"
this.Success()
return
}
downloadURL := "/httpdns/apps/sdk/doc?platform=" + url.QueryEscape(platform)
if len(strings.TrimSpace(params.Version)) > 0 {
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
}
this.Data["exists"] = true
this.Data["url"] = downloadURL
this.Success()
return
}
archivePath := findSDKArchivePath(filename, params.Version)
if len(archivePath) == 0 {
this.Data["exists"] = false
this.Data["message"] = "SDK package is unavailable, please upload first"
this.Success()
return
}
downloadURL := "/httpdns/apps/sdk/download?platform=" + url.QueryEscape(platform)
if len(strings.TrimSpace(params.Version)) > 0 {
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
}
downloadURL += "&raw=1"
this.Data["exists"] = true
this.Data["url"] = downloadURL
this.Success()
}

View File

@@ -1,10 +1,11 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"os"
"path/filepath"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type SdkDocAction struct {
@@ -17,15 +18,18 @@ func (this *SdkDocAction) Init() {
func (this *SdkDocAction) RunGet(params struct {
Platform string
Version string
}) {
platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Fail(err.Error())
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
var data []byte
uploadedDocPath := findUploadedSDKDocPath(platform)
uploadedDocPath := findUploadedSDKDocPath(platform, params.Version)
if len(uploadedDocPath) > 0 {
data, err = os.ReadFile(uploadedDocPath)
}
@@ -44,11 +48,18 @@ func (this *SdkDocAction) RunGet(params struct {
}
if len(data) == 0 || err != nil {
this.Fail("当前服务器未找到 SDK 集成文档请先在“SDK 集成”页面上传对应平台文档")
this.Data["exists"] = false
this.Data["message"] = "SDK documentation is not found on server, please upload first"
this.Success()
return
}
downloadName := filepath.Base(uploadedDocPath)
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
downloadName = "httpdns-sdk-" + strings.ToLower(platform) + ".md"
}
this.AddHeader("Content-Type", "text/markdown; charset=utf-8")
this.AddHeader("Content-Disposition", "attachment; filename=\"httpdns-sdk-"+strings.ToLower(platform)+"-README.md\";")
this.AddHeader("Content-Disposition", "attachment; filename=\""+downloadName+"\";")
_, _ = this.ResponseWriter.Write(data)
}

View File

@@ -1,9 +1,11 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"io"
"os"
"path/filepath"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type SdkDownloadAction struct {
@@ -16,30 +18,48 @@ func (this *SdkDownloadAction) Init() {
func (this *SdkDownloadAction) RunGet(params struct {
Platform string
Version string
Raw int
}) {
_, _, _, filename, err := resolveSDKPlatform(params.Platform)
if err != nil {
this.Fail(err.Error())
this.Data["exists"] = false
this.Data["message"] = err.Error()
this.Success()
return
}
archivePath := findSDKArchivePath(filename)
archivePath := findSDKArchivePath(filename, params.Version)
if len(archivePath) == 0 {
this.Fail("当前服务器未找到 SDK 包请先在“SDK 集成”页面上传对应平台包: " + filename)
this.Data["exists"] = false
this.Data["message"] = "SDK archive not found on server, please upload first: " + filename
this.Success()
return
}
fp, err := os.Open(archivePath)
if err != nil {
this.Fail("打开 SDK 包失败: " + err.Error())
this.Data["exists"] = false
this.Data["message"] = "failed to open SDK archive: " + err.Error()
this.Success()
return
}
defer func() {
_ = fp.Close()
}()
this.AddHeader("Content-Type", "application/zip")
this.AddHeader("Content-Disposition", "attachment; filename=\""+filename+"\";")
downloadName := filepath.Base(archivePath)
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
downloadName = filename
}
if params.Raw == 1 {
this.AddHeader("Content-Type", "application/octet-stream")
this.AddHeader("X-SDK-Filename", downloadName)
} else {
this.AddHeader("Content-Type", "application/zip")
this.AddHeader("Content-Disposition", "attachment; filename=\""+downloadName+"\";")
}
this.AddHeader("X-Accel-Buffering", "no")
_, _ = io.Copy(this.ResponseWriter, fp)
}

View File

@@ -2,18 +2,58 @@ package apps
import (
"errors"
"github.com/iwind/TeaGo/Tea"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/iwind/TeaGo/Tea"
)
func sdkUploadDir() string {
dirs := sdkUploadDirs()
if len(dirs) > 0 {
return dirs[0]
}
return filepath.Clean(Tea.Root + "/data/httpdns/sdk")
}
func sdkUploadDirs() []string {
candidates := []string{
filepath.Clean(Tea.Root + "/../data/httpdns/sdk"),
filepath.Clean(Tea.Root + "/data/httpdns/sdk"),
filepath.Clean(Tea.Root + "/../edge-admin/data/httpdns/sdk"),
filepath.Clean(Tea.Root + "/../edge-user/data/httpdns/sdk"),
filepath.Clean(Tea.Root + "/../../data/httpdns/sdk"),
}
results := make([]string, 0, len(candidates))
seen := map[string]bool{}
for _, dir := range candidates {
if len(dir) == 0 || seen[dir] {
continue
}
seen[dir] = true
results = append(results, dir)
}
return results
}
func sdkUploadSearchDirs() []string {
dirs := sdkUploadDirs()
results := make([]string, 0, len(dirs))
for _, dir := range dirs {
stat, err := os.Stat(dir)
if err == nil && stat.IsDir() {
results = append(results, dir)
}
}
if len(results) == 0 {
results = append(results, sdkUploadDir())
}
return results
}
func findFirstExistingDir(paths []string) string {
for _, path := range paths {
stat, err := os.Stat(path)
@@ -27,7 +67,7 @@ func findFirstExistingDir(paths []string) string {
func findFirstExistingFile(paths []string) string {
for _, path := range paths {
stat, err := os.Stat(path)
if err == nil && !stat.IsDir() {
if err == nil && !stat.IsDir() && stat.Size() > 0 {
return path
}
}
@@ -45,6 +85,9 @@ func findNewestExistingFile(paths []string) string {
if err != nil || stat.IsDir() {
continue
}
if stat.Size() <= 0 {
continue
}
if len(result.path) == 0 || stat.ModTime().After(result.modTime) || (stat.ModTime().Equal(result.modTime) && path > result.path) {
result.path = path
result.modTime = stat.ModTime()
@@ -85,22 +128,23 @@ func resolveSDKPlatform(platform string) (key string, relativeDir string, readme
}
}
func findSDKArchivePath(downloadFilename string) string {
searchDirs := []string{sdkUploadDir()}
func findSDKArchivePath(downloadFilename string, version string) string {
searchDirs := sdkUploadSearchDirs()
// 1) Exact filename first.
exactFiles := []string{}
for _, dir := range searchDirs {
exactFiles = append(exactFiles, filepath.Join(dir, downloadFilename))
}
path := findFirstExistingFile(exactFiles)
if len(path) > 0 {
return path
}
// 2) Version-suffixed archives, e.g. httpdns-sdk-android-v1.4.8.zip
normalizedVersion := strings.TrimSpace(version)
base := strings.TrimSuffix(downloadFilename, ".zip")
patternName := base + "-*.zip"
if len(normalizedVersion) > 0 {
versionFiles := []string{}
for _, dir := range searchDirs {
versionFiles = append(versionFiles, filepath.Join(dir, base+"-v"+normalizedVersion+".zip"))
}
if path := findFirstExistingFile(versionFiles); len(path) > 0 {
return path
}
return ""
}
patternName := base + "-v*.zip"
matches := []string{}
for _, dir := range searchDirs {
found, _ := filepath.Glob(filepath.Join(dir, patternName))
@@ -115,28 +159,52 @@ func findSDKArchivePath(downloadFilename string) string {
return findNewestExistingFile(matches)
}
exactFiles := []string{}
for _, dir := range searchDirs {
exactFiles = append(exactFiles, filepath.Join(dir, downloadFilename))
}
if path := findFirstExistingFile(exactFiles); len(path) > 0 {
return path
}
return ""
}
func findUploadedSDKDocPath(platform string) string {
func findUploadedSDKDocPath(platform string, version string) string {
platform = strings.ToLower(strings.TrimSpace(platform))
if len(platform) == 0 {
return ""
}
searchDir := sdkUploadDir()
exact := filepath.Join(searchDir, "httpdns-sdk-"+platform+".md")
if file := findFirstExistingFile([]string{exact}); len(file) > 0 {
return file
}
pattern := filepath.Join(searchDir, "httpdns-sdk-"+platform+"-*.md")
matches, _ := filepath.Glob(pattern)
if len(matches) == 0 {
searchDirs := sdkUploadSearchDirs()
normalizedVersion := strings.TrimSpace(version)
if len(normalizedVersion) > 0 {
exactVersion := []string{}
for _, dir := range searchDirs {
exactVersion = append(exactVersion, filepath.Join(dir, "httpdns-sdk-"+platform+"-v"+normalizedVersion+".md"))
}
if file := findFirstExistingFile(exactVersion); len(file) > 0 {
return file
}
return ""
}
sort.Strings(matches)
return findNewestExistingFile(matches)
matches := []string{}
for _, dir := range searchDirs {
pattern := filepath.Join(dir, "httpdns-sdk-"+platform+"-v*.md")
found, _ := filepath.Glob(pattern)
matches = append(matches, found...)
}
if len(matches) > 0 {
sort.Strings(matches)
return findNewestExistingFile(matches)
}
exact := []string{}
for _, dir := range searchDirs {
exact = append(exact, filepath.Join(dir, "httpdns-sdk-"+platform+".md"))
}
return findFirstExistingFile(exact)
}
func findLocalSDKDocPath(platform string) string {

View File

@@ -2,15 +2,16 @@ package apps
import (
"errors"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/actions"
)
type SdkUploadAction struct {
@@ -44,7 +45,7 @@ func (this *SdkUploadAction) RunPost(params struct {
AppId int64
Platform string
Version string
SDKFile *actions.File
SdkFile *actions.File
DocFile *actions.File
Must *actions.Must
@@ -63,7 +64,7 @@ func (this *SdkUploadAction) RunPost(params struct {
return
}
if params.SDKFile == nil && params.DocFile == nil {
if params.SdkFile == nil && params.DocFile == nil {
this.Fail("请至少上传一个文件")
return
}
@@ -75,24 +76,25 @@ func (this *SdkUploadAction) RunPost(params struct {
return
}
if params.SDKFile != nil {
filename := strings.ToLower(strings.TrimSpace(params.SDKFile.Filename))
if params.SdkFile != nil {
filename := strings.ToLower(strings.TrimSpace(params.SdkFile.Filename))
if !strings.HasSuffix(filename, ".zip") {
this.Fail("SDK 包仅支持 .zip 文件")
return
}
sdkData, readErr := params.SDKFile.Read()
sdkData, readErr := params.SdkFile.Read()
if readErr != nil {
this.Fail("读取 SDK 包失败: " + readErr.Error())
return
}
if len(sdkData) == 0 {
this.Fail("SDK 包文件为空,请重新上传")
return
}
baseName := strings.TrimSuffix(downloadFilename, ".zip")
err = saveSDKUploadFile(uploadDir, downloadFilename, sdkData)
if err == nil {
err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
}
err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
if err != nil {
this.Fail("保存 SDK 包失败: " + err.Error())
return
@@ -111,12 +113,12 @@ func (this *SdkUploadAction) RunPost(params struct {
this.Fail("读取集成文档失败: " + readErr.Error())
return
}
filename := "httpdns-sdk-" + platform + ".md"
err = saveSDKUploadFile(uploadDir, filename, docData)
if err == nil {
err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
if len(docData) == 0 {
this.Fail("集成文档文件为空,请重新上传")
return
}
err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
if err != nil {
this.Fail("保存集成文档失败: " + err.Error())
return
@@ -151,12 +153,6 @@ func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
}
func listUploadedSDKFiles() []map[string]interface{} {
dir := sdkUploadDir()
entries, err := os.ReadDir(dir)
if err != nil {
return []map[string]interface{}{}
}
type item struct {
Name string
Platform string
@@ -166,30 +162,45 @@ func listUploadedSDKFiles() []map[string]interface{} {
UpdatedAt int64
}
items := make([]item, 0)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
platform, version, fileType, ok := parseSDKUploadFilename(name)
if !ok {
byName := map[string]item{}
for _, dir := range sdkUploadSearchDirs() {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
platform, version, fileType, ok := parseSDKUploadFilename(name)
if !ok {
continue
}
info, statErr := entry.Info()
if statErr != nil {
continue
}
info, statErr := entry.Info()
if statErr != nil {
continue
}
items = append(items, item{
Name: name,
Platform: platform,
FileType: fileType,
Version: version,
SizeBytes: info.Size(),
UpdatedAt: info.ModTime().Unix(),
})
current := item{
Name: name,
Platform: platform,
FileType: fileType,
Version: version,
SizeBytes: info.Size(),
UpdatedAt: info.ModTime().Unix(),
}
old, exists := byName[name]
if !exists || current.UpdatedAt >= old.UpdatedAt {
byName[name] = current
}
}
}
items := make([]item, 0, len(byName))
for _, it := range byName {
items = append(items, it)
}
sort.Slice(items, func(i, j int) bool {
@@ -200,14 +211,14 @@ func listUploadedSDKFiles() []map[string]interface{} {
})
result := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
for _, it := range items {
result = append(result, map[string]interface{}{
"name": item.Name,
"platform": item.Platform,
"fileType": item.FileType,
"version": item.Version,
"sizeText": formatSDKFileSize(item.SizeBytes),
"updatedAt": time.Unix(item.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
"name": it.Name,
"platform": it.Platform,
"fileType": it.FileType,
"version": it.Version,
"sizeText": formatSDKFileSize(it.SizeBytes),
"updatedAt": time.Unix(it.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
})
}
return result
@@ -231,7 +242,7 @@ func parseSDKUploadFilename(filename string) (platform string, version string, f
}
main := strings.TrimSuffix(strings.TrimPrefix(filename, "httpdns-sdk-"), ext)
version = "latest"
version = ""
if idx := strings.Index(main, "-v"); idx > 0 && idx+2 < len(main) {
version = main[idx+2:]
main = main[:idx]
@@ -241,6 +252,9 @@ func parseSDKUploadFilename(filename string) (platform string, version string, f
switch main {
case "android", "ios", "flutter":
platform = main
if len(version) == 0 {
version = "-"
}
return platform, version, fileType, true
default:
return "", "", "", false

View File

@@ -1,10 +1,11 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"os"
"path/filepath"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
)
type SdkUploadDeleteAction struct {
@@ -42,15 +43,16 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
return
}
fullPath := filepath.Join(sdkUploadDir(), filename)
_, err := os.Stat(fullPath)
if err != nil {
this.Success()
return
}
if err = os.Remove(fullPath); err != nil {
this.Fail("删除失败: " + err.Error())
return
for _, dir := range sdkUploadDirs() {
fullPath := filepath.Join(dir, filename)
_, err := os.Stat(fullPath)
if err != nil {
continue
}
if err = os.Remove(fullPath); err != nil {
this.Fail("删除失败: " + err.Error())
return
}
}
this.Success()

View File

@@ -10,8 +10,10 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
)
type ClusterSettingsAction struct {
@@ -41,13 +43,14 @@ func (this *ClusterSettingsAction) RunGet(params struct {
}
settings := maps.Map{
"name": cluster.GetString("name"),
"gatewayDomain": cluster.GetString("gatewayDomain"),
"cacheTtl": cluster.GetInt("defaultTTL"),
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
"installDir": cluster.GetString("installDir"),
"isOn": cluster.GetBool("isOn"),
"isDefaultCluster": cluster.GetBool("isDefault"),
"name": cluster.GetString("name"),
"gatewayDomain": cluster.GetString("gatewayDomain"),
"cacheTtl": cluster.GetInt("defaultTTL"),
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
"installDir": cluster.GetString("installDir"),
"isOn": cluster.GetBool("isOn"),
"isDefaultCluster": cluster.GetBool("isDefault"),
"isDefaultBackupCluster": false,
}
if settings.GetInt("cacheTtl") <= 0 {
settings["cacheTtl"] = 30
@@ -59,6 +62,19 @@ func (this *ClusterSettingsAction) RunGet(params struct {
settings["installDir"] = "/opt/edge-httpdns"
}
defaultBackupResp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{
Code: string(systemconfigs.SettingCodeHTTPDNSDefaultBackupClusterId),
})
if err != nil {
this.ErrorPage(err)
return
}
defaultBackupClusterId := int64(0)
if defaultBackupResp != nil && len(defaultBackupResp.GetValueJSON()) > 0 {
defaultBackupClusterId = types.Int64(string(defaultBackupResp.GetValueJSON()))
}
settings["isDefaultBackupCluster"] = defaultBackupClusterId == params.ClusterId
listenAddresses := []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolHTTPS,
@@ -104,14 +120,15 @@ func (this *ClusterSettingsAction) RunGet(params struct {
}
func (this *ClusterSettingsAction) RunPost(params struct {
ClusterId int64
Name string
GatewayDomain string
CacheTtl int32
FallbackTimeout int32
InstallDir string
IsOn bool
IsDefaultCluster bool
ClusterId int64
Name string
GatewayDomain string
CacheTtl int32
FallbackTimeout int32
InstallDir string
IsOn bool
IsDefaultCluster bool
IsDefaultBackupCluster bool
Addresses []byte
SslPolicyJSON []byte
@@ -137,7 +154,15 @@ func (this *ClusterSettingsAction) RunPost(params struct {
params.InstallDir = "/opt/edge-httpdns"
}
if params.IsDefaultCluster && !params.IsOn {
this.Fail("默认集群必须保持启用状态")
this.Fail("默认集群必须保持启用状态")
return
}
if params.IsDefaultBackupCluster && !params.IsOn {
this.Fail("默认备用集群必须保持启用状态")
return
}
if params.IsDefaultCluster && params.IsDefaultBackupCluster {
this.Fail("默认主集群和默认备用集群不能是同一个集群")
return
}
@@ -195,5 +220,33 @@ func (this *ClusterSettingsAction) RunPost(params struct {
return
}
backupClusterValue := int64(0)
if params.IsDefaultBackupCluster {
backupClusterValue = params.ClusterId
} else {
readResp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{
Code: string(systemconfigs.SettingCodeHTTPDNSDefaultBackupClusterId),
})
if err != nil {
this.ErrorPage(err)
return
}
if readResp != nil && len(readResp.GetValueJSON()) > 0 {
oldBackupClusterId := types.Int64(string(readResp.GetValueJSON()))
if oldBackupClusterId != params.ClusterId {
backupClusterValue = oldBackupClusterId
}
}
}
_, err = this.RPC().SysSettingRPC().UpdateSysSetting(this.AdminContext(), &pb.UpdateSysSettingRequest{
Code: string(systemconfigs.SettingCodeHTTPDNSDefaultBackupClusterId),
ValueJSON: []byte(strconv.FormatInt(backupClusterValue, 10)),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,11 +1,13 @@
package clusters
import (
"strconv"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/actions"
)
@@ -23,13 +25,14 @@ func (this *CreateAction) RunGet(params struct{}) {
}
func (this *CreateAction) RunPost(params struct {
Name string
GatewayDomain string
CacheTtl int32
FallbackTimeout int32
InstallDir string
IsOn bool
IsDefault bool
Name string
GatewayDomain string
CacheTtl int32
FallbackTimeout int32
InstallDir string
IsOn bool
IsDefaultPrimary bool
IsDefaultBackup bool
Must *actions.Must
}) {
@@ -49,6 +52,19 @@ func (this *CreateAction) RunPost(params struct {
params.Must.Field("name", params.Name).Require("请输入集群名称")
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
if params.IsDefaultPrimary && !params.IsOn {
this.Fail("默认主集群必须保持启用状态")
return
}
if params.IsDefaultBackup && !params.IsOn {
this.Fail("默认备用集群必须保持启用状态")
return
}
if params.IsDefaultPrimary && params.IsDefaultBackup {
this.Fail("默认主集群和默认备用集群不能是同一个集群")
return
}
resp, err := this.RPC().HTTPDNSClusterRPC().CreateHTTPDNSCluster(this.AdminContext(), &pb.CreateHTTPDNSClusterRequest{
Name: params.Name,
ServiceDomain: params.GatewayDomain,
@@ -56,13 +72,24 @@ func (this *CreateAction) RunPost(params struct {
FallbackTimeoutMs: params.FallbackTimeout,
InstallDir: params.InstallDir,
IsOn: params.IsOn,
IsDefault: params.IsDefault,
IsDefault: params.IsDefaultPrimary,
})
if err != nil {
this.ErrorPage(err)
return
}
if params.IsDefaultBackup {
_, err = this.RPC().SysSettingRPC().UpdateSysSetting(this.AdminContext(), &pb.UpdateSysSettingRequest{
Code: string(systemconfigs.SettingCodeHTTPDNSDefaultBackupClusterId),
ValueJSON: []byte(strconv.FormatInt(resp.GetClusterId(), 10)),
})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["clusterId"] = resp.GetClusterId()
this.Success()
}

View File

@@ -102,6 +102,8 @@ func (this *IndexAction) RunPost(params struct {
NsIsOn bool
HttpdnsIsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
@@ -112,6 +114,15 @@ func (this *IndexAction) RunPost(params struct {
Gt(0, "请选择一个集群")
var config = userconfigs.DefaultUserRegisterConfig()
{
// 先读取现有配置,避免保存时把未出现在当前表单里的字段重置为默认值
resp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{
Code: systemconfigs.SettingCodeUserRegisterConfig,
})
if err == nil && len(resp.ValueJSON) > 0 {
_ = json.Unmarshal(resp.ValueJSON, config)
}
}
config.IsOn = params.IsOn
config.ComplexPassword = params.ComplexPassword
config.RequireVerification = params.RequireVerification
@@ -142,6 +153,7 @@ func (this *IndexAction) RunPost(params struct {
config.ADIsOn = params.AdIsOn
config.NSIsOn = params.NsIsOn
config.HTTPDNSIsOn = params.HttpdnsIsOn
configJSON, err := json.Marshal(config)
if err != nil {
@@ -157,10 +169,15 @@ func (this *IndexAction) RunPost(params struct {
return
}
if params.FeatureOp != "keep" {
featureOp := params.FeatureOp
if featureOp != "overwrite" && featureOp != "append" && featureOp != "keep" {
featureOp = "keep"
}
if featureOp != "keep" {
_, err = this.RPC().UserRPC().UpdateAllUsersFeatures(this.AdminContext(), &pb.UpdateAllUsersFeaturesRequest{
FeatureCodes: params.Features,
Overwrite: params.FeatureOp == "overwrite",
Overwrite: featureOp == "overwrite",
})
if err != nil {
this.ErrorPage(err)

View File

@@ -0,0 +1,28 @@
{$layout "layout_popup"}
<h3 v-if="!isUpdating">添加端口绑定</h3>
<h3 v-if="isUpdating">修改端口绑定</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="supportRange" :value="supportRange ? 1 : 0"/>
<table class="ui table definition selectable">
<tr>
<td>网络协议</td>
<td>
<select class="ui dropdown auto-width" name="protocol" v-model="protocol" @change="changeProtocol">
<option v-for="p in protocols" :value="p.code">{{p.name}}</option>
</select>
</td>
</tr>
<tr>
<td class="title">端口 *</td>
<td>
<input type="text" name="address" ref="focus" v-model="address"/>
<p class="comment">可以是一个数字端口通常不超过65535也可以是"地址:端口"的方式。<span v-if="supportRange">支持端口范围,形式为<code-label>port1-port2</code-label></span>
<span v-if="from.length == 0 && protocol == 'http'">HTTP常用端口为<a href="" title="点击添加" @click.prevent="addPort('80')">80</a></span>
<span v-if="from.length == 0 && protocol == 'https'">HTTPS常用端口为<a href="" title="点击添加" @click.prevent="addPort('443')">443</a></span>
</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,47 @@
Tea.context(function () {
this.success = NotifyPopup;
this.isUpdating = false
this.address = ""
this.protocol = this.protocols[0].code
// 初始化
// from 用来标记是否为特殊的节点
if (this.from.length == 0) {
if (this.protocol == "http") {
this.address = "80"
} else if (this.protocol == "https") {
this.address = "443"
}
}
if (window.parent.UPDATING_ADDR != null) {
this.isUpdating = true
let addr = window.parent.UPDATING_ADDR
this.protocol = addr.protocol
if (addr.host.length == 0) {
this.address = addr.portRange
} else {
this.address = addr.host.quoteIP() + ":" + addr.portRange
}
}
this.changeProtocol = function () {
if (this.from.length > 0) {
return
}
switch (this.protocol) {
case "http":
this.address = "80"
break
case "https":
this.address = "443"
break
}
}
this.addPort = function (port) {
this.address = port
}
});

View File

@@ -67,8 +67,24 @@
<table class="ui table selectable definition" v-show="activeSection == 'basic'">
<tr>
<td class="title">App ID</td>
<td><code>{{settings.appId}}</code></td>
<td class="title">主集群</td>
<td>
<select class="ui dropdown auto-width" name="primaryClusterId" v-model="settings.primaryClusterId">
<option :value="0">[不设置]</option>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<p class="comment httpdns-note">未设置时,按默认主集群处理(当前默认主集群:{{settings.defaultPrimaryClusterName || '-' }})。</p>
</td>
</tr>
<tr>
<td class="title">备集群</td>
<td>
<select class="ui dropdown auto-width" name="backupClusterId" v-model="settings.backupClusterId">
<option :value="0">[不设置]</option>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<p class="comment httpdns-note">未设置时,按默认备用集群处理(当前默认备用集群:{{settings.defaultBackupClusterName || '-' }})。</p>
</td>
</tr>
<tr>
<td class="title">应用启用</td>
@@ -86,14 +102,34 @@
</table>
<table class="ui table selectable definition httpdns-auth-table" v-show="activeSection == 'auth'">
<tr>
<td class="title">App ID</td>
<td>
<code>{{settings.appId}}</code>
<a href="" class="httpdns-mini-icon" title="复制 App ID" @click.prevent="copySecret(settings.appId, 'App ID')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td class="title">主服务域名</td>
<td>
<code v-if="settings.primaryServiceDomain && settings.primaryServiceDomain.length > 0">{{settings.primaryServiceDomain}}</code>
<span class="grey" v-else>未配置</span>
<a v-if="settings.primaryServiceDomain && settings.primaryServiceDomain.length > 0" href="" class="httpdns-mini-icon" title="复制主服务域名" @click.prevent="copySecret(settings.primaryServiceDomain, '主服务域名')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td class="title">备用服务域名</td>
<td>
<code v-if="settings.backupServiceDomain && settings.backupServiceDomain.length > 0">{{settings.backupServiceDomain}}</code>
<span class="grey" v-else>未配置</span>
<a v-if="settings.backupServiceDomain && settings.backupServiceDomain.length > 0" href="" class="httpdns-mini-icon" title="复制备用服务域名" @click.prevent="copySecret(settings.backupServiceDomain, '备用服务域名')"><i class="copy outline icon"></i></a>
</td>
</tr>
<tr>
<td class="title">请求验签</td>
<td>
<span
:class="settings.signEnabled ? 'httpdns-state-on' : 'httpdns-state-off'">{{settings.signEnabled
? "已开启" : "已关闭"}}</span>
<a href="" class="ui mini button basic" style="margin-left: .8em;"
@click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
<span :class="settings.signEnabled ? 'httpdns-state-on' : 'httpdns-state-off'">{{settings.signEnabled ? "已开启" : "已关闭"}}</span>
<a href="" class="ui mini button basic" style="margin-left: .8em;" @click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
<p class="comment httpdns-note">打开后,服务端会对请求进行签名校验。</p>
</td>
</tr>
@@ -102,14 +138,9 @@
<td>
<div class="httpdns-secret-line">
<code>{{signSecretVisible ? settings.signSecretPlain : settings.signSecretMasked}}</code>
<a href="" class="httpdns-mini-icon" @click.prevent="signSecretVisible = !signSecretVisible"
:title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon"
:class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-icon" title="复制加签 Secret"
@click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i
class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-icon" title="重置加签 Secret" @click.prevent="resetSignSecret"><i
class="redo icon"></i></a>
<a href="" class="httpdns-mini-icon" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-icon" title="复制加签 Secret" @click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-icon" title="重置加签 Secret" @click.prevent="resetSignSecret"><i class="redo icon"></i></a>
</div>
<p class="comment httpdns-note">最近更新:{{settings.signSecretUpdatedAt}}</p>
<p class="comment httpdns-note" v-if="!settings.signEnabled">请求验签已关闭,当前不使用加签 Secret。</p>

View File

@@ -34,10 +34,13 @@
<tr>
<td>所属用户</td>
<td>
<user-selector></user-selector>
<select class="ui dropdown auto-width" name="userId">
<option value="0">[不设置]</option>
<option v-for="user in users" :value="user.id">{{user.fullname}} ({{user.username}})</option>
</select>
<p class="comment">可以选择当前应用所属的平台用户。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,4 +1,4 @@
{$layout}
{$layout}
{$template "menu"}
<div class="ui margin"></div>
@@ -26,16 +26,22 @@
<table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0">
<colgroup>
<col style="width:32%;" />
<col style="width:26%;" />
<col style="width:20%;" />
<col style="width:12%;" />
<col style="width:10%;" />
<col style="width:11%;" />
<col style="width:11%;" />
<col style="width:12%;" />
<col style="width:8%;" />
<col style="width:6%;" />
<col style="width:20%;" />
</colgroup>
<thead>
<tr>
<th>应用名称</th>
<th>AppID</th>
<th>主集群</th>
<th>备用集群</th>
<th>用户</th>
<th class="center">绑定域名数</th>
<th class="center">状态</th>
<th>操作</th>
@@ -54,6 +60,26 @@
<code>{{app.appId}}</code>
<copy-icon :text="app.appId"></copy-icon>
</td>
<td>
<span v-if="app.primaryCluster != null && app.primaryCluster.id > 0">
{{app.primaryCluster.name || ('#' + app.primaryCluster.id)}}
<link-icon :href="'/httpdns/clusters/cluster?clusterId=' + app.primaryCluster.id"></link-icon>
</span>
<span v-else class="disabled">-</span>
</td>
<td>
<span v-if="app.backupCluster != null && app.backupCluster.id > 0">
{{app.backupCluster.name || ('#' + app.backupCluster.id)}}
<link-icon :href="'/httpdns/clusters/cluster?clusterId=' + app.backupCluster.id"></link-icon>
</span>
<span v-else class="disabled">-</span>
</td>
<td>
<span v-if="app.user != null && app.user.id > 0">
{{app.user.fullname}} ({{app.user.username}})
</span>
<span v-else class="disabled">-</span>
</td>
<td class="center"><a :href="'/httpdns/apps/domains?appId=' + app.id">{{app.domainCount}}</a></td>
<td class="center">
<label-on :v-is-on="app.isOn"></label-on>
@@ -68,4 +94,4 @@
</table>
<div class="page" v-html="page"></div>
</div>
</div>

View File

@@ -49,8 +49,8 @@
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=android"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=android"><i class="icon book"></i> 下载文档</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=android" @click="downloadSDK('android', $event)"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=android" @click="downloadDoc('android', $event)"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
@@ -63,8 +63,8 @@
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=ios"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=ios"><i class="icon book"></i> 下载文档</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=ios" @click="downloadSDK('ios', $event)"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=ios" @click="downloadDoc('ios', $event)"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>
@@ -77,8 +77,8 @@
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=flutter"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=flutter"><i class="icon book"></i> 下载文档</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/download?platform=flutter" @click="downloadSDK('flutter', $event)"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="/httpdns/apps/sdk/doc?platform=flutter" @click="downloadDoc('flutter', $event)"><i class="icon book"></i> 下载文档</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,123 @@
Tea.context(function () {
this.downloadSDK = function (platform, event) {
this.checkAndDownload(platform, "sdk", event)
}
this.downloadDoc = function (platform, event) {
this.checkAndDownload(platform, "doc", event)
}
this.checkAndDownload = function (platform, type, event) {
if (event != null && typeof event.preventDefault == "function") {
event.preventDefault()
}
this.$get("/httpdns/apps/sdk/check")
.params({
platform: platform,
type: type
})
.success(function (resp) {
let data = (resp != null && resp.data != null) ? resp.data : {}
if (!data.exists) {
teaweb.warn(data.message || "当前暂无可下载文件")
return
}
if (typeof data.url == "string" && data.url.length > 0) {
this.downloadByBlob(data.url, platform, type)
return
}
teaweb.warn("下载地址生成失败,请稍后重试")
}.bind(this))
}
this.downloadByBlob = function (url, platform, type) {
let defaultFileName = "httpdns-sdk-" + platform + (type == "doc" ? ".md" : ".zip")
let xhr = new XMLHttpRequest()
xhr.open("GET", url, true)
xhr.responseType = "blob"
xhr.onload = function () {
if (xhr.status < 200 || xhr.status >= 300) {
teaweb.warn("下载失败HTTP " + xhr.status + "")
return
}
if (xhr.status == 204) {
teaweb.warn("下载被浏览器扩展或下载工具拦截,请暂时关闭相关扩展后重试")
return
}
let contentType = (xhr.getResponseHeader("Content-Type") || "").toLowerCase()
if (contentType.indexOf("application/json") >= 0) {
let reader = new FileReader()
reader.onload = function () {
try {
let json = JSON.parse(reader.result)
teaweb.warn((json && json.message) ? json.message : "下载失败,请稍后重试")
} catch (_e) {
teaweb.warn("下载失败,请稍后重试")
}
}
reader.readAsText(xhr.response)
return
}
let disposition = xhr.getResponseHeader("Content-Disposition") || ""
let fileName = xhr.getResponseHeader("X-SDK-Filename") || this.parseFileName(disposition) || defaultFileName
if (xhr.response == null || xhr.response.size <= 0) {
teaweb.warn("下载文件为空,请检查已上传 SDK 包是否完整")
return
}
this.saveBlob(xhr.response, fileName)
}.bind(this)
xhr.onerror = function () {
teaweb.warn("下载失败,请检查网络后重试")
}
xhr.send()
}
this.parseFileName = function (disposition) {
if (typeof disposition != "string" || disposition.length == 0) {
return ""
}
let utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i)
if (utf8Match != null && utf8Match.length > 1) {
try {
return decodeURIComponent(utf8Match[1])
} catch (_e) {
}
}
let plainMatch = disposition.match(/filename="?([^";]+)"?/i)
if (plainMatch != null && plainMatch.length > 1) {
return plainMatch[1]
}
return ""
}
this.saveBlob = function (blob, fileName) {
if (window.navigator != null && typeof window.navigator.msSaveOrOpenBlob == "function") {
window.navigator.msSaveOrOpenBlob(blob, fileName)
return
}
let objectURL = window.URL.createObjectURL(blob)
let a = document.createElement("a")
a.style.display = "none"
a.href = objectURL
a.download = fileName
document.body.appendChild(a)
a.click()
setTimeout(function () {
window.URL.revokeObjectURL(objectURL)
if (a.parentNode != null) {
a.parentNode.removeChild(a)
}
}, 30000)
}
})

View File

@@ -1,4 +1,4 @@
{$layout}
{$layout}
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
@@ -34,7 +34,7 @@
<td class="title">版本号 *</td>
<td>
<input type="text" name="version" v-model="version" maxlength="32"/>
<p class="comment">默认 `1.0.0`。同平台上传会覆盖“最新版本”下载内容</p>
<p class="comment">默认 `1.0.0`。同平台+同版本再次上传会覆盖该版本文件</p>
</td>
</tr>
<tr>
@@ -90,4 +90,3 @@
<div class="ui message" v-else>
暂无上传记录。
</div>

View File

@@ -1,4 +1,4 @@
Tea.context(function () {
Tea.context(function () {
this.platform = "android";
this.version = this.defaultVersion || "1.0.0";
this.isUploading = false;

View File

@@ -11,7 +11,6 @@
<div class="item"><strong>集群设置</strong></div>
</second-menu>
{$template "/left_menu_with_menu"}
<div class="right-box with-menu">
@@ -39,50 +38,65 @@
<td>节点安装根目录</td>
<td>
<input type="text" name="installDir" maxlength="100" v-model="settings.installDir" />
<p class="comment">边缘节点安装 HTTPDNS 服务的默认所在目录,此目录将被用于下发配置。通常保持默认即可。</p>
<p class="comment">边缘节点安装 HTTPDNS 服务的默认目录,通常保持默认即可。</p>
</td>
</tr>
<tr>
<td>默认解析 TTL</td>
<td>
<div class="ui input right labeled">
<input type="text" name="cacheTtl" maxlength="5" v-model="settings.cacheTtl"
style="width: 6em" />
<input type="text" name="cacheTtl" maxlength="5" v-model="settings.cacheTtl" style="width: 6em" />
<span class="ui label"></span>
</div>
<p class="comment">SDK HTTPDNS 请求域名解析时,返回的默认缓存有效期 (TTL)。SDK 超时后将重新发起请求</p>
<p class="comment">SDK 通过 HTTPDNS 解析域名时返回的默认 TTL</p>
</td>
</tr>
<tr>
<td>降级超时容忍度</td>
<td>
<div class="ui input right labeled">
<input type="text" name="fallbackTimeout" maxlength="5" v-model="settings.fallbackTimeout"
style="width: 6em" />
<input type="text" name="fallbackTimeout" maxlength="5" v-model="settings.fallbackTimeout" style="width: 6em" />
<span class="ui label">毫秒</span>
</div>
<p class="comment">HTTPDNS 节点回源查询其它 DNS 时的最大等待时间。超出此时间将触发服务降级逻辑(返回上一有效缓存或错误)。</p>
<p class="comment">节点回源查询上游 DNS 时的最大等待时间。</p>
</td>
</tr>
<tr>
<td>启用当前集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn"
@change="syncDefaultCluster" />
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn" @change="syncDefaultCluster" />
<label></label>
</div>
<p class="comment">如果取消启用,整个集群的 HTTPDNS 解析服务将停止</p>
<p class="comment">取消启用后,该集群不会参与 HTTPDNS 服务</p>
</td>
</tr>
<tr>
<td>默认集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isDefaultCluster" value="1" v-model="settings.isDefaultCluster" />
<label>为默认部署集群</label>
<input type="checkbox" value="1" v-model="settings.defaultClusterEnabled" @change="syncDefaultClusterSelection" />
<label>设为默认集群</label>
</div>
<p class="comment">全局设置。如果应用未单独指定集群,将默认分配和部署到该集群中。</p>
<div class="ui form" style="margin-top: .8em;" v-if="settings.defaultClusterEnabled">
<div class="grouped fields" style="margin:0;">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" v-model="settings.defaultClusterRole" value="primary" @change="syncDefaultClusterSelection" />
<label>主集群</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" v-model="settings.defaultClusterRole" value="backup" @change="syncDefaultClusterSelection" />
<label>备用集群</label>
</div>
</div>
</div>
</div>
<input type="hidden" name="isDefaultCluster" value="1" v-if="settings.defaultClusterEnabled && settings.defaultClusterRole == 'primary'" />
<input type="hidden" name="isDefaultBackupCluster" value="1" v-if="settings.defaultClusterEnabled && settings.defaultClusterRole == 'backup'" />
<p class="comment">同一时刻最多 1 个默认集群角色,新设置会自动取消旧设置。</p>
</td>
</tr>
</table>
@@ -91,15 +105,12 @@
<tr>
<td class="title">绑定端口 *</td>
<td>
<network-addresses-box :v-url="'/httpdns/addPortPopup'" :v-addresses="tlsConfig.listen"
:v-protocol="'tls'" :v-support-range="true"></network-addresses-box>
<network-addresses-box :v-url="'/httpdns/addPortPopup'" :v-addresses="tlsConfig.listen" :v-protocol="'tls'" :v-support-range="true"></network-addresses-box>
</td>
</tr>
</table>
<!-- SSL配置 -->
<ssl-config-box v-show="activeSection == 'tls'" :v-ssl-policy="tlsConfig.sslPolicy"
:v-protocol="'tls'"></ssl-config-box>
<ssl-config-box v-show="activeSection == 'tls'" :v-ssl-policy="tlsConfig.sslPolicy" :v-protocol="'tls'"></ssl-config-box>
<submit-btn></submit-btn>
</form>

View File

@@ -1,4 +1,4 @@
Tea.context(function () {
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功")
this.activeSection = this.activeSection || "basic"
@@ -8,9 +8,38 @@ Tea.context(function () {
this.settings = {}
}
// 兼容旧字段,转换成统一“默认集群 + 角色”表现
let isDefaultPrimary = !!this.settings.isDefaultCluster
let isDefaultBackup = !!this.settings.isDefaultBackupCluster
this.settings.defaultClusterEnabled = isDefaultPrimary || isDefaultBackup
this.settings.defaultClusterRole = isDefaultBackup ? "backup" : "primary"
this.syncDefaultCluster = function () {
if (!this.settings.isOn) {
this.settings.defaultClusterEnabled = false
this.settings.defaultClusterRole = "primary"
this.settings.isDefaultCluster = false
this.settings.isDefaultBackupCluster = false
return
}
this.syncDefaultClusterSelection()
}
this.syncDefaultClusterSelection = function () {
if (!this.settings.defaultClusterEnabled) {
this.settings.isDefaultCluster = false
this.settings.isDefaultBackupCluster = false
return
}
if (this.settings.defaultClusterRole === "backup") {
this.settings.isDefaultCluster = false
this.settings.isDefaultBackupCluster = true
} else {
this.settings.defaultClusterRole = "primary"
this.settings.isDefaultCluster = true
this.settings.isDefaultBackupCluster = false
}
}
})

View File

@@ -1,4 +1,4 @@
{$layout}
{$layout}
{$template "menu"}
<div class="ui margin"></div>
@@ -27,7 +27,7 @@
<input type="text" name="cacheTtl" maxlength="5" value="30" style="width: 6em" />
<span class="ui label"></span>
</div>
<p class="comment">SDK HTTPDNS 请求域名解析时,返回的默认缓存有效期 (TTL)。SDK 超时后将重新发起请求</p>
<p class="comment">SDK 通过 HTTPDNS 解析域名时返回的默认 TTL</p>
</td>
</tr>
<tr>
@@ -37,36 +37,52 @@
<input type="text" name="fallbackTimeout" maxlength="5" value="300" style="width: 6em" />
<span class="ui label">毫秒</span>
</div>
<p class="comment">HTTPDNS 节点回源查询其它 DNS 时的最大等待时间。超出此时间将触发服务降级逻辑(返回上一有效缓存或错误)。</p>
<p class="comment">节点回源查询上游 DNS 时的最大等待时间。</p>
</td>
</tr>
<tr>
<td>节点安装根目录</td>
<td>
<input type="text" name="installDir" maxlength="255" value="/opt/edge-httpdns" />
<p class="comment">边缘节点安装 HTTPDNS 服务的默认所在目录,此目录将被用于下发配置。通常保持默认即可</p>
<p class="comment">边缘节点安装 HTTPDNS 服务的默认目录</p>
</td>
</tr>
<tr>
<td>启用当前集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" checked />
<input type="checkbox" name="isOn" value="1" v-model="isOn" @change="syncDefaultClusterEnabled" checked />
<label></label>
</div>
<p class="comment">如果取消启用,整个集群的 HTTPDNS 解析服务将不被系统分配。</p>
</td>
</tr>
<tr>
<td>默认集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isDefault" value="1" />
<label>为默认部署集群</label>
<input type="checkbox" value="1" v-model="defaultClusterEnabled" @change="syncDefaultClusterEnabled" />
<label>设为默认集群</label>
</div>
<p class="comment">全局设置。如果应用未单独指定集群,将默认分配和部署到该集群中。</p>
<div class="ui form" style="margin-top: .8em;" v-if="defaultClusterEnabled">
<div class="grouped fields" style="margin:0;">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" v-model="defaultClusterRole" value="primary" />
<label>主集群</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" v-model="defaultClusterRole" value="backup" />
<label>备用集群</label>
</div>
</div>
</div>
</div>
<input type="hidden" name="isDefaultPrimary" value="1" v-if="defaultClusterEnabled && defaultClusterRole == 'primary'" />
<input type="hidden" name="isDefaultBackup" value="1" v-if="defaultClusterEnabled && defaultClusterRole == 'backup'" />
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -1,4 +1,15 @@
Tea.context(function () {
Tea.context(function () {
this.isOn = true
this.defaultClusterEnabled = false
this.defaultClusterRole = "primary"
this.syncDefaultClusterEnabled = function () {
if (!this.isOn) {
this.defaultClusterEnabled = false
this.defaultClusterRole = "primary"
}
}
this.success = function (resp) {
let clusterId = 0
if (resp != null && resp.data != null && typeof resp.data.clusterId != "undefined") {

View File

@@ -256,5 +256,17 @@
<div class="margin"></div>
</div>
<div class="margin"></div>
<h4>HTTPDNS服务</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">开通HTTPDNS服务</td>
<td><checkbox name="httpdnsIsOn" v-model="config.httpdnsIsOn"></checkbox>
<p class="comment">选中表示自动为用户开通HTTPDNS服务。</p>
</td>
</tr>
</table>
<div class="margin"></div>
<submit-btn></submit-btn>
</form>
</form>

View File

@@ -7,7 +7,8 @@ Tea.context(function () {
this.mobileVerificationMoreOptions = false
this.mobileResetPasswordMoreOptions = false
this.featureOp = "overwrite"
// 默认不影响已有用户功能,避免保存注册设置时误改用户功能绑定
this.featureOp = "keep"
this.featuresVisible = false
this.showFeatures = function () {
@@ -27,4 +28,4 @@ Tea.context(function () {
})
return names.join(" / ")
}
})
})