diff --git a/atsf_server/common/constants.go b/atsf_server/common/constants.go
index 51fc579f..9dfb4273 100644
--- a/atsf_server/common/constants.go
+++ b/atsf_server/common/constants.go
@@ -1,9 +1,10 @@
package common
import (
- "github.com/google/uuid"
"sync"
"time"
+
+ "github.com/google/uuid"
)
var StartTime = time.Now().Unix() // unit: second
@@ -16,7 +17,7 @@ var HomePageLink = ""
// Any options with "Secret", "Token" in its key won't be return by GetOptions
var SessionSecret = uuid.New().String()
-var SQLitePath = "gin-template.db"
+var SQLitePath = "atsflare.db"
var OptionMap map[string]string
var OptionMapRWMutex sync.RWMutex
diff --git a/atsf_server/controller/managed_domain.go b/atsf_server/controller/managed_domain.go
new file mode 100644
index 00000000..15ec4321
--- /dev/null
+++ b/atsf_server/controller/managed_domain.go
@@ -0,0 +1,121 @@
+package controller
+
+import (
+ "encoding/json"
+ "gin-template/service"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+func GetManagedDomains(c *gin.Context) {
+ domains, err := service.ListManagedDomains()
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": domains,
+ })
+}
+
+func CreateManagedDomain(c *gin.Context) {
+ var input service.ManagedDomainInput
+ if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "无效的参数",
+ })
+ return
+ }
+ domain, err := service.CreateManagedDomain(input)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": domain,
+ })
+}
+
+func UpdateManagedDomain(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 64)
+ if err != nil || id == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "无效的参数",
+ })
+ return
+ }
+ var input service.ManagedDomainInput
+ if err = json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "无效的参数",
+ })
+ return
+ }
+ domain, err := service.UpdateManagedDomain(uint(id), input)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": domain,
+ })
+}
+
+func DeleteManagedDomain(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 64)
+ if err != nil || id == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "无效的参数",
+ })
+ return
+ }
+ if err = service.DeleteManagedDomain(uint(id)); err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ })
+}
+
+func MatchManagedDomainCertificate(c *gin.Context) {
+ domain := strings.TrimSpace(c.Query("domain"))
+ result, err := service.MatchManagedDomainCertificate(domain)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": result,
+ })
+}
diff --git a/atsf_server/model/main.go b/atsf_server/model/main.go
index c1e29142..335e450e 100644
--- a/atsf_server/model/main.go
+++ b/atsf_server/model/main.go
@@ -1,8 +1,8 @@
package model
import (
- "github.com/glebarez/sqlite"
"gin-template/common"
+ "github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"os"
@@ -84,6 +84,10 @@ func InitDB() (err error) {
if err != nil {
return err
}
+ err = db.AutoMigrate(&ManagedDomain{})
+ if err != nil {
+ return err
+ }
err = createRootAccountIfNeed()
return err
} else {
diff --git a/atsf_server/model/managed_domain.go b/atsf_server/model/managed_domain.go
new file mode 100644
index 00000000..2dacc761
--- /dev/null
+++ b/atsf_server/model/managed_domain.go
@@ -0,0 +1,41 @@
+package model
+
+import "time"
+
+type ManagedDomain struct {
+ ID uint `json:"id" gorm:"primaryKey"`
+ Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
+ CertID *uint `json:"cert_id"`
+ Enabled bool `json:"enabled" gorm:"not null;default:true"`
+ Remark string `json:"remark" gorm:"size:255"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+func ListManagedDomains() (domains []*ManagedDomain, err error) {
+ err = DB.Order("id desc").Find(&domains).Error
+ return domains, err
+}
+
+func ListEnabledManagedDomainsWithCertificate() (domains []*ManagedDomain, err error) {
+ err = DB.Where("enabled = ? AND cert_id IS NOT NULL", true).Order("id desc").Find(&domains).Error
+ return domains, err
+}
+
+func GetManagedDomainByID(id uint) (*ManagedDomain, error) {
+ domain := &ManagedDomain{}
+ err := DB.First(domain, id).Error
+ return domain, err
+}
+
+func (domain *ManagedDomain) Insert() error {
+ return DB.Create(domain).Error
+}
+
+func (domain *ManagedDomain) Update() error {
+ return DB.Save(domain).Error
+}
+
+func (domain *ManagedDomain) Delete() error {
+ return DB.Delete(domain).Error
+}
diff --git a/atsf_server/router/api-router.go b/atsf_server/router/api-router.go
index 28a8d5d1..1e65b615 100644
--- a/atsf_server/router/api-router.go
+++ b/atsf_server/router/api-router.go
@@ -70,6 +70,15 @@ func SetApiRouter(router *gin.Engine) {
proxyRoute.PUT("/:id", controller.UpdateProxyRoute)
proxyRoute.DELETE("/:id", controller.DeleteProxyRoute)
}
+ managedDomainRoute := apiRouter.Group("/managed-domains")
+ managedDomainRoute.Use(middleware.AdminAuth())
+ {
+ managedDomainRoute.GET("/", controller.GetManagedDomains)
+ managedDomainRoute.GET("/match", controller.MatchManagedDomainCertificate)
+ managedDomainRoute.POST("/", controller.CreateManagedDomain)
+ managedDomainRoute.PUT("/:id", controller.UpdateManagedDomain)
+ managedDomainRoute.DELETE("/:id", controller.DeleteManagedDomain)
+ }
tlsCertificateRoute := apiRouter.Group("/tls-certificates")
tlsCertificateRoute.Use(middleware.AdminAuth())
{
diff --git a/atsf_server/router/api_phase2_managed_domain_test.go b/atsf_server/router/api_phase2_managed_domain_test.go
new file mode 100644
index 00000000..e8efd228
--- /dev/null
+++ b/atsf_server/router/api_phase2_managed_domain_test.go
@@ -0,0 +1,112 @@
+package router_test
+
+import (
+ "gin-template/common"
+ "gin-template/router"
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-contrib/sessions/cookie"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "testing"
+)
+
+func TestPhase2ManagedDomainLifecycle(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ common.RedisEnabled = false
+ setupTestDB(t)
+
+ engine := gin.New()
+ engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret"))))
+ router.SetApiRouter(engine)
+
+ token := prepareRootToken(t)
+ wildcardCertPEM, wildcardKeyPEM := generateCertificatePairForRouterTest(t, []string{"*.example.com"})
+ exactCertPEM, exactKeyPEM := generateCertificatePairForRouterTest(t, []string{"api.example.com"})
+
+ wildcardResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/tls-certificates/", map[string]any{
+ "name": "wildcard-cert",
+ "cert_pem": wildcardCertPEM,
+ "key_pem": wildcardKeyPEM,
+ })
+ var wildcardCertificate map[string]any
+ decodeResponseData(t, wildcardResp, &wildcardCertificate)
+
+ exactResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/tls-certificates/", map[string]any{
+ "name": "exact-cert",
+ "cert_pem": exactCertPEM,
+ "key_pem": exactKeyPEM,
+ })
+ var exactCertificate map[string]any
+ decodeResponseData(t, exactResp, &exactCertificate)
+
+ wildcardID := uint(wildcardCertificate["id"].(float64))
+ exactID := uint(exactCertificate["id"].(float64))
+
+ createWildcard := performJSONRequest(t, engine, token, http.MethodPost, "/api/managed-domains/", map[string]any{
+ "domain": "*.example.com",
+ "cert_id": wildcardID,
+ "enabled": true,
+ "remark": "wildcard binding",
+ })
+ var wildcardDomain map[string]any
+ decodeResponseData(t, createWildcard, &wildcardDomain)
+
+ createExact := performJSONRequest(t, engine, token, http.MethodPost, "/api/managed-domains/", map[string]any{
+ "domain": "api.example.com",
+ "cert_id": exactID,
+ "enabled": true,
+ "remark": "exact binding",
+ })
+ var exactDomain map[string]any
+ decodeResponseData(t, createExact, &exactDomain)
+
+ listResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/managed-domains/", nil)
+ var domains []map[string]any
+ decodeResponseData(t, listResp, &domains)
+ if len(domains) != 2 {
+ t.Fatalf("expected 2 managed domains, got %d", len(domains))
+ }
+
+ matchResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/managed-domains/match?domain=api.example.com", nil)
+ var matchResult map[string]any
+ decodeResponseData(t, matchResp, &matchResult)
+ if matched, ok := matchResult["matched"].(bool); !ok || !matched {
+ t.Fatalf("expected exact domain to be matched, got %#v", matchResult)
+ }
+ candidate, ok := matchResult["candidate"].(map[string]any)
+ if !ok {
+ t.Fatalf("expected candidate payload, got %#v", matchResult["candidate"])
+ }
+ if candidate["match_type"] != "exact" {
+ t.Fatalf("expected exact match type, got %#v", candidate["match_type"])
+ }
+ if uint(candidate["certificate_id"].(float64)) != exactID {
+ t.Fatalf("expected exact certificate id %d, got %#v", exactID, candidate["certificate_id"])
+ }
+
+ updateResp := performJSONRequest(t, engine, token, http.MethodPut, "/api/managed-domains/"+toString(uint(exactDomain["id"].(float64))), map[string]any{
+ "domain": "api.example.com",
+ "cert_id": exactID,
+ "enabled": false,
+ "remark": "disabled exact binding",
+ })
+ decodeResponseData(t, updateResp, &exactDomain)
+
+ matchResp = performJSONRequest(t, engine, token, http.MethodGet, "/api/managed-domains/match?domain=api.example.com", nil)
+ decodeResponseData(t, matchResp, &matchResult)
+ candidate, ok = matchResult["candidate"].(map[string]any)
+ if !ok {
+ t.Fatalf("expected wildcard fallback candidate, got %#v", matchResult["candidate"])
+ }
+ if candidate["match_type"] != "wildcard" {
+ t.Fatalf("expected wildcard fallback, got %#v", candidate["match_type"])
+ }
+ if uint(candidate["certificate_id"].(float64)) != wildcardID {
+ t.Fatalf("expected wildcard certificate id %d, got %#v", wildcardID, candidate["certificate_id"])
+ }
+
+ deleteResp := performJSONRequest(t, engine, token, http.MethodDelete, "/api/managed-domains/"+toString(uint(wildcardDomain["id"].(float64))), nil)
+ if !deleteResp.Success {
+ t.Fatalf("expected delete success, got %s", deleteResp.Message)
+ }
+}
diff --git a/atsf_server/service/managed_domain.go b/atsf_server/service/managed_domain.go
new file mode 100644
index 00000000..7543d9a5
--- /dev/null
+++ b/atsf_server/service/managed_domain.go
@@ -0,0 +1,227 @@
+package service
+
+import (
+ "errors"
+ "fmt"
+ "gin-template/model"
+ "sort"
+ "strings"
+ "unicode"
+)
+
+const (
+ ManagedDomainMatchTypeExact = "exact"
+ ManagedDomainMatchTypeWildcard = "wildcard"
+)
+
+type ManagedDomainInput struct {
+ Domain string `json:"domain"`
+ CertID *uint `json:"cert_id"`
+ Enabled bool `json:"enabled"`
+ Remark string `json:"remark"`
+}
+
+type ManagedDomainMatchCandidate struct {
+ ManagedDomainID uint `json:"managed_domain_id"`
+ Domain string `json:"domain"`
+ MatchType string `json:"match_type"`
+ CertificateID uint `json:"certificate_id"`
+ CertificateName string `json:"certificate_name"`
+}
+
+type ManagedDomainMatchResult struct {
+ Domain string `json:"domain"`
+ Matched bool `json:"matched"`
+ Candidate *ManagedDomainMatchCandidate `json:"candidate,omitempty"`
+ Candidates []ManagedDomainMatchCandidate `json:"candidates"`
+}
+
+func ListManagedDomains() ([]*model.ManagedDomain, error) {
+ return model.ListManagedDomains()
+}
+
+func CreateManagedDomain(input ManagedDomainInput) (*model.ManagedDomain, error) {
+ domain, err := buildManagedDomain(nil, input)
+ if err != nil {
+ return nil, err
+ }
+ if err = domain.Insert(); err != nil {
+ if isUniqueConstraintError(err) {
+ return nil, errors.New("域名已存在")
+ }
+ return nil, err
+ }
+ return domain, nil
+}
+
+func UpdateManagedDomain(id uint, input ManagedDomainInput) (*model.ManagedDomain, error) {
+ domain, err := model.GetManagedDomainByID(id)
+ if err != nil {
+ return nil, err
+ }
+ domain, err = buildManagedDomain(domain, input)
+ if err != nil {
+ return nil, err
+ }
+ if err = domain.Update(); err != nil {
+ if isUniqueConstraintError(err) {
+ return nil, errors.New("域名已存在")
+ }
+ return nil, err
+ }
+ return domain, nil
+}
+
+func DeleteManagedDomain(id uint) error {
+ domain, err := model.GetManagedDomainByID(id)
+ if err != nil {
+ return err
+ }
+ return domain.Delete()
+}
+
+func MatchManagedDomainCertificate(rawDomain string) (*ManagedDomainMatchResult, error) {
+ domain := normalizeManagedDomain(rawDomain)
+ if err := validateManagedDomainPattern(domain); err != nil {
+ return nil, err
+ }
+ managedDomains, err := model.ListEnabledManagedDomainsWithCertificate()
+ if err != nil {
+ return nil, err
+ }
+ candidates := make([]ManagedDomainMatchCandidate, 0)
+ for _, item := range managedDomains {
+ if item.CertID == nil || *item.CertID == 0 {
+ continue
+ }
+ matchType := detectManagedDomainMatchType(item.Domain, domain)
+ if matchType == "" {
+ continue
+ }
+ certificate, err := model.GetTLSCertificateByID(*item.CertID)
+ if err != nil {
+ return nil, fmt.Errorf("托管域名 %s 关联证书不存在", item.Domain)
+ }
+ candidates = append(candidates, ManagedDomainMatchCandidate{
+ ManagedDomainID: item.ID,
+ Domain: item.Domain,
+ MatchType: matchType,
+ CertificateID: certificate.ID,
+ CertificateName: certificate.Name,
+ })
+ }
+ sortManagedDomainCandidates(candidates)
+ result := &ManagedDomainMatchResult{
+ Domain: domain,
+ Matched: len(candidates) > 0,
+ Candidates: candidates,
+ }
+ if len(candidates) > 0 {
+ candidate := candidates[0]
+ result.Candidate = &candidate
+ }
+ return result, nil
+}
+
+func buildManagedDomain(existing *model.ManagedDomain, input ManagedDomainInput) (*model.ManagedDomain, error) {
+ domain := normalizeManagedDomain(input.Domain)
+ remark := strings.TrimSpace(input.Remark)
+ if err := validateManagedDomainPattern(domain); err != nil {
+ return nil, err
+ }
+ if input.CertID != nil && *input.CertID != 0 {
+ if _, err := model.GetTLSCertificateByID(*input.CertID); err != nil {
+ return nil, errors.New("所选证书不存在")
+ }
+ } else {
+ input.CertID = nil
+ }
+ if existing == nil {
+ existing = &model.ManagedDomain{}
+ }
+ existing.Domain = domain
+ existing.CertID = input.CertID
+ existing.Enabled = input.Enabled
+ existing.Remark = remark
+ return existing, nil
+}
+
+func normalizeManagedDomain(domain string) string {
+ return strings.ToLower(strings.TrimSpace(domain))
+}
+
+func validateManagedDomainPattern(domain string) error {
+ if domain == "" {
+ return errors.New("域名不能为空")
+ }
+ if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
+ return errors.New("域名格式不合法")
+ }
+ if strings.Contains(domain, "*") {
+ if !strings.HasPrefix(domain, "*.") || strings.Count(domain, "*") != 1 {
+ return errors.New("通配符域名仅支持 *.example.com 格式")
+ }
+ return validateHostname(strings.TrimPrefix(domain, "*."))
+ }
+ return validateHostname(domain)
+}
+
+func validateHostname(domain string) error {
+ if domain == "" {
+ return errors.New("域名不能为空")
+ }
+ if len(domain) > 253 {
+ return errors.New("域名格式不合法")
+ }
+ labels := strings.Split(domain, ".")
+ if len(labels) < 2 {
+ return errors.New("域名格式不合法")
+ }
+ for _, label := range labels {
+ if len(label) == 0 || len(label) > 63 {
+ return errors.New("域名格式不合法")
+ }
+ if label[0] == '-' || label[len(label)-1] == '-' {
+ return errors.New("域名格式不合法")
+ }
+ for _, r := range label {
+ if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' {
+ continue
+ }
+ return errors.New("域名格式不合法")
+ }
+ }
+ return nil
+}
+
+func detectManagedDomainMatchType(pattern string, domain string) string {
+ if pattern == domain {
+ return ManagedDomainMatchTypeExact
+ }
+ if !strings.HasPrefix(pattern, "*.") {
+ return ""
+ }
+ suffix := strings.TrimPrefix(pattern, "*.")
+ if !strings.HasSuffix(domain, "."+suffix) {
+ return ""
+ }
+ prefix := strings.TrimSuffix(domain, "."+suffix)
+ if prefix == "" || strings.Contains(prefix, ".") {
+ return ""
+ }
+ return ManagedDomainMatchTypeWildcard
+}
+
+func sortManagedDomainCandidates(candidates []ManagedDomainMatchCandidate) {
+ sort.Slice(candidates, func(i int, j int) bool {
+ left := candidates[i]
+ right := candidates[j]
+ if left.MatchType != right.MatchType {
+ return left.MatchType == ManagedDomainMatchTypeExact
+ }
+ if len(left.Domain) != len(right.Domain) {
+ return len(left.Domain) > len(right.Domain)
+ }
+ return left.ManagedDomainID < right.ManagedDomainID
+ })
+}
diff --git a/atsf_server/service/managed_domain_phase2_test.go b/atsf_server/service/managed_domain_phase2_test.go
new file mode 100644
index 00000000..ecc6af6e
--- /dev/null
+++ b/atsf_server/service/managed_domain_phase2_test.go
@@ -0,0 +1,109 @@
+package service
+
+import "testing"
+
+func TestMatchManagedDomainCertificatePrefersExactMatch(t *testing.T) {
+ setupServiceTestDB(t)
+
+ wildcardCertPEM, wildcardKeyPEM := generateCertificatePair(t, []string{"*.example.com"})
+ wildcardCert, err := CreateTLSCertificate(TLSCertificateInput{
+ Name: "wildcard-cert",
+ CertPEM: wildcardCertPEM,
+ KeyPEM: wildcardKeyPEM,
+ })
+ if err != nil {
+ t.Fatalf("failed to create wildcard certificate: %v", err)
+ }
+ exactCertPEM, exactKeyPEM := generateCertificatePair(t, []string{"api.example.com"})
+ exactCert, err := CreateTLSCertificate(TLSCertificateInput{
+ Name: "exact-cert",
+ CertPEM: exactCertPEM,
+ KeyPEM: exactKeyPEM,
+ })
+ if err != nil {
+ t.Fatalf("failed to create exact certificate: %v", err)
+ }
+ if _, err = CreateManagedDomain(ManagedDomainInput{
+ Domain: "*.example.com",
+ CertID: &wildcardCert.ID,
+ Enabled: true,
+ }); err != nil {
+ t.Fatalf("failed to create wildcard managed domain: %v", err)
+ }
+ if _, err = CreateManagedDomain(ManagedDomainInput{
+ Domain: "api.example.com",
+ CertID: &exactCert.ID,
+ Enabled: true,
+ }); err != nil {
+ t.Fatalf("failed to create exact managed domain: %v", err)
+ }
+
+ result, err := MatchManagedDomainCertificate("api.example.com")
+ if err != nil {
+ t.Fatalf("MatchManagedDomainCertificate failed: %v", err)
+ }
+ if !result.Matched || result.Candidate == nil {
+ t.Fatal("expected exact domain to be matched")
+ }
+ if result.Candidate.MatchType != ManagedDomainMatchTypeExact {
+ t.Fatalf("expected exact match first, got %s", result.Candidate.MatchType)
+ }
+ if result.Candidate.CertificateID != exactCert.ID {
+ t.Fatalf("expected exact certificate %d, got %d", exactCert.ID, result.Candidate.CertificateID)
+ }
+ if len(result.Candidates) != 2 {
+ t.Fatalf("expected 2 match candidates, got %d", len(result.Candidates))
+ }
+}
+
+func TestMatchManagedDomainCertificateSupportsWildcard(t *testing.T) {
+ setupServiceTestDB(t)
+
+ certPEM, keyPEM := generateCertificatePair(t, []string{"*.example.com"})
+ certificate, err := CreateTLSCertificate(TLSCertificateInput{
+ Name: "wildcard-cert",
+ CertPEM: certPEM,
+ KeyPEM: keyPEM,
+ })
+ if err != nil {
+ t.Fatalf("failed to create certificate: %v", err)
+ }
+ if _, err = CreateManagedDomain(ManagedDomainInput{
+ Domain: "*.example.com",
+ CertID: &certificate.ID,
+ Enabled: true,
+ }); err != nil {
+ t.Fatalf("failed to create managed domain: %v", err)
+ }
+
+ result, err := MatchManagedDomainCertificate("edge.example.com")
+ if err != nil {
+ t.Fatalf("MatchManagedDomainCertificate failed: %v", err)
+ }
+ if !result.Matched || result.Candidate == nil {
+ t.Fatal("expected wildcard domain to be matched")
+ }
+ if result.Candidate.MatchType != ManagedDomainMatchTypeWildcard {
+ t.Fatalf("expected wildcard match, got %s", result.Candidate.MatchType)
+ }
+
+ deepResult, err := MatchManagedDomainCertificate("deep.edge.example.com")
+ if err != nil {
+ t.Fatalf("MatchManagedDomainCertificate failed: %v", err)
+ }
+ if deepResult.Matched {
+ t.Fatal("expected single-level wildcard not to match deep subdomain")
+ }
+}
+
+func TestCreateManagedDomainRejectsInvalidWildcard(t *testing.T) {
+ setupServiceTestDB(t)
+
+ _, err := CreateManagedDomain(ManagedDomainInput{
+ Domain: "*.*.example.com",
+ Enabled: true,
+ })
+ if err == nil {
+ t.Fatal("expected invalid wildcard domain to fail")
+ }
+}
diff --git a/atsf_server/web/src/App.js b/atsf_server/web/src/App.js
index 2d9b9e99..38ed8f3f 100644
--- a/atsf_server/web/src/App.js
+++ b/atsf_server/web/src/App.js
@@ -1,223 +1,232 @@
-import React, { lazy, Suspense, useContext, useEffect } from 'react';
-import { Route, Routes } from 'react-router-dom';
-import Loading from './components/Loading';
-import User from './pages/User';
-import { PrivateRoute } from './components/PrivateRoute';
-import RegisterForm from './components/RegisterForm';
-import LoginForm from './components/LoginForm';
-import NotFound from './pages/NotFound';
-import Setting from './pages/Setting';
-import EditUser from './pages/User/EditUser';
-import AddUser from './pages/User/AddUser';
-import { API, showError, showNotice } from './helpers';
-import PasswordResetForm from './components/PasswordResetForm';
-import GitHubOAuth from './components/GitHubOAuth';
-import PasswordResetConfirm from './components/PasswordResetConfirm';
-import { UserContext } from './context/User';
-import { StatusContext } from './context/Status';
-import File from './pages/File';
-import ProxyRoute from './pages/ProxyRoute';
-import ConfigVersion from './pages/ConfigVersion';
-import Node from './pages/Node';
-import ApplyLog from './pages/ApplyLog';
-import TLSCertificate from './pages/TLSCertificate';
-
-const Home = lazy(() => import('./pages/Home'));
-const About = lazy(() => import('./pages/About'));
-
-function App() {
- const [userState, userDispatch] = useContext(UserContext);
- const [statusState, statusDispatch] = useContext(StatusContext);
-
- const loadUser = () => {
- let user = localStorage.getItem('user');
- if (user) {
- let data = JSON.parse(user);
- userDispatch({ type: 'login', payload: data });
- }
- };
- const loadStatus = async () => {
- const res = await API.get('/api/status');
- const { success, data } = res.data;
- if (success) {
- console.log(
- `GitHub 仓库地址:https://github.com/songquanpeng/gin-template`
- );
- localStorage.setItem('status', JSON.stringify(data));
- statusDispatch({ type: 'set', payload: data });
- localStorage.setItem('system_name', data.system_name);
- localStorage.setItem('footer_html', data.footer_html);
- localStorage.setItem('home_page_link', data.home_page_link);
- if (
- data.version !== process.env.REACT_APP_VERSION &&
- data.version !== 'v0.0.0' &&
- process.env.REACT_APP_VERSION !== ''
- ) {
- showNotice(
- `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
- );
- }
- } else {
- showError('无法正常连接至服务器!');
- }
- };
-
- useEffect(() => {
- loadUser();
- loadStatus().then();
- }, []);
-
- return (
-
维护精确域名与通配符域名,并为其绑定默认 TLS 证书。
+ ++ {matching + ? '正在按域名自动匹配证书...' + : matchResult?.matched + ? `已匹配${matchResult.candidate?.match_type === 'exact' ? '精确' : '通配符'}规则 ${matchResult.candidate?.domain},推荐证书:${matchResult.candidate?.certificate_name}` + : form.domain.trim() + ? '未找到匹配证书,可继续手动选择。' + : '输入域名后会自动匹配托管证书。'} +
+ ) : null}