mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
refactor(zone): remove legacy route domain storage
第二阶段清理:删除 of_managed_domains 与 of_proxy_routes 冗余域名/证书列, 移除 ManagedDomain 模型与 API、路由侧 legacy 字段维护,以及前端 WebsiteService。 ImportLegacy 在旧列/旧表缺失时跳过对应源,保持幂等。
This commit is contained in:
@@ -17,11 +17,6 @@ const (
|
||||
errCertificateFilesRequired = "certificate file and key file cannot be empty"
|
||||
errCertificatePEMInvalid = "证书 PEM 内容不合法"
|
||||
|
||||
errManagedDomainRequired = "域名不能为空"
|
||||
errManagedDomainInvalid = "域名格式不合法"
|
||||
errManagedDomainWildcardInvalid = "通配符域名仅支持 *.example.com 格式"
|
||||
errManagedDomainExists = "域名已存在"
|
||||
errManagedDomainCertNotFound = "所选证书不存在"
|
||||
|
||||
errDNSAccountInUse = "该 DNS 账号已被证书使用,无法删除"
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ func setupTLSTestDB(t *testing.T) func() {
|
||||
&model.TLSCertificate{},
|
||||
&model.Zone{},
|
||||
&model.ZoneDomain{},
|
||||
&model.ManagedDomain{},
|
||||
&model.DNSAccount{},
|
||||
&model.AcmeAccount{},
|
||||
&model.TaskExecution{}, // 异步任务执行记录也需要 migrate
|
||||
@@ -111,54 +110,6 @@ func generateTestCertificatePair(t *testing.T, dnsNames []string) (string, strin
|
||||
return string(certPEM), string(keyPEM)
|
||||
}
|
||||
|
||||
func TestCreateManagedDomain(t *testing.T) {
|
||||
cleanup := setupTLSTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
certPEM, keyPEM := generateTestCertificatePair(t, []string{"api.example.com"})
|
||||
certificate, err := CreateCertificate(ctx, CertificateInput{
|
||||
Name: "api-cert",
|
||||
CertPEM: certPEM,
|
||||
KeyPEM: keyPEM,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
certID := certificate.ID
|
||||
domain, err := CreateManagedDomain(ctx, ManagedDomainInput{
|
||||
Domain: "api.example.com",
|
||||
CertID: &certID,
|
||||
Enabled: true,
|
||||
Remark: "primary api",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, domain.ID)
|
||||
assert.Equal(t, "api.example.com", domain.Domain)
|
||||
assert.Equal(t, certID, *domain.CertID)
|
||||
assert.True(t, domain.Enabled)
|
||||
assert.Equal(t, "primary api", domain.Remark)
|
||||
|
||||
_, err = CreateManagedDomain(ctx, ManagedDomainInput{
|
||||
Domain: "api.example.com",
|
||||
Enabled: true,
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, errManagedDomainExists, err.Error())
|
||||
}
|
||||
|
||||
func TestCreateManagedDomainRejectsInvalidWildcard(t *testing.T) {
|
||||
cleanup := setupTLSTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := CreateManagedDomain(ctx, ManagedDomainInput{
|
||||
Domain: "*.*.example.com",
|
||||
Enabled: true,
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, errManagedDomainWildcardInvalid, err.Error())
|
||||
}
|
||||
|
||||
func TestCreateCertificateEncryptsPrivateKey(t *testing.T) {
|
||||
cleanup := setupTLSTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
managedDomainMatchTypeExact = "exact"
|
||||
managedDomainMatchTypeWildcard = "wildcard"
|
||||
|
||||
maxManagedDomainLength = 253
|
||||
minManagedDomainLabelCount = 2
|
||||
)
|
||||
|
||||
// ManagedDomainInput 托管域名创建/更新请求。
|
||||
type ManagedDomainInput struct {
|
||||
Domain string `json:"domain"`
|
||||
CertID *uint `json:"cert_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ManagedDomainMatchCandidate 证书匹配候选。
|
||||
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"`
|
||||
}
|
||||
|
||||
// ManagedDomainMatchResult 证书匹配结果。
|
||||
type ManagedDomainMatchResult struct {
|
||||
Domain string `json:"domain"`
|
||||
Matched bool `json:"matched"`
|
||||
Candidate *ManagedDomainMatchCandidate `json:"candidate,omitempty"`
|
||||
Candidates []ManagedDomainMatchCandidate `json:"candidates"`
|
||||
}
|
||||
|
||||
// ListManagedDomains 列出托管域名。
|
||||
func ListManagedDomains(ctx context.Context) ([]model.ManagedDomain, error) {
|
||||
return model.ListManagedDomains(ctx)
|
||||
}
|
||||
|
||||
// CreateManagedDomain 创建托管域名。
|
||||
func CreateManagedDomain(ctx context.Context, input ManagedDomainInput) (*model.ManagedDomain, error) {
|
||||
domain, err := buildManagedDomain(ctx, nil, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = model.CreateManagedDomainRecord(ctx, domain); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New(errManagedDomainExists)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
// UpdateManagedDomain 更新托管域名。
|
||||
func UpdateManagedDomain(ctx context.Context, id uint, input ManagedDomainInput) (*model.ManagedDomain, error) {
|
||||
domain, err := model.GetManagedDomainByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domain, err = buildManagedDomain(ctx, domain, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = model.SaveManagedDomain(ctx, domain); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New(errManagedDomainExists)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
// DeleteManagedDomain 删除托管域名。
|
||||
func DeleteManagedDomain(ctx context.Context, id uint) error {
|
||||
if _, err := model.GetManagedDomainByID(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return model.DeleteManagedDomainRecord(ctx, id)
|
||||
}
|
||||
|
||||
// MatchManagedDomainCertificate 为域名匹配证书。
|
||||
func MatchManagedDomainCertificate(ctx context.Context, rawDomain string) (*ManagedDomainMatchResult, error) {
|
||||
domain := normalizeManagedDomain(rawDomain)
|
||||
if err := validateManagedDomainPattern(domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
managedDomains, err := model.ListEnabledManagedDomainsWithCertificate(ctx)
|
||||
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(ctx, *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(ctx context.Context, 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(ctx, *input.CertID); err != nil {
|
||||
return nil, errors.New(errManagedDomainCertNotFound)
|
||||
}
|
||||
} 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(errManagedDomainRequired)
|
||||
}
|
||||
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
||||
return errors.New(errManagedDomainInvalid)
|
||||
}
|
||||
if strings.Contains(domain, "*") {
|
||||
if !strings.HasPrefix(domain, "*.") || strings.Count(domain, "*") != 1 {
|
||||
return errors.New(errManagedDomainWildcardInvalid)
|
||||
}
|
||||
return validateHostname(strings.TrimPrefix(domain, "*."))
|
||||
}
|
||||
return validateHostname(domain)
|
||||
}
|
||||
|
||||
func validateHostname(domain string) error {
|
||||
if domain == "" {
|
||||
return errors.New(errManagedDomainRequired)
|
||||
}
|
||||
if len(domain) > maxManagedDomainLength {
|
||||
return errors.New(errManagedDomainInvalid)
|
||||
}
|
||||
labels := strings.Split(domain, ".")
|
||||
if len(labels) < minManagedDomainLabelCount {
|
||||
return errors.New(errManagedDomainInvalid)
|
||||
}
|
||||
for _, label := range labels {
|
||||
if len(label) == 0 || len(label) > 63 {
|
||||
return errors.New(errManagedDomainInvalid)
|
||||
}
|
||||
if label[0] == '-' || label[len(label)-1] == '-' {
|
||||
return errors.New(errManagedDomainInvalid)
|
||||
}
|
||||
for _, r := range label {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' {
|
||||
continue
|
||||
}
|
||||
return errors.New(errManagedDomainInvalid)
|
||||
}
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -4,12 +4,11 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -328,126 +327,6 @@ func RenewCertificateHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(certificate))
|
||||
}
|
||||
|
||||
// GetManagedDomains 列出托管域名。
|
||||
// @Summary 列出托管域名
|
||||
// @Description 返回全部托管域名及关联证书,需要管理员权限
|
||||
// @Tags openflare-tls
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.ManagedDomain} "托管域名列表"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
func GetManagedDomains(c *gin.Context) {
|
||||
domains, err := ListManagedDomains(c.Request.Context())
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(domains))
|
||||
}
|
||||
|
||||
// CreateManagedDomainHandler 创建托管域名。
|
||||
// @Summary 创建托管域名
|
||||
// @Description 创建新的托管域名记录,需要管理员权限
|
||||
// @Tags openflare-tls
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body tls.ManagedDomainInput true "托管域名参数"
|
||||
// @Success 200 {object} response.Any{data=model.ManagedDomain} "创建成功的托管域名"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
func CreateManagedDomainHandler(c *gin.Context) {
|
||||
var input ManagedDomainInput
|
||||
if !apiutil.BindJSON(c, &input) {
|
||||
return
|
||||
}
|
||||
domain, err := CreateManagedDomain(c.Request.Context(), input)
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(domain))
|
||||
}
|
||||
|
||||
// UpdateManagedDomainHandler 更新托管域名。
|
||||
// @Summary 更新托管域名
|
||||
// @Description 按 ID 更新托管域名,需要管理员权限
|
||||
// @Tags openflare-tls
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "托管域名 ID"
|
||||
// @Param request body tls.ManagedDomainInput true "托管域名参数"
|
||||
// @Success 200 {object} response.Any{data=model.ManagedDomain} "更新后的托管域名"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Failure 404 {object} response.Any "记录不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
func UpdateManagedDomainHandler(c *gin.Context) {
|
||||
id, ok := apiutil.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input ManagedDomainInput
|
||||
if !apiutil.BindJSON(c, &input) {
|
||||
return
|
||||
}
|
||||
domain, err := UpdateManagedDomain(c.Request.Context(), id, input)
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(domain))
|
||||
}
|
||||
|
||||
// DeleteManagedDomainHandler 删除托管域名。
|
||||
// @Summary 删除托管域名
|
||||
// @Description 按 ID 删除托管域名,需要管理员权限
|
||||
// @Tags openflare-tls
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "托管域名 ID"
|
||||
// @Success 200 {object} response.Any "删除成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Failure 404 {object} response.Any "记录不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
func DeleteManagedDomainHandler(c *gin.Context) {
|
||||
id, ok := apiutil.IDParam(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := DeleteManagedDomain(c.Request.Context(), id); handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// MatchManagedDomainCertificateHandler 匹配域名证书。
|
||||
// @Summary 匹配托管域名证书
|
||||
// @Description 按域名查询可用的证书匹配候选,需要管理员权限
|
||||
// @Tags openflare-tls
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param domain query string true "域名"
|
||||
// @Success 200 {object} response.Any{data=tls.ManagedDomainMatchResult} "证书匹配结果"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
func MatchManagedDomainCertificateHandler(c *gin.Context) {
|
||||
domain := strings.TrimSpace(c.Query("domain"))
|
||||
result, err := MatchManagedDomainCertificate(c.Request.Context(), domain)
|
||||
if handleLogicError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
}
|
||||
|
||||
// GetDNSAccounts 列出 DNS 账号。
|
||||
// @Summary 列出 DNS 账号
|
||||
// @Description 返回全部 DNS 提供商账号,需要管理员权限
|
||||
|
||||
Reference in New Issue
Block a user