Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
bd0801a8
"frontend/src/vscode:/vscode.git/clone" did not exist on "0b1ce6be8f1e53cab1927fccff74aa7f5af3c4fd"
Commit
bd0801a8
authored
Mar 02, 2026
by
PMExtra
Browse files
feat(registration): add email domain whitelist policy
parent
ba6de4c4
Changes
25
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/setting_handler.go
View file @
bd0801a8
...
...
@@ -77,6 +77,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
response
.
Success
(
c
,
dto
.
SystemSettings
{
RegistrationEnabled
:
settings
.
RegistrationEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
settings
.
RegistrationEmailSuffixWhitelist
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
...
...
@@ -130,12 +131,13 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
// UpdateSettingsRequest 更新设置请求
type
UpdateSettingsRequest
struct
{
// 注册设置
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
RegistrationEmailSuffixWhitelist
[]
string
`json:"registration_email_suffix_whitelist"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
// 邮件服务设置
SMTPHost
string
`json:"smtp_host"`
...
...
@@ -426,50 +428,51 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
settings
:=
&
service
.
SystemSettings
{
RegistrationEnabled
:
req
.
RegistrationEnabled
,
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
req
.
PromoCodeEnabled
,
PasswordResetEnabled
:
req
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
req
.
InvitationCodeEnabled
,
TotpEnabled
:
req
.
TotpEnabled
,
SMTPHost
:
req
.
SMTPHost
,
SMTPPort
:
req
.
SMTPPort
,
SMTPUsername
:
req
.
SMTPUsername
,
SMTPPassword
:
req
.
SMTPPassword
,
SMTPFrom
:
req
.
SMTPFrom
,
SMTPFromName
:
req
.
SMTPFromName
,
SMTPUseTLS
:
req
.
SMTPUseTLS
,
TurnstileEnabled
:
req
.
TurnstileEnabled
,
TurnstileSiteKey
:
req
.
TurnstileSiteKey
,
TurnstileSecretKey
:
req
.
TurnstileSecretKey
,
LinuxDoConnectEnabled
:
req
.
LinuxDoConnectEnabled
,
LinuxDoConnectClientID
:
req
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecret
:
req
.
LinuxDoConnectClientSecret
,
LinuxDoConnectRedirectURL
:
req
.
LinuxDoConnectRedirectURL
,
SiteName
:
req
.
SiteName
,
SiteLogo
:
req
.
SiteLogo
,
SiteSubtitle
:
req
.
SiteSubtitle
,
APIBaseURL
:
req
.
APIBaseURL
,
ContactInfo
:
req
.
ContactInfo
,
DocURL
:
req
.
DocURL
,
HomeContent
:
req
.
HomeContent
,
HideCcsImportButton
:
req
.
HideCcsImportButton
,
PurchaseSubscriptionEnabled
:
purchaseEnabled
,
PurchaseSubscriptionURL
:
purchaseURL
,
SoraClientEnabled
:
req
.
SoraClientEnabled
,
CustomMenuItems
:
customMenuJSON
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultBalance
:
req
.
DefaultBalance
,
DefaultSubscriptions
:
defaultSubscriptions
,
EnableModelFallback
:
req
.
EnableModelFallback
,
FallbackModelAnthropic
:
req
.
FallbackModelAnthropic
,
FallbackModelOpenAI
:
req
.
FallbackModelOpenAI
,
FallbackModelGemini
:
req
.
FallbackModelGemini
,
FallbackModelAntigravity
:
req
.
FallbackModelAntigravity
,
EnableIdentityPatch
:
req
.
EnableIdentityPatch
,
IdentityPatchPrompt
:
req
.
IdentityPatchPrompt
,
MinClaudeCodeVersion
:
req
.
MinClaudeCodeVersion
,
AllowUngroupedKeyScheduling
:
req
.
AllowUngroupedKeyScheduling
,
RegistrationEnabled
:
req
.
RegistrationEnabled
,
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
req
.
RegistrationEmailSuffixWhitelist
,
PromoCodeEnabled
:
req
.
PromoCodeEnabled
,
PasswordResetEnabled
:
req
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
req
.
InvitationCodeEnabled
,
TotpEnabled
:
req
.
TotpEnabled
,
SMTPHost
:
req
.
SMTPHost
,
SMTPPort
:
req
.
SMTPPort
,
SMTPUsername
:
req
.
SMTPUsername
,
SMTPPassword
:
req
.
SMTPPassword
,
SMTPFrom
:
req
.
SMTPFrom
,
SMTPFromName
:
req
.
SMTPFromName
,
SMTPUseTLS
:
req
.
SMTPUseTLS
,
TurnstileEnabled
:
req
.
TurnstileEnabled
,
TurnstileSiteKey
:
req
.
TurnstileSiteKey
,
TurnstileSecretKey
:
req
.
TurnstileSecretKey
,
LinuxDoConnectEnabled
:
req
.
LinuxDoConnectEnabled
,
LinuxDoConnectClientID
:
req
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecret
:
req
.
LinuxDoConnectClientSecret
,
LinuxDoConnectRedirectURL
:
req
.
LinuxDoConnectRedirectURL
,
SiteName
:
req
.
SiteName
,
SiteLogo
:
req
.
SiteLogo
,
SiteSubtitle
:
req
.
SiteSubtitle
,
APIBaseURL
:
req
.
APIBaseURL
,
ContactInfo
:
req
.
ContactInfo
,
DocURL
:
req
.
DocURL
,
HomeContent
:
req
.
HomeContent
,
HideCcsImportButton
:
req
.
HideCcsImportButton
,
PurchaseSubscriptionEnabled
:
purchaseEnabled
,
PurchaseSubscriptionURL
:
purchaseURL
,
SoraClientEnabled
:
req
.
SoraClientEnabled
,
CustomMenuItems
:
customMenuJSON
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultBalance
:
req
.
DefaultBalance
,
DefaultSubscriptions
:
defaultSubscriptions
,
EnableModelFallback
:
req
.
EnableModelFallback
,
FallbackModelAnthropic
:
req
.
FallbackModelAnthropic
,
FallbackModelOpenAI
:
req
.
FallbackModelOpenAI
,
FallbackModelGemini
:
req
.
FallbackModelGemini
,
FallbackModelAntigravity
:
req
.
FallbackModelAntigravity
,
EnableIdentityPatch
:
req
.
EnableIdentityPatch
,
IdentityPatchPrompt
:
req
.
IdentityPatchPrompt
,
MinClaudeCodeVersion
:
req
.
MinClaudeCodeVersion
,
AllowUngroupedKeyScheduling
:
req
.
AllowUngroupedKeyScheduling
,
OpsMonitoringEnabled
:
func
()
bool
{
if
req
.
OpsMonitoringEnabled
!=
nil
{
return
*
req
.
OpsMonitoringEnabled
...
...
@@ -520,6 +523,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
response
.
Success
(
c
,
dto
.
SystemSettings
{
RegistrationEnabled
:
updatedSettings
.
RegistrationEnabled
,
EmailVerifyEnabled
:
updatedSettings
.
EmailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
updatedSettings
.
RegistrationEmailSuffixWhitelist
,
PromoCodeEnabled
:
updatedSettings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
updatedSettings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
updatedSettings
.
InvitationCodeEnabled
,
...
...
@@ -598,6 +602,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
EmailVerifyEnabled
!=
after
.
EmailVerifyEnabled
{
changed
=
append
(
changed
,
"email_verify_enabled"
)
}
if
!
equalStringSlice
(
before
.
RegistrationEmailSuffixWhitelist
,
after
.
RegistrationEmailSuffixWhitelist
)
{
changed
=
append
(
changed
,
"registration_email_suffix_whitelist"
)
}
if
before
.
PasswordResetEnabled
!=
after
.
PasswordResetEnabled
{
changed
=
append
(
changed
,
"password_reset_enabled"
)
}
...
...
@@ -747,6 +754,18 @@ func normalizeDefaultSubscriptions(input []dto.DefaultSubscriptionSetting) []dto
return
normalized
}
func
equalStringSlice
(
a
,
b
[]
string
)
bool
{
if
len
(
a
)
!=
len
(
b
)
{
return
false
}
for
i
:=
range
a
{
if
a
[
i
]
!=
b
[
i
]
{
return
false
}
}
return
true
}
func
equalDefaultSubscriptions
(
a
,
b
[]
service
.
DefaultSubscriptionSetting
)
bool
{
if
len
(
a
)
!=
len
(
b
)
{
return
false
...
...
backend/internal/handler/dto/settings.go
View file @
bd0801a8
...
...
@@ -17,13 +17,14 @@ type CustomMenuItem struct {
// SystemSettings represents the admin settings API response payload.
type
SystemSettings
struct
{
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TotpEncryptionKeyConfigured
bool
`json:"totp_encryption_key_configured"`
// TOTP 加密密钥是否已配置
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
RegistrationEmailSuffixWhitelist
[]
string
`json:"registration_email_suffix_whitelist"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TotpEncryptionKeyConfigured
bool
`json:"totp_encryption_key_configured"`
// TOTP 加密密钥是否已配置
SMTPHost
string
`json:"smtp_host"`
SMTPPort
int
`json:"smtp_port"`
...
...
@@ -88,28 +89,29 @@ type DefaultSubscriptionSetting struct {
}
type
PublicSettings
struct
{
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
APIBaseURL
string
`json:"api_base_url"`
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled
bool
`json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
Version
string
`json:"version"`
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
RegistrationEmailSuffixWhitelist
[]
string
`json:"registration_email_suffix_whitelist"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
APIBaseURL
string
`json:"api_base_url"`
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled
bool
`json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
Version
string
`json:"version"`
}
// SoraS3Settings Sora S3 存储配置 DTO(响应用,不含敏感字段)
...
...
backend/internal/handler/setting_handler.go
View file @
bd0801a8
...
...
@@ -32,27 +32,28 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
}
response
.
Success
(
c
,
dto
.
PublicSettings
{
RegistrationEnabled
:
settings
.
RegistrationEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
SiteName
:
settings
.
SiteName
,
SiteLogo
:
settings
.
SiteLogo
,
SiteSubtitle
:
settings
.
SiteSubtitle
,
APIBaseURL
:
settings
.
APIBaseURL
,
ContactInfo
:
settings
.
ContactInfo
,
DocURL
:
settings
.
DocURL
,
HomeContent
:
settings
.
HomeContent
,
HideCcsImportButton
:
settings
.
HideCcsImportButton
,
PurchaseSubscriptionEnabled
:
settings
.
PurchaseSubscriptionEnabled
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
CustomMenuItems
:
dto
.
ParseUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
Version
:
h
.
version
,
RegistrationEnabled
:
settings
.
RegistrationEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
settings
.
RegistrationEmailSuffixWhitelist
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
SiteName
:
settings
.
SiteName
,
SiteLogo
:
settings
.
SiteLogo
,
SiteSubtitle
:
settings
.
SiteSubtitle
,
APIBaseURL
:
settings
.
APIBaseURL
,
ContactInfo
:
settings
.
ContactInfo
,
DocURL
:
settings
.
DocURL
,
HomeContent
:
settings
.
HomeContent
,
HideCcsImportButton
:
settings
.
HideCcsImportButton
,
PurchaseSubscriptionEnabled
:
settings
.
PurchaseSubscriptionEnabled
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
CustomMenuItems
:
dto
.
ParseUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
Version
:
h
.
version
,
})
}
backend/internal/server/api_contract_test.go
View file @
bd0801a8
...
...
@@ -446,9 +446,10 @@ func TestAPIContracts(t *testing.T) {
setup
:
func
(
t
*
testing
.
T
,
deps
*
contractDeps
)
{
t
.
Helper
()
deps
.
settingRepo
.
SetAll
(
map
[
string
]
string
{
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyEmailVerifyEnabled
:
"false"
,
service
.
SettingKeyPromoCodeEnabled
:
"true"
,
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyEmailVerifyEnabled
:
"false"
,
service
.
SettingKeyRegistrationEmailSuffixWhitelist
:
"[]"
,
service
.
SettingKeyPromoCodeEnabled
:
"true"
,
service
.
SettingKeySMTPHost
:
"smtp.example.com"
,
service
.
SettingKeySMTPPort
:
"587"
,
...
...
@@ -487,6 +488,7 @@ func TestAPIContracts(t *testing.T) {
"data": {
"registration_enabled": true,
"email_verify_enabled": false,
"registration_email_suffix_whitelist": [],
"promo_code_enabled": true,
"password_reset_enabled": false,
"totp_enabled": false,
...
...
backend/internal/service/auth_service.go
View file @
bd0801a8
...
...
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/mail"
"strconv"
"strings"
"time"
...
...
@@ -33,6 +34,7 @@ var (
ErrRefreshTokenExpired
=
infraerrors
.
Unauthorized
(
"REFRESH_TOKEN_EXPIRED"
,
"refresh token has expired"
)
ErrRefreshTokenReused
=
infraerrors
.
Unauthorized
(
"REFRESH_TOKEN_REUSED"
,
"refresh token has been reused"
)
ErrEmailVerifyRequired
=
infraerrors
.
BadRequest
(
"EMAIL_VERIFY_REQUIRED"
,
"email verification is required"
)
ErrEmailSuffixNotAllowed
=
infraerrors
.
BadRequest
(
"EMAIL_SUFFIX_NOT_ALLOWED"
,
"email suffix is not allowed"
)
ErrRegDisabled
=
infraerrors
.
Forbidden
(
"REGISTRATION_DISABLED"
,
"registration is currently disabled"
)
ErrServiceUnavailable
=
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
)
ErrInvitationCodeRequired
=
infraerrors
.
BadRequest
(
"INVITATION_CODE_REQUIRED"
,
"invitation code is required"
)
...
...
@@ -115,6 +117,9 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
if
isReservedEmail
(
email
)
{
return
""
,
nil
,
ErrEmailReserved
}
if
err
:=
s
.
validateRegistrationEmailPolicy
(
ctx
,
email
);
err
!=
nil
{
return
""
,
nil
,
err
}
// 检查是否需要邀请码
var
invitationRedeemCode
*
RedeemCode
...
...
@@ -241,6 +246,9 @@ func (s *AuthService) SendVerifyCode(ctx context.Context, email string) error {
if
isReservedEmail
(
email
)
{
return
ErrEmailReserved
}
if
err
:=
s
.
validateRegistrationEmailPolicy
(
ctx
,
email
);
err
!=
nil
{
return
err
}
// 检查邮箱是否已存在
existsEmail
,
err
:=
s
.
userRepo
.
ExistsByEmail
(
ctx
,
email
)
...
...
@@ -279,6 +287,9 @@ func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string) (*S
if
isReservedEmail
(
email
)
{
return
nil
,
ErrEmailReserved
}
if
err
:=
s
.
validateRegistrationEmailPolicy
(
ctx
,
email
);
err
!=
nil
{
return
nil
,
err
}
// 检查邮箱是否已存在
existsEmail
,
err
:=
s
.
userRepo
.
ExistsByEmail
(
ctx
,
email
)
...
...
@@ -624,6 +635,32 @@ func (s *AuthService) assignDefaultSubscriptions(ctx context.Context, userID int
}
}
func
(
s
*
AuthService
)
validateRegistrationEmailPolicy
(
ctx
context
.
Context
,
email
string
)
error
{
if
s
.
settingService
==
nil
{
return
nil
}
whitelist
:=
s
.
settingService
.
GetRegistrationEmailSuffixWhitelist
(
ctx
)
if
!
IsRegistrationEmailSuffixAllowed
(
email
,
whitelist
)
{
return
buildEmailSuffixNotAllowedError
(
whitelist
)
}
return
nil
}
func
buildEmailSuffixNotAllowedError
(
whitelist
[]
string
)
error
{
if
len
(
whitelist
)
==
0
{
return
ErrEmailSuffixNotAllowed
}
allowed
:=
strings
.
Join
(
whitelist
,
", "
)
return
infraerrors
.
BadRequest
(
"EMAIL_SUFFIX_NOT_ALLOWED"
,
fmt
.
Sprintf
(
"email suffix is not allowed, allowed suffixes: %s"
,
allowed
),
)
.
WithMetadata
(
map
[
string
]
string
{
"allowed_suffixes"
:
strings
.
Join
(
whitelist
,
","
),
"allowed_suffix_count"
:
strconv
.
Itoa
(
len
(
whitelist
)),
})
}
// ValidateToken 验证JWT token并返回用户声明
func
(
s
*
AuthService
)
ValidateToken
(
tokenString
string
)
(
*
JWTClaims
,
error
)
{
// 先做长度校验,尽早拒绝异常超长 token,降低 DoS 风险。
...
...
backend/internal/service/auth_service_register_test.go
View file @
bd0801a8
...
...
@@ -9,6 +9,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/stretchr/testify/require"
)
...
...
@@ -231,6 +232,51 @@ func TestAuthService_Register_ReservedEmail(t *testing.T) {
require
.
ErrorIs
(
t
,
err
,
ErrEmailReserved
)
}
func
TestAuthService_Register_EmailSuffixNotAllowed
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyRegistrationEmailSuffixWhitelist
:
`["@example.com","@company.com"]`
,
},
nil
)
_
,
_
,
err
:=
service
.
Register
(
context
.
Background
(),
"user@other.com"
,
"password"
)
require
.
ErrorIs
(
t
,
err
,
ErrEmailSuffixNotAllowed
)
appErr
:=
infraerrors
.
FromError
(
err
)
require
.
Contains
(
t
,
appErr
.
Message
,
"@example.com"
)
require
.
Contains
(
t
,
appErr
.
Message
,
"@company.com"
)
require
.
Equal
(
t
,
"EMAIL_SUFFIX_NOT_ALLOWED"
,
appErr
.
Reason
)
require
.
Equal
(
t
,
"2"
,
appErr
.
Metadata
[
"allowed_suffix_count"
])
require
.
Equal
(
t
,
"@example.com,@company.com"
,
appErr
.
Metadata
[
"allowed_suffixes"
])
}
func
TestAuthService_Register_EmailSuffixAllowed
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{
nextID
:
8
}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyRegistrationEmailSuffixWhitelist
:
`["example.com"]`
,
},
nil
)
_
,
user
,
err
:=
service
.
Register
(
context
.
Background
(),
"user@example.com"
,
"password"
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
user
)
require
.
Equal
(
t
,
int64
(
8
),
user
.
ID
)
}
func
TestAuthService_SendVerifyCode_EmailSuffixNotAllowed
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyRegistrationEmailSuffixWhitelist
:
`["@example.com","@company.com"]`
,
},
nil
)
err
:=
service
.
SendVerifyCode
(
context
.
Background
(),
"user@other.com"
)
require
.
ErrorIs
(
t
,
err
,
ErrEmailSuffixNotAllowed
)
appErr
:=
infraerrors
.
FromError
(
err
)
require
.
Contains
(
t
,
appErr
.
Message
,
"@example.com"
)
require
.
Contains
(
t
,
appErr
.
Message
,
"@company.com"
)
require
.
Equal
(
t
,
"2"
,
appErr
.
Metadata
[
"allowed_suffix_count"
])
}
func
TestAuthService_Register_CreateError
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{
createErr
:
errors
.
New
(
"create failed"
)}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
...
...
@@ -402,7 +448,7 @@ func TestAuthService_Register_AssignsDefaultSubscriptions(t *testing.T) {
repo
:=
&
userRepoStub
{
nextID
:
42
}
assigner
:=
&
defaultSubscriptionAssignerStub
{}
service
:=
newAuthService
(
repo
,
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyDefaultSubscriptions
:
`[{"group_id":11,"validity_days":30},{"group_id":12,"validity_days":7}]`
,
},
nil
)
service
.
defaultSubAssigner
=
assigner
...
...
backend/internal/service/domain_constants.go
View file @
bd0801a8
...
...
@@ -74,11 +74,12 @@ const LinuxDoConnectSyntheticEmailDomain = "@linuxdo-connect.invalid"
// Setting keys
const
(
// 注册设置
SettingKeyRegistrationEnabled
=
"registration_enabled"
// 是否开放注册
SettingKeyEmailVerifyEnabled
=
"email_verify_enabled"
// 是否开启邮件验证
SettingKeyPromoCodeEnabled
=
"promo_code_enabled"
// 是否启用优惠码功能
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyInvitationCodeEnabled
=
"invitation_code_enabled"
// 是否启用邀请码注册
SettingKeyRegistrationEnabled
=
"registration_enabled"
// 是否开放注册
SettingKeyEmailVerifyEnabled
=
"email_verify_enabled"
// 是否开启邮件验证
SettingKeyRegistrationEmailSuffixWhitelist
=
"registration_email_suffix_whitelist"
// 注册邮箱后缀白名单(JSON 数组)
SettingKeyPromoCodeEnabled
=
"promo_code_enabled"
// 是否启用优惠码功能
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyInvitationCodeEnabled
=
"invitation_code_enabled"
// 是否启用邀请码注册
// 邮件服务设置
SettingKeySMTPHost
=
"smtp_host"
// SMTP服务器地址
...
...
backend/internal/service/registration_email_policy.go
0 → 100644
View file @
bd0801a8
package
service
import
(
"encoding/json"
"fmt"
"regexp"
"strings"
)
var
registrationEmailDomainPattern
=
regexp
.
MustCompile
(
`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$`
,
)
// RegistrationEmailSuffix extracts normalized suffix in "@domain" form.
func
RegistrationEmailSuffix
(
email
string
)
string
{
_
,
domain
,
ok
:=
splitEmailForPolicy
(
email
)
if
!
ok
{
return
""
}
return
"@"
+
domain
}
// IsRegistrationEmailSuffixAllowed checks whether an email is allowed by suffix whitelist.
// Empty whitelist means allow all.
func
IsRegistrationEmailSuffixAllowed
(
email
string
,
whitelist
[]
string
)
bool
{
if
len
(
whitelist
)
==
0
{
return
true
}
suffix
:=
RegistrationEmailSuffix
(
email
)
if
suffix
==
""
{
return
false
}
for
_
,
allowed
:=
range
whitelist
{
if
suffix
==
allowed
{
return
true
}
}
return
false
}
// NormalizeRegistrationEmailSuffixWhitelist normalizes and validates suffix whitelist items.
func
NormalizeRegistrationEmailSuffixWhitelist
(
raw
[]
string
)
([]
string
,
error
)
{
return
normalizeRegistrationEmailSuffixWhitelist
(
raw
,
true
)
}
// ParseRegistrationEmailSuffixWhitelist parses persisted JSON into normalized suffixes.
// Invalid entries are ignored to keep old misconfigurations from breaking runtime reads.
func
ParseRegistrationEmailSuffixWhitelist
(
raw
string
)
[]
string
{
raw
=
strings
.
TrimSpace
(
raw
)
if
raw
==
""
{
return
[]
string
{}
}
var
items
[]
string
if
err
:=
json
.
Unmarshal
([]
byte
(
raw
),
&
items
);
err
!=
nil
{
return
[]
string
{}
}
normalized
,
_
:=
normalizeRegistrationEmailSuffixWhitelist
(
items
,
false
)
if
len
(
normalized
)
==
0
{
return
[]
string
{}
}
return
normalized
}
func
normalizeRegistrationEmailSuffixWhitelist
(
raw
[]
string
,
strict
bool
)
([]
string
,
error
)
{
if
len
(
raw
)
==
0
{
return
nil
,
nil
}
seen
:=
make
(
map
[
string
]
struct
{},
len
(
raw
))
out
:=
make
([]
string
,
0
,
len
(
raw
))
for
_
,
item
:=
range
raw
{
normalized
,
err
:=
normalizeRegistrationEmailSuffix
(
item
)
if
err
!=
nil
{
if
strict
{
return
nil
,
err
}
continue
}
if
normalized
==
""
{
continue
}
if
_
,
ok
:=
seen
[
normalized
];
ok
{
continue
}
seen
[
normalized
]
=
struct
{}{}
out
=
append
(
out
,
normalized
)
}
if
len
(
out
)
==
0
{
return
nil
,
nil
}
return
out
,
nil
}
func
normalizeRegistrationEmailSuffix
(
raw
string
)
(
string
,
error
)
{
value
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
raw
))
if
value
==
""
{
return
""
,
nil
}
domain
:=
value
if
strings
.
Contains
(
value
,
"@"
)
{
if
!
strings
.
HasPrefix
(
value
,
"@"
)
||
strings
.
Count
(
value
,
"@"
)
!=
1
{
return
""
,
fmt
.
Errorf
(
"invalid email suffix: %q"
,
raw
)
}
domain
=
strings
.
TrimPrefix
(
value
,
"@"
)
}
if
domain
==
""
||
strings
.
Contains
(
domain
,
"@"
)
||
!
registrationEmailDomainPattern
.
MatchString
(
domain
)
{
return
""
,
fmt
.
Errorf
(
"invalid email suffix: %q"
,
raw
)
}
return
"@"
+
domain
,
nil
}
func
splitEmailForPolicy
(
raw
string
)
(
local
string
,
domain
string
,
ok
bool
)
{
email
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
raw
))
local
,
domain
,
found
:=
strings
.
Cut
(
email
,
"@"
)
if
!
found
||
local
==
""
||
domain
==
""
||
strings
.
Contains
(
domain
,
"@"
)
{
return
""
,
""
,
false
}
return
local
,
domain
,
true
}
backend/internal/service/registration_email_policy_test.go
0 → 100644
View file @
bd0801a8
//go:build unit
package
service
import
(
"testing"
"github.com/stretchr/testify/require"
)
func
TestNormalizeRegistrationEmailSuffixWhitelist
(
t
*
testing
.
T
)
{
got
,
err
:=
NormalizeRegistrationEmailSuffixWhitelist
([]
string
{
"example.com"
,
"@EXAMPLE.COM"
,
" @foo.bar "
})
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
string
{
"@example.com"
,
"@foo.bar"
},
got
)
}
func
TestNormalizeRegistrationEmailSuffixWhitelist_Invalid
(
t
*
testing
.
T
)
{
_
,
err
:=
NormalizeRegistrationEmailSuffixWhitelist
([]
string
{
"@invalid_domain"
})
require
.
Error
(
t
,
err
)
}
func
TestParseRegistrationEmailSuffixWhitelist
(
t
*
testing
.
T
)
{
got
:=
ParseRegistrationEmailSuffixWhitelist
(
`["example.com","@foo.bar","@invalid_domain"]`
)
require
.
Equal
(
t
,
[]
string
{
"@example.com"
,
"@foo.bar"
},
got
)
}
func
TestIsRegistrationEmailSuffixAllowed
(
t
*
testing
.
T
)
{
require
.
True
(
t
,
IsRegistrationEmailSuffixAllowed
(
"user@example.com"
,
[]
string
{
"@example.com"
}))
require
.
False
(
t
,
IsRegistrationEmailSuffixAllowed
(
"user@sub.example.com"
,
[]
string
{
"@example.com"
}))
require
.
True
(
t
,
IsRegistrationEmailSuffixAllowed
(
"user@any.com"
,
[]
string
{}))
}
backend/internal/service/setting_service.go
View file @
bd0801a8
...
...
@@ -108,6 +108,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
keys
:=
[]
string
{
SettingKeyRegistrationEnabled
,
SettingKeyEmailVerifyEnabled
,
SettingKeyRegistrationEmailSuffixWhitelist
,
SettingKeyPromoCodeEnabled
,
SettingKeyPasswordResetEnabled
,
SettingKeyInvitationCodeEnabled
,
...
...
@@ -144,29 +145,33 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
// Password reset requires email verification to be enabled
emailVerifyEnabled
:=
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
passwordResetEnabled
:=
emailVerifyEnabled
&&
settings
[
SettingKeyPasswordResetEnabled
]
==
"true"
registrationEmailSuffixWhitelist
:=
ParseRegistrationEmailSuffixWhitelist
(
settings
[
SettingKeyRegistrationEmailSuffixWhitelist
],
)
return
&
PublicSettings
{
RegistrationEnabled
:
settings
[
SettingKeyRegistrationEnabled
]
==
"true"
,
EmailVerifyEnabled
:
emailVerifyEnabled
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
passwordResetEnabled
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
SiteName
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteName
,
"Sub2API"
),
SiteLogo
:
settings
[
SettingKeySiteLogo
],
SiteSubtitle
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteSubtitle
,
"Subscription to API Conversion Platform"
),
APIBaseURL
:
settings
[
SettingKeyAPIBaseURL
],
ContactInfo
:
settings
[
SettingKeyContactInfo
],
DocURL
:
settings
[
SettingKeyDocURL
],
HomeContent
:
settings
[
SettingKeyHomeContent
],
HideCcsImportButton
:
settings
[
SettingKeyHideCcsImportButton
]
==
"true"
,
PurchaseSubscriptionEnabled
:
settings
[
SettingKeyPurchaseSubscriptionEnabled
]
==
"true"
,
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
RegistrationEnabled
:
settings
[
SettingKeyRegistrationEnabled
]
==
"true"
,
EmailVerifyEnabled
:
emailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
registrationEmailSuffixWhitelist
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
passwordResetEnabled
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
SiteName
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteName
,
"Sub2API"
),
SiteLogo
:
settings
[
SettingKeySiteLogo
],
SiteSubtitle
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteSubtitle
,
"Subscription to API Conversion Platform"
),
APIBaseURL
:
settings
[
SettingKeyAPIBaseURL
],
ContactInfo
:
settings
[
SettingKeyContactInfo
],
DocURL
:
settings
[
SettingKeyDocURL
],
HomeContent
:
settings
[
SettingKeyHomeContent
],
HideCcsImportButton
:
settings
[
SettingKeyHideCcsImportButton
]
==
"true"
,
PurchaseSubscriptionEnabled
:
settings
[
SettingKeyPurchaseSubscriptionEnabled
]
==
"true"
,
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
},
nil
}
...
...
@@ -196,51 +201,53 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
// Return a struct that matches the frontend's expected format
return
&
struct
{
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key,omitempty"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo,omitempty"`
SiteSubtitle
string
`json:"site_subtitle,omitempty"`
APIBaseURL
string
`json:"api_base_url,omitempty"`
ContactInfo
string
`json:"contact_info,omitempty"`
DocURL
string
`json:"doc_url,omitempty"`
HomeContent
string
`json:"home_content,omitempty"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled
bool
`json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url,omitempty"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
CustomMenuItems
json
.
RawMessage
`json:"custom_menu_items"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
Version
string
`json:"version,omitempty"`
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
RegistrationEmailSuffixWhitelist
[]
string
`json:"registration_email_suffix_whitelist"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key,omitempty"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo,omitempty"`
SiteSubtitle
string
`json:"site_subtitle,omitempty"`
APIBaseURL
string
`json:"api_base_url,omitempty"`
ContactInfo
string
`json:"contact_info,omitempty"`
DocURL
string
`json:"doc_url,omitempty"`
HomeContent
string
`json:"home_content,omitempty"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled
bool
`json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url,omitempty"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
CustomMenuItems
json
.
RawMessage
`json:"custom_menu_items"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
Version
string
`json:"version,omitempty"`
}{
RegistrationEnabled
:
settings
.
RegistrationEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
SiteName
:
settings
.
SiteName
,
SiteLogo
:
settings
.
SiteLogo
,
SiteSubtitle
:
settings
.
SiteSubtitle
,
APIBaseURL
:
settings
.
APIBaseURL
,
ContactInfo
:
settings
.
ContactInfo
,
DocURL
:
settings
.
DocURL
,
HomeContent
:
settings
.
HomeContent
,
HideCcsImportButton
:
settings
.
HideCcsImportButton
,
PurchaseSubscriptionEnabled
:
settings
.
PurchaseSubscriptionEnabled
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
CustomMenuItems
:
filterUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
Version
:
s
.
version
,
RegistrationEnabled
:
settings
.
RegistrationEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
settings
.
RegistrationEmailSuffixWhitelist
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
SiteName
:
settings
.
SiteName
,
SiteLogo
:
settings
.
SiteLogo
,
SiteSubtitle
:
settings
.
SiteSubtitle
,
APIBaseURL
:
settings
.
APIBaseURL
,
ContactInfo
:
settings
.
ContactInfo
,
DocURL
:
settings
.
DocURL
,
HomeContent
:
settings
.
HomeContent
,
HideCcsImportButton
:
settings
.
HideCcsImportButton
,
PurchaseSubscriptionEnabled
:
settings
.
PurchaseSubscriptionEnabled
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
CustomMenuItems
:
filterUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
Version
:
s
.
version
,
},
nil
}
...
...
@@ -356,12 +363,25 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
if
err
:=
s
.
validateDefaultSubscriptionGroups
(
ctx
,
settings
.
DefaultSubscriptions
);
err
!=
nil
{
return
err
}
normalizedWhitelist
,
err
:=
NormalizeRegistrationEmailSuffixWhitelist
(
settings
.
RegistrationEmailSuffixWhitelist
)
if
err
!=
nil
{
return
infraerrors
.
BadRequest
(
"INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST"
,
err
.
Error
())
}
if
normalizedWhitelist
==
nil
{
normalizedWhitelist
=
[]
string
{}
}
settings
.
RegistrationEmailSuffixWhitelist
=
normalizedWhitelist
updates
:=
make
(
map
[
string
]
string
)
// 注册设置
updates
[
SettingKeyRegistrationEnabled
]
=
strconv
.
FormatBool
(
settings
.
RegistrationEnabled
)
updates
[
SettingKeyEmailVerifyEnabled
]
=
strconv
.
FormatBool
(
settings
.
EmailVerifyEnabled
)
registrationEmailSuffixWhitelistJSON
,
err
:=
json
.
Marshal
(
settings
.
RegistrationEmailSuffixWhitelist
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"marshal registration email suffix whitelist: %w"
,
err
)
}
updates
[
SettingKeyRegistrationEmailSuffixWhitelist
]
=
string
(
registrationEmailSuffixWhitelistJSON
)
updates
[
SettingKeyPromoCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
PromoCodeEnabled
)
updates
[
SettingKeyPasswordResetEnabled
]
=
strconv
.
FormatBool
(
settings
.
PasswordResetEnabled
)
updates
[
SettingKeyInvitationCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
InvitationCodeEnabled
)
...
...
@@ -514,6 +534,15 @@ func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
return
value
==
"true"
}
// GetRegistrationEmailSuffixWhitelist returns normalized registration email suffix whitelist.
func
(
s
*
SettingService
)
GetRegistrationEmailSuffixWhitelist
(
ctx
context
.
Context
)
[]
string
{
value
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
SettingKeyRegistrationEmailSuffixWhitelist
)
if
err
!=
nil
{
return
[]
string
{}
}
return
ParseRegistrationEmailSuffixWhitelist
(
value
)
}
// IsPromoCodeEnabled 检查是否启用优惠码功能
func
(
s
*
SettingService
)
IsPromoCodeEnabled
(
ctx
context
.
Context
)
bool
{
value
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
SettingKeyPromoCodeEnabled
)
...
...
@@ -617,20 +646,21 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
// 初始化默认设置
defaults
:=
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyEmailVerifyEnabled
:
"false"
,
SettingKeyPromoCodeEnabled
:
"true"
,
// 默认启用优惠码功能
SettingKeySiteName
:
"Sub2API"
,
SettingKeySiteLogo
:
""
,
SettingKeyPurchaseSubscriptionEnabled
:
"false"
,
SettingKeyPurchaseSubscriptionURL
:
""
,
SettingKeySoraClientEnabled
:
"false"
,
SettingKeyCustomMenuItems
:
"[]"
,
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyDefaultSubscriptions
:
"[]"
,
SettingKeySMTPPort
:
"587"
,
SettingKeySMTPUseTLS
:
"false"
,
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyEmailVerifyEnabled
:
"false"
,
SettingKeyRegistrationEmailSuffixWhitelist
:
"[]"
,
SettingKeyPromoCodeEnabled
:
"true"
,
// 默认启用优惠码功能
SettingKeySiteName
:
"Sub2API"
,
SettingKeySiteLogo
:
""
,
SettingKeyPurchaseSubscriptionEnabled
:
"false"
,
SettingKeyPurchaseSubscriptionURL
:
""
,
SettingKeySoraClientEnabled
:
"false"
,
SettingKeyCustomMenuItems
:
"[]"
,
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyDefaultSubscriptions
:
"[]"
,
SettingKeySMTPPort
:
"587"
,
SettingKeySMTPUseTLS
:
"false"
,
// Model fallback defaults
SettingKeyEnableModelFallback
:
"false"
,
SettingKeyFallbackModelAnthropic
:
"claude-3-5-sonnet-20241022"
,
...
...
@@ -661,33 +691,34 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
func
(
s
*
SettingService
)
parseSettings
(
settings
map
[
string
]
string
)
*
SystemSettings
{
emailVerifyEnabled
:=
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
result
:=
&
SystemSettings
{
RegistrationEnabled
:
settings
[
SettingKeyRegistrationEnabled
]
==
"true"
,
EmailVerifyEnabled
:
emailVerifyEnabled
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
emailVerifyEnabled
&&
settings
[
SettingKeyPasswordResetEnabled
]
==
"true"
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
SMTPHost
:
settings
[
SettingKeySMTPHost
],
SMTPUsername
:
settings
[
SettingKeySMTPUsername
],
SMTPFrom
:
settings
[
SettingKeySMTPFrom
],
SMTPFromName
:
settings
[
SettingKeySMTPFromName
],
SMTPUseTLS
:
settings
[
SettingKeySMTPUseTLS
]
==
"true"
,
SMTPPasswordConfigured
:
settings
[
SettingKeySMTPPassword
]
!=
""
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
TurnstileSecretKeyConfigured
:
settings
[
SettingKeyTurnstileSecretKey
]
!=
""
,
SiteName
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteName
,
"Sub2API"
),
SiteLogo
:
settings
[
SettingKeySiteLogo
],
SiteSubtitle
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteSubtitle
,
"Subscription to API Conversion Platform"
),
APIBaseURL
:
settings
[
SettingKeyAPIBaseURL
],
ContactInfo
:
settings
[
SettingKeyContactInfo
],
DocURL
:
settings
[
SettingKeyDocURL
],
HomeContent
:
settings
[
SettingKeyHomeContent
],
HideCcsImportButton
:
settings
[
SettingKeyHideCcsImportButton
]
==
"true"
,
PurchaseSubscriptionEnabled
:
settings
[
SettingKeyPurchaseSubscriptionEnabled
]
==
"true"
,
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
RegistrationEnabled
:
settings
[
SettingKeyRegistrationEnabled
]
==
"true"
,
EmailVerifyEnabled
:
emailVerifyEnabled
,
RegistrationEmailSuffixWhitelist
:
ParseRegistrationEmailSuffixWhitelist
(
settings
[
SettingKeyRegistrationEmailSuffixWhitelist
]),
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
emailVerifyEnabled
&&
settings
[
SettingKeyPasswordResetEnabled
]
==
"true"
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
SMTPHost
:
settings
[
SettingKeySMTPHost
],
SMTPUsername
:
settings
[
SettingKeySMTPUsername
],
SMTPFrom
:
settings
[
SettingKeySMTPFrom
],
SMTPFromName
:
settings
[
SettingKeySMTPFromName
],
SMTPUseTLS
:
settings
[
SettingKeySMTPUseTLS
]
==
"true"
,
SMTPPasswordConfigured
:
settings
[
SettingKeySMTPPassword
]
!=
""
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
TurnstileSecretKeyConfigured
:
settings
[
SettingKeyTurnstileSecretKey
]
!=
""
,
SiteName
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteName
,
"Sub2API"
),
SiteLogo
:
settings
[
SettingKeySiteLogo
],
SiteSubtitle
:
s
.
getStringOrDefault
(
settings
,
SettingKeySiteSubtitle
,
"Subscription to API Conversion Platform"
),
APIBaseURL
:
settings
[
SettingKeyAPIBaseURL
],
ContactInfo
:
settings
[
SettingKeyContactInfo
],
DocURL
:
settings
[
SettingKeyDocURL
],
HomeContent
:
settings
[
SettingKeyHomeContent
],
HideCcsImportButton
:
settings
[
SettingKeyHideCcsImportButton
]
==
"true"
,
PurchaseSubscriptionEnabled
:
settings
[
SettingKeyPurchaseSubscriptionEnabled
]
==
"true"
,
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
}
// 解析整数类型
...
...
backend/internal/service/setting_service_public_test.go
0 → 100644
View file @
bd0801a8
//go:build unit
package
service
import
(
"context"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
type
settingPublicRepoStub
struct
{
values
map
[
string
]
string
}
func
(
s
*
settingPublicRepoStub
)
Get
(
ctx
context
.
Context
,
key
string
)
(
*
Setting
,
error
)
{
panic
(
"unexpected Get call"
)
}
func
(
s
*
settingPublicRepoStub
)
GetValue
(
ctx
context
.
Context
,
key
string
)
(
string
,
error
)
{
panic
(
"unexpected GetValue call"
)
}
func
(
s
*
settingPublicRepoStub
)
Set
(
ctx
context
.
Context
,
key
,
value
string
)
error
{
panic
(
"unexpected Set call"
)
}
func
(
s
*
settingPublicRepoStub
)
GetMultiple
(
ctx
context
.
Context
,
keys
[]
string
)
(
map
[
string
]
string
,
error
)
{
out
:=
make
(
map
[
string
]
string
,
len
(
keys
))
for
_
,
key
:=
range
keys
{
if
value
,
ok
:=
s
.
values
[
key
];
ok
{
out
[
key
]
=
value
}
}
return
out
,
nil
}
func
(
s
*
settingPublicRepoStub
)
SetMultiple
(
ctx
context
.
Context
,
settings
map
[
string
]
string
)
error
{
panic
(
"unexpected SetMultiple call"
)
}
func
(
s
*
settingPublicRepoStub
)
GetAll
(
ctx
context
.
Context
)
(
map
[
string
]
string
,
error
)
{
panic
(
"unexpected GetAll call"
)
}
func
(
s
*
settingPublicRepoStub
)
Delete
(
ctx
context
.
Context
,
key
string
)
error
{
panic
(
"unexpected Delete call"
)
}
func
TestSettingService_GetPublicSettings_ExposesRegistrationEmailSuffixWhitelist
(
t
*
testing
.
T
)
{
repo
:=
&
settingPublicRepoStub
{
values
:
map
[
string
]
string
{
SettingKeyRegistrationEnabled
:
"true"
,
SettingKeyEmailVerifyEnabled
:
"true"
,
SettingKeyRegistrationEmailSuffixWhitelist
:
`["@EXAMPLE.com"," @foo.bar ","@invalid_domain",""]`
,
},
}
svc
:=
NewSettingService
(
repo
,
&
config
.
Config
{})
settings
,
err
:=
svc
.
GetPublicSettings
(
context
.
Background
())
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
string
{
"@example.com"
,
"@foo.bar"
},
settings
.
RegistrationEmailSuffixWhitelist
)
}
backend/internal/service/setting_service_update_test.go
View file @
bd0801a8
...
...
@@ -172,6 +172,28 @@ func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsDuplicateGrou
require
.
Nil
(
t
,
repo
.
updates
)
}
func
TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Normalized
(
t
*
testing
.
T
)
{
repo
:=
&
settingUpdateRepoStub
{}
svc
:=
NewSettingService
(
repo
,
&
config
.
Config
{})
err
:=
svc
.
UpdateSettings
(
context
.
Background
(),
&
SystemSettings
{
RegistrationEmailSuffixWhitelist
:
[]
string
{
"example.com"
,
"@EXAMPLE.com"
,
" @foo.bar "
},
})
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
`["@example.com","@foo.bar"]`
,
repo
.
updates
[
SettingKeyRegistrationEmailSuffixWhitelist
])
}
func
TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Invalid
(
t
*
testing
.
T
)
{
repo
:=
&
settingUpdateRepoStub
{}
svc
:=
NewSettingService
(
repo
,
&
config
.
Config
{})
err
:=
svc
.
UpdateSettings
(
context
.
Background
(),
&
SystemSettings
{
RegistrationEmailSuffixWhitelist
:
[]
string
{
"@invalid_domain"
},
})
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
"INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST"
,
infraerrors
.
Reason
(
err
))
}
func
TestParseDefaultSubscriptions_NormalizesValues
(
t
*
testing
.
T
)
{
got
:=
parseDefaultSubscriptions
(
`[{"group_id":11,"validity_days":30},{"group_id":11,"validity_days":60},{"group_id":0,"validity_days":10},{"group_id":12,"validity_days":99999}]`
)
require
.
Equal
(
t
,
[]
DefaultSubscriptionSetting
{
...
...
backend/internal/service/settings_view.go
View file @
bd0801a8
package
service
type
SystemSettings
struct
{
RegistrationEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
RegistrationEnabled
bool
EmailVerifyEnabled
bool
RegistrationEmailSuffixWhitelist
[]
string
PromoCodeEnabled
bool
PasswordResetEnabled
bool
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
SMTPHost
string
SMTPPort
int
...
...
@@ -76,22 +77,23 @@ type DefaultSubscriptionSetting struct {
}
type
PublicSettings
struct
{
RegistrationEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
TurnstileEnabled
bool
TurnstileSiteKey
string
SiteName
string
SiteLogo
string
SiteSubtitle
string
APIBaseURL
string
ContactInfo
string
DocURL
string
HomeContent
string
HideCcsImportButton
bool
RegistrationEnabled
bool
EmailVerifyEnabled
bool
RegistrationEmailSuffixWhitelist
[]
string
PromoCodeEnabled
bool
PasswordResetEnabled
bool
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
TurnstileEnabled
bool
TurnstileSiteKey
string
SiteName
string
SiteLogo
string
SiteSubtitle
string
APIBaseURL
string
ContactInfo
string
DocURL
string
HomeContent
string
HideCcsImportButton
bool
PurchaseSubscriptionEnabled
bool
PurchaseSubscriptionURL
string
...
...
frontend/src/api/admin/settings.ts
View file @
bd0801a8
...
...
@@ -18,6 +18,7 @@ export interface SystemSettings {
// Registration settings
registration_enabled
:
boolean
email_verify_enabled
:
boolean
registration_email_suffix_whitelist
:
string
[]
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
invitation_code_enabled
:
boolean
...
...
@@ -86,6 +87,7 @@ export interface SystemSettings {
export
interface
UpdateSettingsRequest
{
registration_enabled
?:
boolean
email_verify_enabled
?:
boolean
registration_email_suffix_whitelist
?:
string
[]
promo_code_enabled
?:
boolean
password_reset_enabled
?:
boolean
invitation_code_enabled
?:
boolean
...
...
frontend/src/i18n/locales/en.ts
View file @
bd0801a8
...
...
@@ -312,6 +312,9 @@ export default {
passwordMinLength
:
'
Password must be at least 6 characters
'
,
loginFailed
:
'
Login failed. Please check your credentials and try again.
'
,
registrationFailed
:
'
Registration failed. Please try again.
'
,
emailSuffixNotAllowed
:
'
This email domain is not allowed for registration.
'
,
emailSuffixNotAllowedWithAllowed
:
'
This email domain is not allowed. Allowed domains: {suffixes}
'
,
loginSuccess
:
'
Login successful! Welcome back.
'
,
accountCreatedSuccess
:
'
Account created successfully! Welcome to {siteName}.
'
,
reloginRequired
:
'
Session expired. Please log in again.
'
,
...
...
@@ -3528,6 +3531,11 @@ export default {
enableRegistrationHint
:
'
Allow new users to register
'
,
emailVerification
:
'
Email Verification
'
,
emailVerificationHint
:
'
Require email verification for new registrations
'
,
emailSuffixWhitelist
:
'
Email Domain Whitelist
'
,
emailSuffixWhitelistHint
:
'
Only email addresses from the specified domains can register (for example, @qq.com, @gmail.com)
'
,
emailSuffixWhitelistPlaceholder
:
'
example.com
'
,
emailSuffixWhitelistInputHint
:
'
Leave empty for no restriction
'
,
promoCode
:
'
Promo Code
'
,
promoCodeHint
:
'
Allow users to use promo codes during registration
'
,
invitationCode
:
'
Invitation Code Registration
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
bd0801a8
...
...
@@ -312,6 +312,8 @@ export default {
passwordMinLength
:
'
密码至少需要 6 个字符
'
,
loginFailed
:
'
登录失败,请检查您的凭据后重试。
'
,
registrationFailed
:
'
注册失败,请重试。
'
,
emailSuffixNotAllowed
:
'
该邮箱域名不在允许注册范围内。
'
,
emailSuffixNotAllowedWithAllowed
:
'
该邮箱域名不被允许。可用域名:{suffixes}
'
,
loginSuccess
:
'
登录成功!欢迎回来。
'
,
accountCreatedSuccess
:
'
账户创建成功!欢迎使用 {siteName}。
'
,
reloginRequired
:
'
会话已过期,请重新登录。
'
,
...
...
@@ -3698,6 +3700,11 @@ export default {
enableRegistrationHint
:
'
允许新用户注册
'
,
emailVerification
:
'
邮箱验证
'
,
emailVerificationHint
:
'
新用户注册时需要验证邮箱
'
,
emailSuffixWhitelist
:
'
邮箱域名白名单
'
,
emailSuffixWhitelistHint
:
'
仅允许使用指定域名的邮箱注册账号(例如 @qq.com, @gmail.com)
'
,
emailSuffixWhitelistPlaceholder
:
'
example.com
'
,
emailSuffixWhitelistInputHint
:
'
留空则不限制
'
,
promoCode
:
'
优惠码
'
,
promoCodeHint
:
'
允许用户在注册时使用优惠码
'
,
invitationCode
:
'
邀请码注册
'
,
...
...
frontend/src/stores/app.ts
View file @
bd0801a8
...
...
@@ -312,6 +312,7 @@ export const useAppStore = defineStore('app', () => {
return
{
registration_enabled
:
false
,
email_verify_enabled
:
false
,
registration_email_suffix_whitelist
:
[],
promo_code_enabled
:
true
,
password_reset_enabled
:
false
,
invitation_code_enabled
:
false
,
...
...
frontend/src/types/index.ts
View file @
bd0801a8
...
...
@@ -87,6 +87,7 @@ export interface CustomMenuItem {
export
interface
PublicSettings
{
registration_enabled
:
boolean
email_verify_enabled
:
boolean
registration_email_suffix_whitelist
:
string
[]
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
invitation_code_enabled
:
boolean
...
...
frontend/src/utils/__tests__/authError.spec.ts
0 → 100644
View file @
bd0801a8
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
{
buildAuthErrorMessage
}
from
'
@/utils/authError
'
describe
(
'
buildAuthErrorMessage
'
,
()
=>
{
it
(
'
prefers response detail message when available
'
,
()
=>
{
const
message
=
buildAuthErrorMessage
(
{
response
:
{
data
:
{
detail
:
'
detailed message
'
,
message
:
'
plain message
'
}
},
},
{
fallback
:
'
fallback
'
}
)
expect
(
message
).
toBe
(
'
detailed message
'
)
})
it
(
'
falls back to response message when detail is unavailable
'
,
()
=>
{
const
message
=
buildAuthErrorMessage
(
{
response
:
{
data
:
{
message
:
'
plain message
'
}
},
},
{
fallback
:
'
fallback
'
}
)
expect
(
message
).
toBe
(
'
plain message
'
)
})
it
(
'
falls back to error.message when response payload is unavailable
'
,
()
=>
{
const
message
=
buildAuthErrorMessage
(
{
message
:
'
error message
'
},
{
fallback
:
'
fallback
'
}
)
expect
(
message
).
toBe
(
'
error message
'
)
})
it
(
'
uses fallback when no message can be extracted
'
,
()
=>
{
expect
(
buildAuthErrorMessage
({},
{
fallback
:
'
fallback
'
})).
toBe
(
'
fallback
'
)
})
})
frontend/src/utils/__tests__/registrationEmailPolicy.spec.ts
0 → 100644
View file @
bd0801a8
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
{
isRegistrationEmailSuffixAllowed
,
isRegistrationEmailSuffixDomainValid
,
normalizeRegistrationEmailSuffixDomain
,
normalizeRegistrationEmailSuffixDomains
,
normalizeRegistrationEmailSuffixWhitelist
,
parseRegistrationEmailSuffixWhitelistInput
}
from
'
@/utils/registrationEmailPolicy
'
describe
(
'
registrationEmailPolicy utils
'
,
()
=>
{
it
(
'
normalizeRegistrationEmailSuffixDomain lowercases, strips @, and ignores invalid chars
'
,
()
=>
{
expect
(
normalizeRegistrationEmailSuffixDomain
(
'
@Exa!mple.COM
'
)).
toBe
(
'
example.com
'
)
})
it
(
'
normalizeRegistrationEmailSuffixDomains deduplicates normalized domains
'
,
()
=>
{
expect
(
normalizeRegistrationEmailSuffixDomains
([
'
@example.com
'
,
'
Example.com
'
,
''
,
'
-invalid.com
'
,
'
foo..bar.com
'
,
'
@foo.bar
'
,
'
@foo.bar
'
])
).
toEqual
([
'
example.com
'
,
'
foo.bar
'
])
})
it
(
'
parseRegistrationEmailSuffixWhitelistInput supports separators and deduplicates
'
,
()
=>
{
const
input
=
'
\n
@example.com,example.com,@foo.bar
\t
@FOO.bar
'
expect
(
parseRegistrationEmailSuffixWhitelistInput
(
input
)).
toEqual
([
'
example.com
'
,
'
foo.bar
'
])
})
it
(
'
parseRegistrationEmailSuffixWhitelistInput drops tokens containing invalid chars
'
,
()
=>
{
const
input
=
'
@exa!mple.com, @foo.bar, @bad#token.com, @ok-domain.com
'
expect
(
parseRegistrationEmailSuffixWhitelistInput
(
input
)).
toEqual
([
'
foo.bar
'
,
'
ok-domain.com
'
])
})
it
(
'
parseRegistrationEmailSuffixWhitelistInput drops structurally invalid domains
'
,
()
=>
{
const
input
=
'
@-bad.com, @foo..bar.com, @foo.bar, @xn--ok.com
'
expect
(
parseRegistrationEmailSuffixWhitelistInput
(
input
)).
toEqual
([
'
foo.bar
'
,
'
xn--ok.com
'
])
})
it
(
'
parseRegistrationEmailSuffixWhitelistInput returns empty list for blank input
'
,
()
=>
{
expect
(
parseRegistrationEmailSuffixWhitelistInput
(
'
\n
\n
'
)).
toEqual
([])
})
it
(
'
normalizeRegistrationEmailSuffixWhitelist returns canonical @domain list
'
,
()
=>
{
expect
(
normalizeRegistrationEmailSuffixWhitelist
([
'
@Example.com
'
,
'
foo.bar
'
,
''
,
'
-invalid.com
'
,
'
@foo.bar
'
])
).
toEqual
([
'
@example.com
'
,
'
@foo.bar
'
])
})
it
(
'
isRegistrationEmailSuffixDomainValid matches backend-compatible domain rules
'
,
()
=>
{
expect
(
isRegistrationEmailSuffixDomainValid
(
'
example.com
'
)).
toBe
(
true
)
expect
(
isRegistrationEmailSuffixDomainValid
(
'
foo-bar.example.com
'
)).
toBe
(
true
)
expect
(
isRegistrationEmailSuffixDomainValid
(
'
-bad.com
'
)).
toBe
(
false
)
expect
(
isRegistrationEmailSuffixDomainValid
(
'
foo..bar.com
'
)).
toBe
(
false
)
expect
(
isRegistrationEmailSuffixDomainValid
(
'
localhost
'
)).
toBe
(
false
)
})
it
(
'
isRegistrationEmailSuffixAllowed allows any email when whitelist is empty
'
,
()
=>
{
expect
(
isRegistrationEmailSuffixAllowed
(
'
user@example.com
'
,
[])).
toBe
(
true
)
})
it
(
'
isRegistrationEmailSuffixAllowed applies exact suffix matching
'
,
()
=>
{
expect
(
isRegistrationEmailSuffixAllowed
(
'
user@example.com
'
,
[
'
@example.com
'
])).
toBe
(
true
)
expect
(
isRegistrationEmailSuffixAllowed
(
'
user@sub.example.com
'
,
[
'
@example.com
'
])).
toBe
(
false
)
})
})
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment