This commit is contained in:
parent
bb2477b8b9
commit
56f4abc632
@ -30,6 +30,7 @@ type Config struct {
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"` //多域名设置,把所有收信域名都填进去
|
||||
WebDomain string `json:"webDomain"`
|
||||
AdminRecoveryDomain string `json:"adminRecoveryDomain,omitempty"`
|
||||
AccessHosts []string `json:"accessHosts,omitempty"`
|
||||
DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"`
|
||||
SSLType string `json:"sslType"` // 0表示自动生成证书,HTTP挑战模式,1表示用户上传证书,2表示自动-DNS挑战模式
|
||||
|
||||
@ -49,6 +49,7 @@ type legacyMigrationConfig struct {
|
||||
Domain string `json:"domain"`
|
||||
Domains []string `json:"domains"`
|
||||
WebDomain string `json:"webDomain"`
|
||||
AdminRecoveryDomain string `json:"adminRecoveryDomain"`
|
||||
AccessHosts []string `json:"accessHosts"`
|
||||
DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"`
|
||||
SSLType string `json:"sslType"`
|
||||
@ -88,7 +89,7 @@ type legacyMigrationConfig struct {
|
||||
}
|
||||
|
||||
var legacyMigrationKnownFields = map[string]struct{}{
|
||||
"logLevel": {}, "domain": {}, "domains": {}, "webDomain": {}, "accessHosts": {},
|
||||
"logLevel": {}, "domain": {}, "domains": {}, "webDomain": {}, "adminRecoveryDomain": {}, "accessHosts": {},
|
||||
"dkimPrivateKeyPath": {}, "sslType": {}, "SSLPrivateKeyPath": {}, "SSLPublicKeyPath": {},
|
||||
"dbDSN": {}, "dbType": {}, "httpsEnabled": {}, "spamFilterLevel": {}, "httpPort": {}, "httpsPort": {}, "tlsMode": {}, "trustedProxyCidrs": {},
|
||||
"weChatPushAppId": {}, "weChatPushSecret": {}, "weChatPushTemplateId": {}, "weChatPushUserId": {},
|
||||
@ -499,10 +500,11 @@ func legacyRuntimeConfig(root string, legacy legacyMigrationConfig) (RuntimeConf
|
||||
LogLevel: legacy.LogLevel,
|
||||
Database: database,
|
||||
Domains: DomainConfig{
|
||||
Primary: legacy.Domain,
|
||||
Accepted: accepted,
|
||||
Web: legacy.WebDomain,
|
||||
AccessHosts: append([]string(nil), legacy.AccessHosts...),
|
||||
Primary: legacy.Domain,
|
||||
Accepted: accepted,
|
||||
Web: legacy.WebDomain,
|
||||
AdminRecovery: legacy.AdminRecoveryDomain,
|
||||
AccessHosts: append([]string(nil), legacy.AccessHosts...),
|
||||
},
|
||||
HTTP: HTTPConfig{
|
||||
Port: legacy.HttpPort,
|
||||
|
||||
@ -52,8 +52,9 @@ func runtimeCompatibility(runtime RuntimeConfig) (*Config, error) {
|
||||
cfg := &Config{
|
||||
LogLevel: root.LogLevel, Domain: root.Domains.Primary,
|
||||
Domains: append([]string(nil), root.Domains.Accepted...), WebDomain: root.Domains.Web,
|
||||
AccessHosts: append([]string(nil), root.Domains.AccessHosts...),
|
||||
DbType: driver, DbDSN: dsn, SetupState: runtime.SetupState,
|
||||
AdminRecoveryDomain: root.Domains.AdminRecovery,
|
||||
AccessHosts: append([]string(nil), root.Domains.AccessHosts...),
|
||||
DbType: driver, DbDSN: dsn, SetupState: runtime.SetupState,
|
||||
IsInit: runtime.SetupState == SetupStateReady,
|
||||
HttpPort: root.HTTP.Port, HttpsPort: root.HTTP.HTTPSPort,
|
||||
HttpsEnabled: 2, TLSMode: root.HTTP.TLSMode,
|
||||
|
||||
@ -55,7 +55,7 @@ func TestPartitionedCompatibilityStartupAndWriteProtection(t *testing.T) {
|
||||
PublishConfigSnapshot(oldSnapshot)
|
||||
})
|
||||
writeLegacyMigrationFixture(t, root, map[string]any{
|
||||
"logLevel": "info", "domain": "mail.example.com", "webDomain": "mail.example.com",
|
||||
"logLevel": "info", "domain": "mail.example.com", "webDomain": "mail.example.com", "adminRecoveryDomain": "recovery.mail.example.com",
|
||||
"dbType": "sqlite", "dbDSN": "./config/existing.db", "isInit": true,
|
||||
"sslType": "1", "SSLPublicKeyPath": "./config/ssl/site.crt", "SSLPrivateKeyPath": "./config/ssl/site.key",
|
||||
"httpPort": 80, "httpsPort": 443, "httpsEnabled": 1,
|
||||
@ -76,7 +76,7 @@ func TestPartitionedCompatibilityStartupAndWriteProtection(t *testing.T) {
|
||||
if !cfg.IsSetupReady() || cfg.DbType != DBTypeSQLite || cfg.DbDSN != filepath.Join(root, "data", "existing.db") {
|
||||
t.Fatal("partitioned startup lost initialization or database identity")
|
||||
}
|
||||
if cfg.WebDomain != "mail.example.com" || cfg.SSL.CertificateRef != "data/ssl/site.crt" {
|
||||
if cfg.WebDomain != "mail.example.com" || cfg.AdminRecoveryDomain != "recovery.mail.example.com" || cfg.SSL.CertificateRef != "data/ssl/site.crt" {
|
||||
t.Fatal("partitioned startup lost domain or certificate path")
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +133,28 @@ func CurrentWebDomain() string {
|
||||
return cfg.WebDomain
|
||||
}
|
||||
|
||||
// CurrentAdminRecoveryDomain 返回超级管理员本机恢复通道的部署标识域名。
|
||||
// 此值不用于提供公网重置路由;未配置时沿用主邮件域名。
|
||||
func CurrentAdminRecoveryDomain() string {
|
||||
if manager := CurrentManager(); manager != nil {
|
||||
runtime := manager.Snapshot()
|
||||
if runtime.Generation != 0 {
|
||||
if domain := runtime.Root.Domains.AdminRecovery; domain != "" {
|
||||
return domain
|
||||
}
|
||||
return runtime.Root.Domains.Primary
|
||||
}
|
||||
}
|
||||
cfg := CurrentConfig()
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
if cfg.AdminRecoveryDomain != "" {
|
||||
return cfg.AdminRecoveryDomain
|
||||
}
|
||||
return cfg.Domain
|
||||
}
|
||||
|
||||
// SetRuntimeSetupPort stores the temporary setup listener port outside the persisted configuration.
|
||||
func SetRuntimeSetupPort(port int) {
|
||||
setupPort.Store(int64(port))
|
||||
|
||||
@ -216,6 +216,42 @@ func TestCurrentMailDomainsFallsBackToLegacyConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentAdminRecoveryDomainUsesConfiguredValueAndSafeFallback(t *testing.T) {
|
||||
previousManager := CurrentManager()
|
||||
previousInstance := Instance
|
||||
defer func() {
|
||||
SetRuntimeManager(previousManager)
|
||||
Instance = previousInstance
|
||||
PublishConfigSnapshot(previousInstance)
|
||||
}()
|
||||
|
||||
runtime := validRuntimeConfig()
|
||||
runtime.Root.Domains.AdminRecovery = "recovery.example.com"
|
||||
SetRuntimeManager(&ConfigManager{snapshots: runtimeAccessSnapshotStore{runtime: runtime}})
|
||||
if got := CurrentAdminRecoveryDomain(); got != "recovery.example.com" {
|
||||
t.Fatalf("configured recovery domain = %q", got)
|
||||
}
|
||||
|
||||
runtime.Root.Domains.AdminRecovery = ""
|
||||
runtime.Root.Domains.Primary = "example.com"
|
||||
SetRuntimeManager(&ConfigManager{snapshots: runtimeAccessSnapshotStore{runtime: runtime}})
|
||||
if got := CurrentAdminRecoveryDomain(); got != "example.com" {
|
||||
t.Fatalf("partition fallback recovery domain = %q", got)
|
||||
}
|
||||
|
||||
SetRuntimeManager(nil)
|
||||
Instance = &Config{Domain: "legacy.example.com", AdminRecoveryDomain: "recovery.legacy.example.com"}
|
||||
PublishConfigSnapshot(Instance)
|
||||
if got := CurrentAdminRecoveryDomain(); got != "recovery.legacy.example.com" {
|
||||
t.Fatalf("legacy configured recovery domain = %q", got)
|
||||
}
|
||||
Instance = &Config{Domain: "legacy.example.com"}
|
||||
PublishConfigSnapshot(Instance)
|
||||
if got := CurrentAdminRecoveryDomain(); got != "legacy.example.com" {
|
||||
t.Fatalf("legacy fallback recovery domain = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSMTPRuntimeSettingsReadsPartitionSnapshot(t *testing.T) {
|
||||
previousManager := CurrentManager()
|
||||
defer SetRuntimeManager(previousManager)
|
||||
|
||||
@ -83,7 +83,7 @@ func ValidateRuntimeConfig(runtime RuntimeConfig, options RuntimeValidationOptio
|
||||
}
|
||||
|
||||
func validateDomains(domains DomainConfig) error {
|
||||
if domains.Primary == "" && domains.Web == "" && len(domains.Accepted) == 0 && len(domains.AccessHosts) == 0 {
|
||||
if domains.Primary == "" && domains.Web == "" && domains.AdminRecovery == "" && len(domains.Accepted) == 0 && len(domains.AccessHosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
primary, err := normalizeConfigDomain(domains.Primary)
|
||||
@ -94,6 +94,12 @@ func validateDomains(domains DomainConfig) error {
|
||||
if err != nil || web != strings.ToLower(strings.TrimSuffix(domains.Web, ".")) {
|
||||
return ErrInvalidDomainConfig
|
||||
}
|
||||
if domains.AdminRecovery != "" {
|
||||
recovery, recoveryErr := normalizeConfigDomain(domains.AdminRecovery)
|
||||
if recoveryErr != nil || recovery != strings.ToLower(strings.TrimSuffix(domains.AdminRecovery, ".")) {
|
||||
return ErrInvalidDomainConfig
|
||||
}
|
||||
}
|
||||
seen := make(map[string]struct{}, len(domains.Accepted))
|
||||
for _, item := range domains.Accepted {
|
||||
domain, normalizeErr := normalizeConfigDomain(item)
|
||||
|
||||
@ -65,6 +65,9 @@ func TestValidateRuntimeConfigRejectsCrossSectionViolations(t *testing.T) {
|
||||
{name: "primary_domain_missing", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
|
||||
runtime.Root.Domains.Accepted = []string{"example.com"}
|
||||
}, want: ErrInvalidDomainConfig},
|
||||
{name: "invalid_admin_recovery_domain", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
|
||||
runtime.Root.Domains.AdminRecovery = "https://recovery.example.com/reset"
|
||||
}, want: ErrInvalidDomainConfig},
|
||||
{name: "generation_mismatch", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) { runtime.Email.Meta.Generation++ }, want: ErrSectionGenerationMismatch},
|
||||
{name: "missing_super_admin", mutate: func(_ *RuntimeConfig, options *RuntimeValidationOptions) { options.HasSuperAdmin = false }, want: ErrInvalidRuntimeConfig},
|
||||
{name: "network_without_capability", mutate: func(runtime *RuntimeConfig, _ *RuntimeValidationOptions) {
|
||||
|
||||
@ -35,10 +35,11 @@ type SectionMeta struct {
|
||||
}
|
||||
|
||||
type DomainConfig struct {
|
||||
Primary string `json:"primary,omitempty"`
|
||||
Accepted []string `json:"accepted,omitempty"`
|
||||
Web string `json:"web,omitempty"`
|
||||
AccessHosts []string `json:"accessHosts,omitempty"`
|
||||
Primary string `json:"primary,omitempty"`
|
||||
Accepted []string `json:"accepted,omitempty"`
|
||||
Web string `json:"web,omitempty"`
|
||||
AdminRecovery string `json:"adminRecovery,omitempty"`
|
||||
AccessHosts []string `json:"accessHosts,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
|
||||
@ -192,6 +192,9 @@ func validationCertificateDomains(candidate *RuntimeConfig) []string {
|
||||
if candidate.Root.Domains.Web != "" {
|
||||
domains = append(domains, candidate.Root.Domains.Web)
|
||||
}
|
||||
if candidate.Root.Domains.AdminRecovery != "" {
|
||||
domains = append(domains, candidate.Root.Domains.AdminRecovery)
|
||||
}
|
||||
for _, domain := range candidate.Root.Domains.Accepted {
|
||||
domains = append(domains, "smtp."+domain, "imap."+domain, "pop."+domain)
|
||||
}
|
||||
@ -201,11 +204,11 @@ func validationCertificateDomains(candidate *RuntimeConfig) []string {
|
||||
}
|
||||
|
||||
func runtimeCertificateDomains(candidate *RuntimeConfig) []string {
|
||||
return SSLCertificateDomains(candidate.Root.Domains.Primary, candidate.Root.Domains.Web, candidate.Root.Domains.Accepted)
|
||||
return SSLCertificateDomains(candidate.Root.Domains.Primary, candidate.Root.Domains.Web, candidate.Root.Domains.Accepted, candidate.Root.Domains.AdminRecovery)
|
||||
}
|
||||
|
||||
// SSLCertificateDomains 统一初始化和配置管理所需的 Web、主域及邮件协议域名。
|
||||
func SSLCertificateDomains(primary, web string, accepted []string) []string {
|
||||
// SSLCertificateDomains 统一初始化和配置管理所需的 Web、主域、邮件协议及恢复通道域名。
|
||||
func SSLCertificateDomains(primary, web string, accepted []string, adminRecovery ...string) []string {
|
||||
seen := make(map[string]bool)
|
||||
var domains []string
|
||||
appendDomain := func(value string) {
|
||||
@ -217,6 +220,9 @@ func SSLCertificateDomains(primary, web string, accepted []string) []string {
|
||||
}
|
||||
appendDomain(web)
|
||||
appendDomain(primary)
|
||||
for _, domain := range adminRecovery {
|
||||
appendDomain(domain)
|
||||
}
|
||||
for _, domain := range accepted {
|
||||
domain = strings.TrimSpace(domain)
|
||||
if domain == "" {
|
||||
|
||||
@ -37,6 +37,7 @@ func CurrentCertificateConfig() *Config {
|
||||
cfg.SSL = runtime.SSL.TLS
|
||||
cfg.Domain, cfg.WebDomain = runtime.Root.Domains.Primary, runtime.Root.Domains.Web
|
||||
cfg.Domains = runtime.Root.Domains.Accepted
|
||||
cfg.AdminRecoveryDomain = runtime.Root.Domains.AdminRecovery
|
||||
cfg.TLSMode = runtime.Root.HTTP.TLSMode
|
||||
cfg.HttpsEnabled = 2
|
||||
if runtime.Root.HTTP.HTTPSEnabled {
|
||||
@ -98,7 +99,12 @@ func loadValidatedRuntimeCertificate(cfg *Config) (tls.Certificate, error) {
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
runtime := RuntimeConfig{Root: RootConfig{Domains: DomainConfig{Primary: cfg.Domain, Web: cfg.WebDomain, Accepted: cfg.Domains}}}
|
||||
runtime := RuntimeConfig{Root: RootConfig{Domains: DomainConfig{
|
||||
Primary: cfg.Domain,
|
||||
Web: cfg.WebDomain,
|
||||
Accepted: cfg.Domains,
|
||||
AdminRecovery: cfg.AdminRecoveryDomain,
|
||||
}}}
|
||||
runtime.SSL.TLS = cfg.SSL
|
||||
if cfg.SSLType == SSLTypeAutoDNS {
|
||||
runtime.SSL.TLS.Management = SSLManagementProject
|
||||
|
||||
@ -191,6 +191,22 @@ func TestValidateSSLMaterialRejectsDomainExpiryAndWeakKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSLCertificateDomainsIncludesConfiguredAdminRecoveryDomain(t *testing.T) {
|
||||
domains := SSLCertificateDomains("example.com", "mail.example.com", []string{"example.com"}, "recovery.example.com")
|
||||
for _, want := range []string{"example.com", "mail.example.com", "recovery.example.com", "smtp.example.com", "imap.example.com", "pop.example.com"} {
|
||||
found := false
|
||||
for _, got := range domains {
|
||||
if got == want {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("certificate domains %v do not include %q", domains, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareCustomSSLPathsKeepsDirectFilesWithoutCopying(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
previousRoot := ROOT_PATH
|
||||
|
||||
@ -306,7 +306,7 @@ func sslDomains(cfg *config.Config) []string {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return config.SSLCertificateDomains(cfg.Domain, cfg.WebDomain, cfg.Domains)
|
||||
return config.SSLCertificateDomains(cfg.Domain, cfg.WebDomain, cfg.Domains, cfg.AdminRecoveryDomain)
|
||||
}
|
||||
|
||||
func certificateMatchesConfiguredDomains(cert *x509.Certificate, cfg *config.Config) bool {
|
||||
@ -343,6 +343,9 @@ func validateWildcardConfiguration(cfg *config.Config) error {
|
||||
if cfg.WebDomain != "" && !covered(cfg.WebDomain) {
|
||||
return newUserCertificateError("Web 域名必须是主域名的一级子域名,泛域名证书不覆盖根域名或多级子域名")
|
||||
}
|
||||
if cfg.AdminRecoveryDomain != "" && !covered(cfg.AdminRecoveryDomain) {
|
||||
return newUserCertificateError("超级管理员恢复域名必须是主邮件域名的一级子域名,才能由 DNS 泛域名证书覆盖")
|
||||
}
|
||||
for _, domain := range cfg.Domains {
|
||||
if !covered("smtp."+domain) || !covered("imap."+domain) || !covered("pop."+domain) {
|
||||
return newUserCertificateError("附加邮箱域名的服务地址不在主域泛域名证书覆盖范围内")
|
||||
|
||||
@ -52,17 +52,18 @@ func TestWildcardCertificateDomains(t *testing.T) {
|
||||
|
||||
func TestCertificateDomainsFollowChallengeMode(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Domain: "xuans.com",
|
||||
WebDomain: "web.xuans.com",
|
||||
Domains: []string{"xuans.com"},
|
||||
Domain: "xuans.com",
|
||||
WebDomain: "web.xuans.com",
|
||||
AdminRecoveryDomain: "recovery.xuans.com",
|
||||
Domains: []string{"xuans.com"},
|
||||
}
|
||||
|
||||
cfg.SSLType = config.SSLTypeAutoHTTP
|
||||
httpDomains := certificateDomains(cfg)
|
||||
if len(httpDomains) != 5 {
|
||||
t.Fatalf("got %d HTTP domains, want 5: %v", len(httpDomains), httpDomains)
|
||||
if len(httpDomains) != 6 {
|
||||
t.Fatalf("got %d HTTP domains, want 6: %v", len(httpDomains), httpDomains)
|
||||
}
|
||||
if httpDomains[0] != "web.xuans.com" || httpDomains[1] != "xuans.com" {
|
||||
if httpDomains[0] != "web.xuans.com" || httpDomains[1] != "xuans.com" || httpDomains[2] != "recovery.xuans.com" {
|
||||
t.Fatalf("unexpected HTTP domains: %v", httpDomains)
|
||||
}
|
||||
|
||||
@ -73,6 +74,26 @@ func TestCertificateDomainsFollowChallengeMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSWildcardRejectsUncoveredAdminRecoveryDomain(t *testing.T) {
|
||||
base := &config.Config{
|
||||
Domain: "example.com",
|
||||
WebDomain: "mail.example.com",
|
||||
Domains: []string{"example.com"},
|
||||
SSLType: config.SSLTypeAutoDNS,
|
||||
AdminRecoveryDomain: "recovery.example.com",
|
||||
}
|
||||
if err := validateWildcardConfiguration(base); err != nil {
|
||||
t.Fatalf("covered recovery domain was rejected: %v", err)
|
||||
}
|
||||
for _, value := range []string{"example.com", "a.recovery.example.com", "recovery.other.example"} {
|
||||
candidate := *base
|
||||
candidate.AdminRecoveryDomain = value
|
||||
if err := validateWildcardConfiguration(&candidate); err == nil {
|
||||
t.Fatalf("uncovered recovery domain %q was accepted", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSChallengeWildcardProducesSingleRecord(t *testing.T) {
|
||||
challenge := &DNSChallenge{authInfo: map[string]*authInfo{}}
|
||||
if err := challenge.Present("*.xuans.com", "token", "key-auth"); err != nil {
|
||||
|
||||
@ -88,6 +88,7 @@ const baseGroups = [
|
||||
group('site', '站点与域名', '', [
|
||||
field('domains.primary', '主邮件域名', 'text', { required: true, placeholder: 'example.com' }),
|
||||
field('domains.web', '网页访问域名', 'text', { required: true, placeholder: 'mail.example.com' }),
|
||||
field('domains.adminRecovery', '超级管理员恢复域名', 'text', { placeholder: 'recovery.example.com', hint: '留空时使用主邮件域名。该配置仅用于本机恢复通道的部署标识与证书规划,不开放公网重置页面。' }),
|
||||
field('domains.accepted', '接收邮件的域名', 'list', { required: true, wide: false, hint: '需包含主邮件域名,每个域名单独填写。', placeholder: 'example.com' }),
|
||||
field('logLevel', '日志级别', 'select', { options: options([['', '默认级别'], ['debug', '调试'], ['info', '信息'], ['warn', '警告'], ['error', '错误']]) })
|
||||
]),
|
||||
@ -326,6 +327,9 @@ export function validateFormConfig(section, config) {
|
||||
for (const name of ['primary', 'web']) {
|
||||
if (!validDomain(domains[name])) errors[`domains.${name}`] = '请填写有效域名,不包含协议、路径或空格'
|
||||
}
|
||||
if (domains.adminRecovery !== undefined && domains.adminRecovery !== '' && !validDomain(domains.adminRecovery)) {
|
||||
errors['domains.adminRecovery'] = '请填写有效域名,不包含协议、路径或空格'
|
||||
}
|
||||
if (Array.isArray(domains.accepted) && domains.accepted.every(validDomain)) {
|
||||
const accepted = domains.accepted.map(normalizedDomain)
|
||||
if (new Set(accepted).size !== accepted.length) errors['domains.accepted'] = '接收域名不能重复,大小写和末尾点视为同一域名'
|
||||
|
||||
@ -7,7 +7,10 @@ import { createRequire } from 'node:module'
|
||||
const root = new URL('../../frontend/src/views/components/ConfigSettings/', import.meta.url)
|
||||
const source = readFileSync(new URL('ConfigForm/ConfigForm.js', root), 'utf8')
|
||||
const { readFormConfig, getFieldGroups, updateFormConfig, validateFormConfig } = compileFunction(
|
||||
source.replaceAll('export function ', 'function ') + '\nreturn { readFormConfig, getFieldGroups, updateFormConfig, validateFormConfig }'
|
||||
source
|
||||
.replaceAll('export function ', 'function ')
|
||||
.replaceAll('export const ', 'const ') +
|
||||
'\nreturn { readFormConfig, getFieldGroups, updateFormConfig, validateFormConfig }'
|
||||
)()
|
||||
|
||||
test('基础配置不再展示冗余安全选项与开关,通行密钥由系统默认解析,用户权限仍由账号设置管理', () => {
|
||||
@ -274,12 +277,26 @@ test('开关、数字、空数字和列表保持真实类型,代理 HTTPS 正
|
||||
test('域名校验兼容大小写和末尾点,拒绝重复项、空白和路径', () => {
|
||||
const valid = { database: { type: 'sqlite' }, domains: { primary: 'Example.com.', web: 'mail.example.com', accepted: ['example.com'] } }
|
||||
assert.deepEqual(validateFormConfig('base', valid), {})
|
||||
const fields = getFieldGroups('base', valid).flatMap(group => group.fields)
|
||||
const recovery = fields.find(field => field.path.join('.') === 'domains.adminRecovery')
|
||||
assert.ok(recovery)
|
||||
assert.match(recovery.hint, /不开放公网重置页面/)
|
||||
assert.deepEqual(validateFormConfig('base', { ...valid, domains: { ...valid.domains, adminRecovery: '' } }), {})
|
||||
assert.deepEqual(validateFormConfig('base', { ...valid, domains: { ...valid.domains, adminRecovery: 'recovery.example.com' } }), {})
|
||||
for (const accepted of [[], ['example.com', 'Example.com.'], [''], [' https://example.com'], ['other.example.com'], 'example.com', [null]]) {
|
||||
assert.ok(validateFormConfig('base', { ...valid, domains: { ...valid.domains, accepted } })['domains.accepted'])
|
||||
}
|
||||
for (const primary of ['https://example.com', 'example.com/path', 'bad..com', '-bad.com', '127.0.0.1', '例子.com']) {
|
||||
assert.ok(validateFormConfig('base', { ...valid, domains: { ...valid.domains, primary } })['domains.primary'])
|
||||
}
|
||||
for (const adminRecovery of ['https://example.com', 'example.com/path', 'bad..com', '-bad.com', '127.0.0.1', '例子.com', ' recovery.example.com']) {
|
||||
assert.ok(validateFormConfig('base', { ...valid, domains: { ...valid.domains, adminRecovery } })['domains.adminRecovery'])
|
||||
}
|
||||
const text = JSON.stringify({ ...valid, domains: { ...valid.domains, accepted: ['example.com'], accessHosts: ['192.0.2.10'], adminRecovery: '' } })
|
||||
const updated = readFormConfig(updateFormConfig(text, 'base', ['domains', 'adminRecovery'], 'recovery.example.com'))
|
||||
assert.equal(updated.domains.adminRecovery, 'recovery.example.com')
|
||||
assert.deepEqual(updated.domains.accepted, ['example.com'])
|
||||
assert.deepEqual(updated.domains.accessHosts, ['192.0.2.10'])
|
||||
})
|
||||
|
||||
test('插件网络列表拒绝无效和重复目标,空设置可编辑且非标准设置不覆盖', () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user