mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-01 15:02:58 +08:00
feat: add default-off switch for email domain registration quota
PR #5423 relaxed the email suffix whitelist: once a whitelist is configured, non-whitelisted registrable domains are each allowed to register one account. That behavior activated unconditionally. Add registration_email_domain_quota_enabled (default false) to gate it: - Off (default): restore pre-#5423 strict whitelist semantics — with a non-empty whitelist, non-whitelisted domains are rejected with EMAIL_SUFFIX_NOT_ALLOWED; the register/verify views restore the client-side whitelist pre-check and allowed-domain hint. - On: keep #5423 behavior — one account per non-whitelisted registrable domain (EMAIL_DOMAIN_REGISTRATION_LIMIT). - Empty whitelist keeps allowing all domains in both states. Gating lives in validateRegistrationEmailQuota and (as a race-safety backstop) createUserWithRegistrationEmailGuard; the repository-level domain lock + in-tx recheck is unchanged. The admin update field is *bool (omitted = keep current) so stale full-payload saves cannot silently flip the switch. Email binding and OAuth auto-signup keep their strict policy, and pending-OAuth bind-login for existing accounts is unaffected because the handler resolves existing emails before the quota check. Frontend adds the toggle to admin settings (zh/en copy; whitelist hint restored to strict wording, quota wording moved to the new toggle) and exposes the flag via public settings + SSR injection payload. Tests: #5423 quota tests now enable the switch explicitly; new default-off regression tests cover register/send-code/async/pending OAuth/OIDC create-account plus both register views; API contract JSON and the injection drift guard are updated.
This commit is contained in:
@@ -136,6 +136,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
RegistrationEnabled: settings.RegistrationEnabled,
|
||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
||||
RegistrationEmailDomainQuotaEnabled: settings.RegistrationEmailDomainQuotaEnabled,
|
||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||
FrontendURL: settings.FrontendURL,
|
||||
|
||||
@@ -41,6 +41,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if !equalStringSlice(before.RegistrationEmailSuffixWhitelist, after.RegistrationEmailSuffixWhitelist) {
|
||||
changed = append(changed, "registration_email_suffix_whitelist")
|
||||
}
|
||||
if before.RegistrationEmailDomainQuotaEnabled != after.RegistrationEmailDomainQuotaEnabled {
|
||||
changed = append(changed, "registration_email_domain_quota_enabled")
|
||||
}
|
||||
if before.PromoCodeEnabled != after.PromoCodeEnabled {
|
||||
changed = append(changed, "promo_code_enabled")
|
||||
}
|
||||
|
||||
@@ -23,22 +23,23 @@ import (
|
||||
// UpdateSettingsRequest 更新设置请求
|
||||
type UpdateSettingsRequest struct {
|
||||
// 注册设置
|
||||
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"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
||||
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
||||
PasskeyEnabled *bool `json:"passkey_enabled"` // Passkey 登录(省略=保持现值)
|
||||
SessionBindingEnabled *bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定(省略=保持现值)
|
||||
StepUpEnabled *bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA(省略=保持现值)
|
||||
AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementDocuments []dto.LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
|
||||
RegistrationEmailDomainQuotaEnabled *bool `json:"registration_email_domain_quota_enabled"` // 非白名单域名限量注册开关(省略=保持现值)
|
||||
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
||||
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
||||
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
||||
PasskeyEnabled *bool `json:"passkey_enabled"` // Passkey 登录(省略=保持现值)
|
||||
SessionBindingEnabled *bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定(省略=保持现值)
|
||||
StepUpEnabled *bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA(省略=保持现值)
|
||||
AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementDocuments []dto.LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
|
||||
// 邮件服务设置
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
@@ -512,6 +513,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
if req.PasskeyEnabled != nil {
|
||||
passkeyEnabled = *req.PasskeyEnabled
|
||||
}
|
||||
registrationEmailDomainQuotaEnabled := previousSettings.RegistrationEmailDomainQuotaEnabled
|
||||
if req.RegistrationEmailDomainQuotaEnabled != nil {
|
||||
registrationEmailDomainQuotaEnabled = *req.RegistrationEmailDomainQuotaEnabled
|
||||
}
|
||||
if passkeyEnabled {
|
||||
configured, _, _ := h.settingService.PasskeyConfiguration()
|
||||
if !configured {
|
||||
@@ -1489,44 +1494,45 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
DefaultPlatformQuotas: req.DefaultPlatformQuotas,
|
||||
AccountSchedulingThresholds: req.AccountSchedulingThresholds,
|
||||
|
||||
RegistrationEnabled: req.RegistrationEnabled,
|
||||
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
|
||||
PromoCodeEnabled: req.PromoCodeEnabled,
|
||||
PasswordResetEnabled: req.PasswordResetEnabled,
|
||||
FrontendURL: req.FrontendURL,
|
||||
InvitationCodeEnabled: req.InvitationCodeEnabled,
|
||||
TotpEnabled: req.TotpEnabled,
|
||||
PasskeyEnabled: passkeyEnabled,
|
||||
SessionBindingEnabled: sessionBindingEnabled,
|
||||
StepUpEnabled: stepUpEnabled,
|
||||
AuditLogRetentionDays: req.AuditLogRetentionDays,
|
||||
LoginAgreementEnabled: req.LoginAgreementEnabled,
|
||||
LoginAgreementMode: loginAgreementMode,
|
||||
LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
|
||||
LoginAgreementDocuments: loginAgreementDocuments,
|
||||
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,
|
||||
TencentCaptchaEnabled: req.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: req.TencentCaptchaAppID,
|
||||
TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
|
||||
TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
|
||||
TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
|
||||
TencentCaptchaRegion: req.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: req.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: req.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecret: req.AliyunCaptchaAccessKeySecret,
|
||||
AliyunCaptchaSceneID: req.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: req.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: req.AliyunCaptchaRegion,
|
||||
RegistrationEnabled: req.RegistrationEnabled,
|
||||
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
|
||||
RegistrationEmailDomainQuotaEnabled: registrationEmailDomainQuotaEnabled,
|
||||
PromoCodeEnabled: req.PromoCodeEnabled,
|
||||
PasswordResetEnabled: req.PasswordResetEnabled,
|
||||
FrontendURL: req.FrontendURL,
|
||||
InvitationCodeEnabled: req.InvitationCodeEnabled,
|
||||
TotpEnabled: req.TotpEnabled,
|
||||
PasskeyEnabled: passkeyEnabled,
|
||||
SessionBindingEnabled: sessionBindingEnabled,
|
||||
StepUpEnabled: stepUpEnabled,
|
||||
AuditLogRetentionDays: req.AuditLogRetentionDays,
|
||||
LoginAgreementEnabled: req.LoginAgreementEnabled,
|
||||
LoginAgreementMode: loginAgreementMode,
|
||||
LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
|
||||
LoginAgreementDocuments: loginAgreementDocuments,
|
||||
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,
|
||||
TencentCaptchaEnabled: req.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: req.TencentCaptchaAppID,
|
||||
TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
|
||||
TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
|
||||
TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
|
||||
TencentCaptchaRegion: req.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: req.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: req.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecret: req.AliyunCaptchaAccessKeySecret,
|
||||
AliyunCaptchaSceneID: req.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: req.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: req.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: func() bool {
|
||||
if req.APIKeyACLTrustForwardedIP != nil {
|
||||
return *req.APIKeyACLTrustForwardedIP
|
||||
@@ -2102,6 +2108,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
RegistrationEnabled: updatedSettings.RegistrationEnabled,
|
||||
EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
|
||||
RegistrationEmailDomainQuotaEnabled: updatedSettings.RegistrationEmailDomainQuotaEnabled,
|
||||
PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
|
||||
PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
|
||||
FrontendURL: updatedSettings.FrontendURL,
|
||||
|
||||
@@ -1473,7 +1473,8 @@ func TestCreateOIDCOAuthAccountRejectsSecondEmailOutsideRegistrationSuffixWhitel
|
||||
},
|
||||
},
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyRegistrationEmailSuffixWhitelist: `["@qq.com"]`,
|
||||
service.SettingKeyRegistrationEmailSuffixWhitelist: `["@qq.com"]`,
|
||||
service.SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
@@ -1520,6 +1521,60 @@ func TestCreateOIDCOAuthAccountRejectsSecondEmailOutsideRegistrationSuffixWhitel
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
// 域名限量注册开关默认关闭:白名单外域名保持 PR5423 之前的严格拒绝语义,
|
||||
// 即使该域名下还没有任何账户也不放行。
|
||||
func TestCreateOIDCOAuthAccountRejectsEmailOutsideWhitelistWhenQuotaDisabled(t *testing.T) {
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
emailVerifyEnabled: true,
|
||||
emailCache: &oauthPendingFlowEmailCacheStub{
|
||||
verificationCodes: map[string]*service.VerificationCodeData{
|
||||
"foo@gmail.com": {
|
||||
Code: "135790",
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().UTC().Add(15 * time.Minute),
|
||||
},
|
||||
},
|
||||
},
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyRegistrationEmailSuffixWhitelist: `["@qq.com"]`,
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
session, err := client.PendingAuthSession.Create().
|
||||
SetSessionToken("suffix-strict-session-token").
|
||||
SetIntent("login").
|
||||
SetProviderType("oidc").
|
||||
SetProviderKey("https://issuer.example").
|
||||
SetProviderSubject("oidc-suffix-strict-123").
|
||||
SetBrowserSessionKey("suffix-strict-browser-session-key").
|
||||
SetUpstreamIdentityClaims(map[string]any{
|
||||
"username": "oidc_user",
|
||||
}).
|
||||
SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := bytes.NewBufferString(`{"email":"foo@gmail.com","verify_code":"135790","password":"secret-123"}`)
|
||||
recorder := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(recorder)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/oidc/create-account", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("suffix-strict-browser-session-key")})
|
||||
ginCtx.Request = req
|
||||
|
||||
handler.CreateOIDCOAuthAccount(ginCtx)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
payload := decodeJSONBody(t, recorder)
|
||||
require.Equal(t, "EMAIL_SUFFIX_NOT_ALLOWED", payload["reason"])
|
||||
|
||||
count, err := client.User.Query().Where(dbuser.EmailEQ("foo@gmail.com")).Count(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
func TestSendPendingOAuthVerifyCodeExistingEmailReturnsBindLoginState(t *testing.T) {
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithEmailVerification(t, false, "owner@example.com", "135790")
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -27,26 +27,27 @@ type CustomEndpoint struct {
|
||||
|
||||
// SystemSettings represents the admin settings API response payload.
|
||||
type SystemSettings struct {
|
||||
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"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
||||
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
||||
TotpEncryptionKeyConfigured bool `json:"totp_encryption_key_configured"` // TOTP 加密密钥是否已配置
|
||||
PasskeyEnabled bool `json:"passkey_enabled"`
|
||||
PasskeyConfigured bool `json:"passkey_configured"`
|
||||
PasskeyRPID string `json:"passkey_rp_id"`
|
||||
PasskeyRPOrigins []string `json:"passkey_rp_origins"`
|
||||
SessionBindingEnabled bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定
|
||||
StepUpEnabled bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA
|
||||
AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementDocuments []LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
|
||||
RegistrationEmailDomainQuotaEnabled bool `json:"registration_email_domain_quota_enabled"`
|
||||
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
||||
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
||||
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
|
||||
TotpEncryptionKeyConfigured bool `json:"totp_encryption_key_configured"` // TOTP 加密密钥是否已配置
|
||||
PasskeyEnabled bool `json:"passkey_enabled"`
|
||||
PasskeyConfigured bool `json:"passkey_configured"`
|
||||
PasskeyRPID string `json:"passkey_rp_id"`
|
||||
PasskeyRPOrigins []string `json:"passkey_rp_origins"`
|
||||
SessionBindingEnabled bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定
|
||||
StepUpEnabled bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA
|
||||
AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementDocuments []LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
@@ -347,58 +348,59 @@ type DefaultSubscriptionSetting struct {
|
||||
}
|
||||
|
||||
type PublicSettings struct {
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||
ForceEmailOnThirdPartySignup bool `json:"force_email_on_third_party_signup"`
|
||||
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 双因素认证
|
||||
PasskeyEnabled bool `json:"passkey_enabled"`
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementRevision string `json:"login_agreement_revision"`
|
||||
LoginAgreementDocuments []LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
TencentCaptchaRegion string `json:"tencent_captcha_region"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
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"`
|
||||
CompactHomeEnabled bool `json:"compact_home_enabled"`
|
||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||
TableDefaultPageSize int `json:"table_default_page_size"`
|
||||
TablePageSizeOptions []int `json:"table_page_size_options"`
|
||||
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
|
||||
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
|
||||
DingTalkOAuthEnabled bool `json:"dingtalk_oauth_enabled"`
|
||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||
WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"`
|
||||
WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"`
|
||||
WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"`
|
||||
WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"`
|
||||
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
||||
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
||||
GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
|
||||
GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
|
||||
SoraClientEnabled bool `json:"sora_client_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
PaymentEnabled bool `json:"payment_enabled"`
|
||||
Version string `json:"version"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||
ForceEmailOnThirdPartySignup bool `json:"force_email_on_third_party_signup"`
|
||||
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
|
||||
RegistrationEmailDomainQuotaEnabled bool `json:"registration_email_domain_quota_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 双因素认证
|
||||
PasskeyEnabled bool `json:"passkey_enabled"`
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementRevision string `json:"login_agreement_revision"`
|
||||
LoginAgreementDocuments []LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
TencentCaptchaRegion string `json:"tencent_captcha_region"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
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"`
|
||||
CompactHomeEnabled bool `json:"compact_home_enabled"`
|
||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||
TableDefaultPageSize int `json:"table_default_page_size"`
|
||||
TablePageSizeOptions []int `json:"table_page_size_options"`
|
||||
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
|
||||
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
|
||||
DingTalkOAuthEnabled bool `json:"dingtalk_oauth_enabled"`
|
||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||
WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"`
|
||||
WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"`
|
||||
WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"`
|
||||
WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"`
|
||||
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
||||
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
||||
GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
|
||||
GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
|
||||
SoraClientEnabled bool `json:"sora_client_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
PaymentEnabled bool `json:"payment_enabled"`
|
||||
Version string `json:"version"`
|
||||
// 服务器全局时区(IANA 名称与当前 UTC 偏移,如 "Asia/Shanghai" / "+08:00")。
|
||||
// 高峰时段等按服务器本地时间判定的窗口,前端展示时据此标注,避免用户按浏览器本地时间误读。
|
||||
ServerTimezone string `json:"server_timezone"`
|
||||
|
||||
@@ -44,63 +44,64 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
response.Success(c, dto.PublicSettings{
|
||||
RegistrationEnabled: settings.RegistrationEnabled,
|
||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||
ForceEmailOnThirdPartySignup: settings.ForceEmailOnThirdPartySignup,
|
||||
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
||||
TotpEnabled: settings.TotpEnabled,
|
||||
PasskeyEnabled: settings.PasskeyEnabled,
|
||||
LoginAgreementEnabled: settings.LoginAgreementEnabled,
|
||||
LoginAgreementMode: settings.LoginAgreementMode,
|
||||
LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
|
||||
LoginAgreementRevision: settings.LoginAgreementRevision,
|
||||
LoginAgreementDocuments: publicLoginAgreementDocumentsToDTO(settings.LoginAgreementDocuments),
|
||||
TurnstileEnabled: settings.TurnstileEnabled,
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
TencentCaptchaRegion: settings.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
SiteName: settings.SiteName,
|
||||
SiteLogo: settings.SiteLogo,
|
||||
SiteSubtitle: settings.SiteSubtitle,
|
||||
APIBaseURL: settings.APIBaseURL,
|
||||
ContactInfo: settings.ContactInfo,
|
||||
DocURL: settings.DocURL,
|
||||
HomeContent: settings.HomeContent,
|
||||
CompactHomeEnabled: settings.CompactHomeEnabled,
|
||||
HideCcsImportButton: settings.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
TableDefaultPageSize: settings.TableDefaultPageSize,
|
||||
TablePageSizeOptions: settings.TablePageSizeOptions,
|
||||
CustomMenuItems: dto.ParseUserVisibleMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
|
||||
DingTalkOAuthEnabled: settings.DingTalkOAuthEnabled,
|
||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||
WeChatOAuthEnabled: settings.WeChatOAuthEnabled,
|
||||
WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled,
|
||||
WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled,
|
||||
WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled,
|
||||
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
||||
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
||||
GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
|
||||
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
PaymentEnabled: settings.PaymentEnabled,
|
||||
Version: h.version,
|
||||
ServerTimezone: timezone.Name(),
|
||||
ServerUTCOffset: timezone.UTCOffset(),
|
||||
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
||||
RegistrationEnabled: settings.RegistrationEnabled,
|
||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||
ForceEmailOnThirdPartySignup: settings.ForceEmailOnThirdPartySignup,
|
||||
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
||||
RegistrationEmailDomainQuotaEnabled: settings.RegistrationEmailDomainQuotaEnabled,
|
||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
||||
TotpEnabled: settings.TotpEnabled,
|
||||
PasskeyEnabled: settings.PasskeyEnabled,
|
||||
LoginAgreementEnabled: settings.LoginAgreementEnabled,
|
||||
LoginAgreementMode: settings.LoginAgreementMode,
|
||||
LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
|
||||
LoginAgreementRevision: settings.LoginAgreementRevision,
|
||||
LoginAgreementDocuments: publicLoginAgreementDocumentsToDTO(settings.LoginAgreementDocuments),
|
||||
TurnstileEnabled: settings.TurnstileEnabled,
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
TencentCaptchaRegion: settings.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
SiteName: settings.SiteName,
|
||||
SiteLogo: settings.SiteLogo,
|
||||
SiteSubtitle: settings.SiteSubtitle,
|
||||
APIBaseURL: settings.APIBaseURL,
|
||||
ContactInfo: settings.ContactInfo,
|
||||
DocURL: settings.DocURL,
|
||||
HomeContent: settings.HomeContent,
|
||||
CompactHomeEnabled: settings.CompactHomeEnabled,
|
||||
HideCcsImportButton: settings.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
TableDefaultPageSize: settings.TableDefaultPageSize,
|
||||
TablePageSizeOptions: settings.TablePageSizeOptions,
|
||||
CustomMenuItems: dto.ParseUserVisibleMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
|
||||
DingTalkOAuthEnabled: settings.DingTalkOAuthEnabled,
|
||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||
WeChatOAuthEnabled: settings.WeChatOAuthEnabled,
|
||||
WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled,
|
||||
WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled,
|
||||
WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled,
|
||||
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
||||
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
||||
GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
|
||||
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
PaymentEnabled: settings.PaymentEnabled,
|
||||
Version: h.version,
|
||||
ServerTimezone: timezone.Name(),
|
||||
ServerUTCOffset: timezone.UTCOffset(),
|
||||
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
||||
|
||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||
ChannelMonitorMode: settings.ChannelMonitorMode,
|
||||
|
||||
@@ -714,6 +714,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
"registration_enabled": true,
|
||||
"email_verify_enabled": false,
|
||||
"registration_email_suffix_whitelist": [],
|
||||
"registration_email_domain_quota_enabled": false,
|
||||
"promo_code_enabled": true,
|
||||
"password_reset_enabled": false,
|
||||
"frontend_url": "",
|
||||
@@ -1059,6 +1060,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
"registration_enabled": true,
|
||||
"email_verify_enabled": false,
|
||||
"registration_email_suffix_whitelist": [],
|
||||
"registration_email_domain_quota_enabled": false,
|
||||
"promo_code_enabled": true,
|
||||
"password_reset_enabled": false,
|
||||
"frontend_url": "",
|
||||
|
||||
@@ -206,8 +206,9 @@ func TestRegisterOAuthEmailAccount_NonWhitelistDomainLimit(t *testing.T) {
|
||||
&redeemCodeRepoStub{},
|
||||
&refreshTokenCacheStub{},
|
||||
map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
},
|
||||
&emailCacheStub{data: &VerificationCodeData{
|
||||
Code: "246810",
|
||||
@@ -236,8 +237,9 @@ func TestRegisterVerifiedOAuthEmailAccount_NonWhitelistDomainLimit(t *testing.T)
|
||||
nil,
|
||||
&refreshTokenCacheStub{},
|
||||
map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
},
|
||||
&emailCacheStub{},
|
||||
nil,
|
||||
@@ -261,8 +263,9 @@ func TestSendPendingOAuthVerifyCode_NonWhitelistDomainLimit(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
},
|
||||
&emailCacheStub{},
|
||||
nil,
|
||||
@@ -272,6 +275,25 @@ func TestSendPendingOAuthVerifyCode_NonWhitelistDomainLimit(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
|
||||
}
|
||||
|
||||
// 域名限量注册开关默认关闭:白名单外域名在 pending OAuth 发码阶段即被严格拒绝。
|
||||
func TestSendPendingOAuthVerifyCode_NonWhitelistDomainRejectedWhenQuotaDisabled(t *testing.T) {
|
||||
userRepo := &userRepoStub{domainCounts: map[string]int{"custom.example": 0}}
|
||||
authService := newOAuthEmailFlowAuthService(
|
||||
userRepo,
|
||||
nil,
|
||||
nil,
|
||||
map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
},
|
||||
&emailCacheStub{},
|
||||
nil,
|
||||
)
|
||||
|
||||
_, err := authService.SendPendingOAuthVerifyCode(context.Background(), "first@custom.example")
|
||||
require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
|
||||
}
|
||||
|
||||
func TestSendPendingOAuthVerifyCode_NilServiceReturnsUnavailable(t *testing.T) {
|
||||
var authService *AuthService
|
||||
|
||||
|
||||
@@ -1207,6 +1207,7 @@ func (s *AuthService) validateRegistrationEmailPolicy(ctx context.Context, email
|
||||
}
|
||||
|
||||
// validateRegistrationEmailQuota 保留白名单为空时的全放行行为;配置白名单后,
|
||||
// 非白名单域名默认直接拒绝(严格白名单模式);仅当域名限量注册开关开启时,
|
||||
// 非白名单域名每个最多允许一个账户。
|
||||
func (s *AuthService) validateRegistrationEmailQuota(ctx context.Context, email string) error {
|
||||
if s.settingService == nil {
|
||||
@@ -1216,6 +1217,9 @@ func (s *AuthService) validateRegistrationEmailQuota(ctx context.Context, email
|
||||
if !IsRegistrationEmailSuffixLimited(email, whitelist) {
|
||||
return nil
|
||||
}
|
||||
if !s.settingService.IsRegistrationEmailDomainQuotaEnabled(ctx) {
|
||||
return buildEmailSuffixNotAllowedError(whitelist)
|
||||
}
|
||||
|
||||
domain := RegistrationEmailDomain(email)
|
||||
if domain == "" {
|
||||
@@ -1253,6 +1257,10 @@ func (s *AuthService) createUserWithRegistrationEmailGuard(ctx context.Context,
|
||||
if !IsRegistrationEmailSuffixLimited(user.Email, whitelist) {
|
||||
return s.userRepo.CreateWithEmailAliasGuard(ctx, user)
|
||||
}
|
||||
// 开关关闭时非白名单域名在校验阶段已被拒绝;此处兜底防御设置竞态变更。
|
||||
if s.settingService == nil || !s.settingService.IsRegistrationEmailDomainQuotaEnabled(ctx) {
|
||||
return buildEmailSuffixNotAllowedError(whitelist)
|
||||
}
|
||||
if domain == "" {
|
||||
return buildEmailSuffixNotAllowedError(whitelist)
|
||||
}
|
||||
|
||||
@@ -416,8 +416,9 @@ func TestAuthService_Register_ReservedEmail(t *testing.T) {
|
||||
func TestAuthService_Register_EmailSuffixNotAllowed(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"other.com": 1}}
|
||||
service := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com","@company.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com","@company.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
_, _, err := service.Register(context.Background(), "user@other.com", "password")
|
||||
@@ -430,8 +431,9 @@ func TestAuthService_Register_EmailSuffixNotAllowed(t *testing.T) {
|
||||
func TestAuthService_Register_NonWhitelistDomainAllowsFirstAccount(t *testing.T) {
|
||||
repo := &userRepoStub{nextID: 9, domainCounts: map[string]int{"custom.example": 0}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
_, user, err := svc.Register(context.Background(), "first@custom.example", "password")
|
||||
@@ -442,14 +444,81 @@ func TestAuthService_Register_NonWhitelistDomainAllowsFirstAccount(t *testing.T)
|
||||
func TestAuthService_Register_NonWhitelistDomainRejectsSecondAccount(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 1}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
_, _, err := svc.Register(context.Background(), "second@sub.custom.example", "password")
|
||||
require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
|
||||
}
|
||||
|
||||
// 域名限量注册开关默认关闭:白名单外域名保持 PR5423 之前的严格拒绝语义,
|
||||
// 即使该域名下还没有任何账户也不放行。
|
||||
func TestAuthService_Register_NonWhitelistDomainRejectedWhenQuotaDisabledByDefault(t *testing.T) {
|
||||
repo := &userRepoStub{nextID: 9, domainCounts: map[string]int{"custom.example": 0}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
}, nil, nil)
|
||||
|
||||
_, _, err := svc.Register(context.Background(), "first@custom.example", "password")
|
||||
require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
|
||||
appErr := infraerrors.FromError(err)
|
||||
require.Equal(t, "EMAIL_SUFFIX_NOT_ALLOWED", appErr.Reason)
|
||||
require.Empty(t, repo.created)
|
||||
require.Zero(t, repo.domainLimitedCreates)
|
||||
}
|
||||
|
||||
func TestAuthService_Register_NonWhitelistDomainRejectedWhenQuotaExplicitlyDisabled(t *testing.T) {
|
||||
repo := &userRepoStub{nextID: 9, domainCounts: map[string]int{"custom.example": 0}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "false",
|
||||
}, nil, nil)
|
||||
|
||||
_, _, err := svc.Register(context.Background(), "first@custom.example", "password")
|
||||
require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
|
||||
require.Empty(t, repo.created)
|
||||
}
|
||||
|
||||
// 开关关闭不影响白名单命中域名的正常注册。
|
||||
func TestAuthService_Register_WhitelistDomainAllowedWhenQuotaDisabled(t *testing.T) {
|
||||
repo := &userRepoStub{nextID: 12}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
}, nil, nil)
|
||||
|
||||
_, user, err := svc.Register(context.Background(), "user@example.com", "password")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(12), user.ID)
|
||||
require.Zero(t, repo.domainLimitedCreates)
|
||||
}
|
||||
|
||||
func TestAuthService_SendVerifyCode_NonWhitelistDomainRejectedWhenQuotaDisabled(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 0}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
}, nil, nil)
|
||||
|
||||
err := svc.SendVerifyCode(context.Background(), "user@custom.example")
|
||||
require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
|
||||
}
|
||||
|
||||
func TestAuthService_SendVerifyCodeAsync_NonWhitelistDomainRejectedWhenQuotaDisabled(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 0}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
}, nil, nil)
|
||||
|
||||
_, err := svc.SendVerifyCodeAsync(context.Background(), "user@custom.example")
|
||||
require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
|
||||
}
|
||||
|
||||
func TestAuthService_Register_EmptyWhitelistAllowsAllDomains(t *testing.T) {
|
||||
repo := &userRepoStub{nextID: 10}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
@@ -477,8 +546,9 @@ func TestAuthService_Register_EmailSuffixAllowed(t *testing.T) {
|
||||
func TestAuthService_SendVerifyCode_EmailSuffixNotAllowed(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"other.com": 1}}
|
||||
service := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com","@company.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com","@company.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
err := service.SendVerifyCode(context.Background(), "user@other.com")
|
||||
@@ -490,8 +560,9 @@ func TestAuthService_SendVerifyCode_EmailSuffixNotAllowed(t *testing.T) {
|
||||
func TestAuthService_SendVerifyCode_NonWhitelistDomainLimit(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 1}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
err := svc.SendVerifyCode(context.Background(), "user@custom.example")
|
||||
@@ -501,8 +572,9 @@ func TestAuthService_SendVerifyCode_NonWhitelistDomainLimit(t *testing.T) {
|
||||
func TestAuthService_SendVerifyCodeAsync_NonWhitelistDomainLimit(t *testing.T) {
|
||||
repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 1}}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
_, err := svc.SendVerifyCodeAsync(context.Background(), "user@custom.example")
|
||||
|
||||
@@ -142,24 +142,27 @@ const (
|
||||
SettingKeyRegistrationEnabled = "registration_enabled" // 是否开放注册
|
||||
SettingKeyEmailVerifyEnabled = "email_verify_enabled" // 是否开启邮件验证
|
||||
SettingKeyRegistrationEmailSuffixWhitelist = "registration_email_suffix_whitelist" // 注册邮箱后缀白名单(JSON 数组)
|
||||
SettingKeyPromoCodeEnabled = "promo_code_enabled" // 是否启用优惠码功能
|
||||
SettingKeyPasswordResetEnabled = "password_reset_enabled" // 是否启用忘记密码功能(需要先开启邮件验证)
|
||||
SettingKeyFrontendURL = "frontend_url" // 前端基础URL,用于生成邮件中的重置密码链接
|
||||
SettingKeyInvitationCodeEnabled = "invitation_code_enabled" // 是否启用邀请码注册
|
||||
SettingKeyAffiliateEnabled = "affiliate_enabled" // 邀请返利功能总开关
|
||||
SettingKeyAffiliateRebateRate = "affiliate_rebate_rate" // 邀请返利比例(百分比,0-100)
|
||||
SettingKeyAffiliateRebateFreezeHours = "affiliate_rebate_freeze_hours" // 返利冻结期(小时,0=不冻结)
|
||||
SettingKeyAffiliateRebateDurationDays = "affiliate_rebate_duration_days" // 返利有效期(天,0=永久)
|
||||
SettingKeyAffiliateRebatePerInviteeCap = "affiliate_rebate_per_invitee_cap" // 单人返利上限(0=无上限)
|
||||
SettingKeyAffiliateAdminRechargeEnabled = "affiliate_admin_recharge_enabled" // 管理员充值是否产生返利
|
||||
SettingKeyRiskControlEnabled = "risk_control_enabled" // 是否启用风控中心入口与审计链路
|
||||
SettingKeyContentModerationConfig = "content_moderation_config" // 内容审计配置(JSON)
|
||||
SettingKeyCyberSessionBlockEnabled = "cyber_session_block_enabled" // cyber 命中后会话级自动屏蔽总开关(默认关)
|
||||
SettingKeyCyberSessionBlockTTLSeconds = "cyber_session_block_ttl_seconds" // 会话屏蔽 TTL 秒数(默认 3600)
|
||||
SettingKeyLoginAgreementEnabled = "login_agreement_enabled" // 登录前是否要求同意条款
|
||||
SettingKeyLoginAgreementMode = "login_agreement_mode" // 条款确认展示模式:modal / checkbox
|
||||
SettingKeyLoginAgreementUpdatedAt = "login_agreement_updated_at" // 条款更新日期(展示用)
|
||||
SettingKeyLoginAgreementDocuments = "login_agreement_documents" // 条款文档列表(JSON,Markdown 内容)
|
||||
// 白名单非空时,是否放行非白名单域名按主域名限量注册(每域名 1 个账户)。
|
||||
// 默认 false:非白名单域名直接拒绝(白名单严格模式)。
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled = "registration_email_domain_quota_enabled"
|
||||
SettingKeyPromoCodeEnabled = "promo_code_enabled" // 是否启用优惠码功能
|
||||
SettingKeyPasswordResetEnabled = "password_reset_enabled" // 是否启用忘记密码功能(需要先开启邮件验证)
|
||||
SettingKeyFrontendURL = "frontend_url" // 前端基础URL,用于生成邮件中的重置密码链接
|
||||
SettingKeyInvitationCodeEnabled = "invitation_code_enabled" // 是否启用邀请码注册
|
||||
SettingKeyAffiliateEnabled = "affiliate_enabled" // 邀请返利功能总开关
|
||||
SettingKeyAffiliateRebateRate = "affiliate_rebate_rate" // 邀请返利比例(百分比,0-100)
|
||||
SettingKeyAffiliateRebateFreezeHours = "affiliate_rebate_freeze_hours" // 返利冻结期(小时,0=不冻结)
|
||||
SettingKeyAffiliateRebateDurationDays = "affiliate_rebate_duration_days" // 返利有效期(天,0=永久)
|
||||
SettingKeyAffiliateRebatePerInviteeCap = "affiliate_rebate_per_invitee_cap" // 单人返利上限(0=无上限)
|
||||
SettingKeyAffiliateAdminRechargeEnabled = "affiliate_admin_recharge_enabled" // 管理员充值是否产生返利
|
||||
SettingKeyRiskControlEnabled = "risk_control_enabled" // 是否启用风控中心入口与审计链路
|
||||
SettingKeyContentModerationConfig = "content_moderation_config" // 内容审计配置(JSON)
|
||||
SettingKeyCyberSessionBlockEnabled = "cyber_session_block_enabled" // cyber 命中后会话级自动屏蔽总开关(默认关)
|
||||
SettingKeyCyberSessionBlockTTLSeconds = "cyber_session_block_ttl_seconds" // 会话屏蔽 TTL 秒数(默认 3600)
|
||||
SettingKeyLoginAgreementEnabled = "login_agreement_enabled" // 登录前是否要求同意条款
|
||||
SettingKeyLoginAgreementMode = "login_agreement_mode" // 条款确认展示模式:modal / checkbox
|
||||
SettingKeyLoginAgreementUpdatedAt = "login_agreement_updated_at" // 条款更新日期(展示用)
|
||||
SettingKeyLoginAgreementDocuments = "login_agreement_documents" // 条款文档列表(JSON,Markdown 内容)
|
||||
|
||||
// 邮件服务设置
|
||||
SettingKeySMTPHost = "smtp_host" // SMTP服务器地址
|
||||
|
||||
@@ -48,8 +48,9 @@ func TestIsRegistrationEmailSuffixAllowed(t *testing.T) {
|
||||
func TestRegistrationEmailQuotaRejectsMalformedDomainWhenWhitelistConfigured(t *testing.T) {
|
||||
repo := &userRepoStub{}
|
||||
svc := newAuthService(repo, map[string]string{
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
|
||||
}, nil, nil)
|
||||
|
||||
_, _, err := svc.Register(context.Background(), "malformed-email", "password")
|
||||
|
||||
@@ -33,6 +33,16 @@ func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
// IsRegistrationEmailDomainQuotaEnabled 检查白名单非空时是否放行非白名单域名限量注册。
|
||||
// 安全默认:设置缺失或查询出错时按关闭处理(保持白名单严格模式)。
|
||||
func (s *SettingService) IsRegistrationEmailDomainQuotaEnabled(ctx context.Context) bool {
|
||||
value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEmailDomainQuotaEnabled)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -58,6 +58,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
SettingKeyRegistrationEnabled: "true",
|
||||
SettingKeyEmailVerifyEnabled: "false",
|
||||
SettingKeyRegistrationEmailSuffixWhitelist: "[]",
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled: "false",
|
||||
SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
|
||||
SettingKeyLoginAgreementEnabled: "false",
|
||||
SettingKeyLoginAgreementMode: defaultLoginAgreementMode,
|
||||
@@ -310,6 +311,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
||||
EmailVerifyEnabled: emailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: ParseRegistrationEmailSuffixWhitelist(settings[SettingKeyRegistrationEmailSuffixWhitelist]),
|
||||
RegistrationEmailDomainQuotaEnabled: settings[SettingKeyRegistrationEmailDomainQuotaEnabled] == "true",
|
||||
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
|
||||
PasswordResetEnabled: emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true",
|
||||
FrontendURL: settings[SettingKeyFrontendURL],
|
||||
|
||||
@@ -160,6 +160,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
SettingKeyEmailVerifyEnabled,
|
||||
SettingKeyForceEmailOnThirdPartySignup,
|
||||
SettingKeyRegistrationEmailSuffixWhitelist,
|
||||
SettingKeyRegistrationEmailDomainQuotaEnabled,
|
||||
SettingKeyPromoCodeEnabled,
|
||||
SettingKeyPasswordResetEnabled,
|
||||
SettingKeyInvitationCodeEnabled,
|
||||
@@ -294,60 +295,61 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
}
|
||||
|
||||
return &PublicSettings{
|
||||
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
||||
EmailVerifyEnabled: emailVerifyEnabled,
|
||||
ForceEmailOnThirdPartySignup: settings[SettingKeyForceEmailOnThirdPartySignup] == "true",
|
||||
RegistrationEmailSuffixWhitelist: registrationEmailSuffixWhitelist,
|
||||
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
|
||||
PasswordResetEnabled: passwordResetEnabled,
|
||||
InvitationCodeEnabled: settings[SettingKeyInvitationCodeEnabled] == "true",
|
||||
TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
|
||||
PasskeyEnabled: s.passkeyConfigured() && s.passkeySettingEnabled(settings),
|
||||
LoginAgreementEnabled: settings[SettingKeyLoginAgreementEnabled] == "true" && len(loginAgreementDocuments) > 0,
|
||||
LoginAgreementMode: normalizeLoginAgreementMode(settings[SettingKeyLoginAgreementMode]),
|
||||
LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
|
||||
LoginAgreementRevision: buildLoginAgreementRevision(loginAgreementUpdatedAt, loginAgreementDocuments),
|
||||
LoginAgreementDocuments: loginAgreementDocuments,
|
||||
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
||||
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
||||
TencentCaptchaEnabled: settings[SettingKeyTencentCaptchaEnabled] == "true",
|
||||
TencentCaptchaAppID: settings[SettingKeyTencentCaptchaAppID],
|
||||
TencentCaptchaRegion: normalizeTencentCaptchaRegion(settings[SettingKeyTencentCaptchaRegion]),
|
||||
AliyunCaptchaEnabled: settings[SettingKeyAliyunCaptchaEnabled] == "true",
|
||||
AliyunCaptchaSceneID: settings[SettingKeyAliyunCaptchaSceneID],
|
||||
AliyunCaptchaPrefix: settings[SettingKeyAliyunCaptchaPrefix],
|
||||
AliyunCaptchaRegion: normalizeAliyunCaptchaRegion(settings[SettingKeyAliyunCaptchaRegion]),
|
||||
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],
|
||||
CompactHomeEnabled: settings[SettingKeyCompactHomeEnabled] == "true",
|
||||
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
||||
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
|
||||
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||
TableDefaultPageSize: tableDefaultPageSize,
|
||||
TablePageSizeOptions: tablePageSizeOptions,
|
||||
CustomMenuItems: settings[SettingKeyCustomMenuItems],
|
||||
CustomEndpoints: settings[SettingKeyCustomEndpoints],
|
||||
LinuxDoOAuthEnabled: linuxDoEnabled,
|
||||
DingTalkOAuthEnabled: dingTalkEnabled,
|
||||
WeChatOAuthEnabled: weChatEnabled,
|
||||
WeChatOAuthOpenEnabled: weChatOpenEnabled,
|
||||
WeChatOAuthMPEnabled: weChatMPEnabled,
|
||||
WeChatOAuthMobileEnabled: weChatMobileEnabled,
|
||||
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
|
||||
PaymentEnabled: settings[SettingPaymentEnabled] == "true",
|
||||
OIDCOAuthEnabled: oidcEnabled,
|
||||
OIDCOAuthProviderName: oidcProviderName,
|
||||
GitHubOAuthEnabled: gitHubEnabled,
|
||||
GoogleOAuthEnabled: googleEnabled,
|
||||
BalanceLowNotifyEnabled: settings[SettingKeyBalanceLowNotifyEnabled] == "true",
|
||||
AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true",
|
||||
BalanceLowNotifyThreshold: balanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings[SettingKeyBalanceLowNotifyRechargeURL],
|
||||
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
||||
EmailVerifyEnabled: emailVerifyEnabled,
|
||||
ForceEmailOnThirdPartySignup: settings[SettingKeyForceEmailOnThirdPartySignup] == "true",
|
||||
RegistrationEmailSuffixWhitelist: registrationEmailSuffixWhitelist,
|
||||
RegistrationEmailDomainQuotaEnabled: settings[SettingKeyRegistrationEmailDomainQuotaEnabled] == "true",
|
||||
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
|
||||
PasswordResetEnabled: passwordResetEnabled,
|
||||
InvitationCodeEnabled: settings[SettingKeyInvitationCodeEnabled] == "true",
|
||||
TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
|
||||
PasskeyEnabled: s.passkeyConfigured() && s.passkeySettingEnabled(settings),
|
||||
LoginAgreementEnabled: settings[SettingKeyLoginAgreementEnabled] == "true" && len(loginAgreementDocuments) > 0,
|
||||
LoginAgreementMode: normalizeLoginAgreementMode(settings[SettingKeyLoginAgreementMode]),
|
||||
LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
|
||||
LoginAgreementRevision: buildLoginAgreementRevision(loginAgreementUpdatedAt, loginAgreementDocuments),
|
||||
LoginAgreementDocuments: loginAgreementDocuments,
|
||||
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
||||
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
||||
TencentCaptchaEnabled: settings[SettingKeyTencentCaptchaEnabled] == "true",
|
||||
TencentCaptchaAppID: settings[SettingKeyTencentCaptchaAppID],
|
||||
TencentCaptchaRegion: normalizeTencentCaptchaRegion(settings[SettingKeyTencentCaptchaRegion]),
|
||||
AliyunCaptchaEnabled: settings[SettingKeyAliyunCaptchaEnabled] == "true",
|
||||
AliyunCaptchaSceneID: settings[SettingKeyAliyunCaptchaSceneID],
|
||||
AliyunCaptchaPrefix: settings[SettingKeyAliyunCaptchaPrefix],
|
||||
AliyunCaptchaRegion: normalizeAliyunCaptchaRegion(settings[SettingKeyAliyunCaptchaRegion]),
|
||||
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],
|
||||
CompactHomeEnabled: settings[SettingKeyCompactHomeEnabled] == "true",
|
||||
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
|
||||
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
|
||||
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||
TableDefaultPageSize: tableDefaultPageSize,
|
||||
TablePageSizeOptions: tablePageSizeOptions,
|
||||
CustomMenuItems: settings[SettingKeyCustomMenuItems],
|
||||
CustomEndpoints: settings[SettingKeyCustomEndpoints],
|
||||
LinuxDoOAuthEnabled: linuxDoEnabled,
|
||||
DingTalkOAuthEnabled: dingTalkEnabled,
|
||||
WeChatOAuthEnabled: weChatEnabled,
|
||||
WeChatOAuthOpenEnabled: weChatOpenEnabled,
|
||||
WeChatOAuthMPEnabled: weChatMPEnabled,
|
||||
WeChatOAuthMobileEnabled: weChatMobileEnabled,
|
||||
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
|
||||
PaymentEnabled: settings[SettingPaymentEnabled] == "true",
|
||||
OIDCOAuthEnabled: oidcEnabled,
|
||||
OIDCOAuthProviderName: oidcProviderName,
|
||||
GitHubOAuthEnabled: gitHubEnabled,
|
||||
GoogleOAuthEnabled: googleEnabled,
|
||||
BalanceLowNotifyEnabled: settings[SettingKeyBalanceLowNotifyEnabled] == "true",
|
||||
AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true",
|
||||
BalanceLowNotifyThreshold: balanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings[SettingKeyBalanceLowNotifyRechargeURL],
|
||||
|
||||
ChannelMonitorEnabled: !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled]),
|
||||
ChannelMonitorMode: normalizeChannelMonitorMode(settings[SettingKeyChannelMonitorMode]),
|
||||
@@ -536,56 +538,57 @@ func (s *SettingService) IsUserErrorViewAllowed(ctx context.Context) bool {
|
||||
// A unit test diffs this struct's JSON keys against dto.PublicSettings to catch
|
||||
// drift automatically (see setting_service_injection_test.go).
|
||||
type PublicSettingsInjectionPayload struct {
|
||||
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"`
|
||||
PasskeyEnabled bool `json:"passkey_enabled"`
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementRevision string `json:"login_agreement_revision"`
|
||||
LoginAgreementDocuments []LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
TencentCaptchaRegion string `json:"tencent_captcha_region"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
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"`
|
||||
CompactHomeEnabled bool `json:"compact_home_enabled"`
|
||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||
TableDefaultPageSize int `json:"table_default_page_size"`
|
||||
TablePageSizeOptions []int `json:"table_page_size_options"`
|
||||
CustomMenuItems json.RawMessage `json:"custom_menu_items"`
|
||||
CustomEndpoints json.RawMessage `json:"custom_endpoints"`
|
||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||
DingTalkOAuthEnabled bool `json:"dingtalk_oauth_enabled"`
|
||||
WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"`
|
||||
WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"`
|
||||
WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"`
|
||||
WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"`
|
||||
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
||||
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
||||
GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
|
||||
GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
PaymentEnabled bool `json:"payment_enabled"`
|
||||
Version string `json:"version"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
||||
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
|
||||
RegistrationEmailDomainQuotaEnabled bool `json:"registration_email_domain_quota_enabled"`
|
||||
PromoCodeEnabled bool `json:"promo_code_enabled"`
|
||||
PasswordResetEnabled bool `json:"password_reset_enabled"`
|
||||
InvitationCodeEnabled bool `json:"invitation_code_enabled"`
|
||||
TotpEnabled bool `json:"totp_enabled"`
|
||||
PasskeyEnabled bool `json:"passkey_enabled"`
|
||||
LoginAgreementEnabled bool `json:"login_agreement_enabled"`
|
||||
LoginAgreementMode string `json:"login_agreement_mode"`
|
||||
LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
|
||||
LoginAgreementRevision string `json:"login_agreement_revision"`
|
||||
LoginAgreementDocuments []LoginAgreementDocument `json:"login_agreement_documents"`
|
||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
TencentCaptchaRegion string `json:"tencent_captcha_region"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
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"`
|
||||
CompactHomeEnabled bool `json:"compact_home_enabled"`
|
||||
HideCcsImportButton bool `json:"hide_ccs_import_button"`
|
||||
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||
TableDefaultPageSize int `json:"table_default_page_size"`
|
||||
TablePageSizeOptions []int `json:"table_page_size_options"`
|
||||
CustomMenuItems json.RawMessage `json:"custom_menu_items"`
|
||||
CustomEndpoints json.RawMessage `json:"custom_endpoints"`
|
||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||
DingTalkOAuthEnabled bool `json:"dingtalk_oauth_enabled"`
|
||||
WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"`
|
||||
WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"`
|
||||
WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"`
|
||||
WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"`
|
||||
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
||||
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
||||
GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
|
||||
GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
PaymentEnabled bool `json:"payment_enabled"`
|
||||
Version string `json:"version"`
|
||||
// 服务器全局时区(IANA 名称与当前 UTC 偏移),高峰时段等服务端本地时间窗口的展示标注用
|
||||
ServerTimezone string `json:"server_timezone"`
|
||||
ServerUTCOffset string `json:"server_utc_offset"`
|
||||
@@ -620,62 +623,63 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
||||
}
|
||||
|
||||
return &PublicSettingsInjectionPayload{
|
||||
RegistrationEnabled: settings.RegistrationEnabled,
|
||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
||||
TotpEnabled: settings.TotpEnabled,
|
||||
PasskeyEnabled: settings.PasskeyEnabled,
|
||||
LoginAgreementEnabled: settings.LoginAgreementEnabled,
|
||||
LoginAgreementMode: settings.LoginAgreementMode,
|
||||
LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
|
||||
LoginAgreementRevision: settings.LoginAgreementRevision,
|
||||
LoginAgreementDocuments: settings.LoginAgreementDocuments,
|
||||
TurnstileEnabled: settings.TurnstileEnabled,
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
TencentCaptchaRegion: settings.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
SiteName: settings.SiteName,
|
||||
SiteLogo: settings.SiteLogo,
|
||||
SiteSubtitle: settings.SiteSubtitle,
|
||||
APIBaseURL: settings.APIBaseURL,
|
||||
ContactInfo: settings.ContactInfo,
|
||||
DocURL: settings.DocURL,
|
||||
HomeContent: settings.HomeContent,
|
||||
CompactHomeEnabled: settings.CompactHomeEnabled,
|
||||
HideCcsImportButton: settings.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
TableDefaultPageSize: settings.TableDefaultPageSize,
|
||||
TablePageSizeOptions: settings.TablePageSizeOptions,
|
||||
CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints),
|
||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||
DingTalkOAuthEnabled: settings.DingTalkOAuthEnabled,
|
||||
WeChatOAuthEnabled: settings.WeChatOAuthEnabled,
|
||||
WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled,
|
||||
WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled,
|
||||
WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled,
|
||||
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
||||
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
||||
GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
|
||||
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
PaymentEnabled: settings.PaymentEnabled,
|
||||
Version: s.version,
|
||||
ServerTimezone: timezone.Name(),
|
||||
ServerUTCOffset: timezone.UTCOffset(),
|
||||
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
||||
RegistrationEnabled: settings.RegistrationEnabled,
|
||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
||||
RegistrationEmailDomainQuotaEnabled: settings.RegistrationEmailDomainQuotaEnabled,
|
||||
PromoCodeEnabled: settings.PromoCodeEnabled,
|
||||
PasswordResetEnabled: settings.PasswordResetEnabled,
|
||||
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
||||
TotpEnabled: settings.TotpEnabled,
|
||||
PasskeyEnabled: settings.PasskeyEnabled,
|
||||
LoginAgreementEnabled: settings.LoginAgreementEnabled,
|
||||
LoginAgreementMode: settings.LoginAgreementMode,
|
||||
LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
|
||||
LoginAgreementRevision: settings.LoginAgreementRevision,
|
||||
LoginAgreementDocuments: settings.LoginAgreementDocuments,
|
||||
TurnstileEnabled: settings.TurnstileEnabled,
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
TencentCaptchaRegion: settings.TencentCaptchaRegion,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
SiteName: settings.SiteName,
|
||||
SiteLogo: settings.SiteLogo,
|
||||
SiteSubtitle: settings.SiteSubtitle,
|
||||
APIBaseURL: settings.APIBaseURL,
|
||||
ContactInfo: settings.ContactInfo,
|
||||
DocURL: settings.DocURL,
|
||||
HomeContent: settings.HomeContent,
|
||||
CompactHomeEnabled: settings.CompactHomeEnabled,
|
||||
HideCcsImportButton: settings.HideCcsImportButton,
|
||||
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
TableDefaultPageSize: settings.TableDefaultPageSize,
|
||||
TablePageSizeOptions: settings.TablePageSizeOptions,
|
||||
CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints),
|
||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||
DingTalkOAuthEnabled: settings.DingTalkOAuthEnabled,
|
||||
WeChatOAuthEnabled: settings.WeChatOAuthEnabled,
|
||||
WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled,
|
||||
WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled,
|
||||
WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled,
|
||||
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
||||
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
||||
GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
|
||||
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
PaymentEnabled: settings.PaymentEnabled,
|
||||
Version: s.version,
|
||||
ServerTimezone: timezone.Name(),
|
||||
ServerUTCOffset: timezone.UTCOffset(),
|
||||
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
||||
|
||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||
ChannelMonitorMode: settings.ChannelMonitorMode,
|
||||
|
||||
@@ -169,6 +169,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
||||
return nil, fmt.Errorf("marshal registration email suffix whitelist: %w", err)
|
||||
}
|
||||
updates[SettingKeyRegistrationEmailSuffixWhitelist] = string(registrationEmailSuffixWhitelistJSON)
|
||||
updates[SettingKeyRegistrationEmailDomainQuotaEnabled] = strconv.FormatBool(settings.RegistrationEmailDomainQuotaEnabled)
|
||||
updates[SettingKeyPromoCodeEnabled] = strconv.FormatBool(settings.PromoCodeEnabled)
|
||||
updates[SettingKeyPasswordResetEnabled] = strconv.FormatBool(settings.PasswordResetEnabled)
|
||||
updates[SettingKeyFrontendURL] = settings.FrontendURL
|
||||
|
||||
@@ -12,22 +12,23 @@ func firstNonEmpty(values ...string) string {
|
||||
}
|
||||
|
||||
type SystemSettings struct {
|
||||
RegistrationEnabled bool
|
||||
EmailVerifyEnabled bool
|
||||
RegistrationEmailSuffixWhitelist []string
|
||||
PromoCodeEnabled bool
|
||||
PasswordResetEnabled bool
|
||||
FrontendURL string
|
||||
InvitationCodeEnabled bool
|
||||
TotpEnabled bool // TOTP 双因素认证
|
||||
PasskeyEnabled bool // Passkey 登录
|
||||
SessionBindingEnabled bool // 会话 IP/UA 绑定(变更即失效)
|
||||
StepUpEnabled bool // 敏感操作 step-up 2FA 门控
|
||||
AuditLogRetentionDays int // 审计日志保留天数(<=0 永久保留)
|
||||
LoginAgreementEnabled bool
|
||||
LoginAgreementMode string
|
||||
LoginAgreementUpdatedAt string
|
||||
LoginAgreementDocuments []LoginAgreementDocument
|
||||
RegistrationEnabled bool
|
||||
EmailVerifyEnabled bool
|
||||
RegistrationEmailSuffixWhitelist []string
|
||||
RegistrationEmailDomainQuotaEnabled bool // 白名单非空时放行非白名单域名限量注册(默认关闭)
|
||||
PromoCodeEnabled bool
|
||||
PasswordResetEnabled bool
|
||||
FrontendURL string
|
||||
InvitationCodeEnabled bool
|
||||
TotpEnabled bool // TOTP 双因素认证
|
||||
PasskeyEnabled bool // Passkey 登录
|
||||
SessionBindingEnabled bool // 会话 IP/UA 绑定(变更即失效)
|
||||
StepUpEnabled bool // 敏感操作 step-up 2FA 门控
|
||||
AuditLogRetentionDays int // 审计日志保留天数(<=0 永久保留)
|
||||
LoginAgreementEnabled bool
|
||||
LoginAgreementMode string
|
||||
LoginAgreementUpdatedAt string
|
||||
LoginAgreementDocuments []LoginAgreementDocument
|
||||
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
@@ -312,38 +313,39 @@ type DefaultSubscriptionSetting struct {
|
||||
}
|
||||
|
||||
type PublicSettings struct {
|
||||
RegistrationEnabled bool
|
||||
EmailVerifyEnabled bool
|
||||
ForceEmailOnThirdPartySignup bool
|
||||
RegistrationEmailSuffixWhitelist []string
|
||||
PromoCodeEnabled bool
|
||||
PasswordResetEnabled bool
|
||||
InvitationCodeEnabled bool
|
||||
TotpEnabled bool // TOTP 双因素认证
|
||||
PasskeyEnabled bool
|
||||
LoginAgreementEnabled bool
|
||||
LoginAgreementMode string
|
||||
LoginAgreementUpdatedAt string
|
||||
LoginAgreementRevision string
|
||||
LoginAgreementDocuments []LoginAgreementDocument
|
||||
TurnstileEnabled bool
|
||||
TurnstileSiteKey string
|
||||
TencentCaptchaEnabled bool
|
||||
TencentCaptchaAppID string
|
||||
TencentCaptchaRegion string
|
||||
AliyunCaptchaEnabled bool
|
||||
AliyunCaptchaSceneID string
|
||||
AliyunCaptchaPrefix string
|
||||
AliyunCaptchaRegion string
|
||||
SiteName string
|
||||
SiteLogo string
|
||||
SiteSubtitle string
|
||||
APIBaseURL string
|
||||
ContactInfo string
|
||||
DocURL string
|
||||
HomeContent string
|
||||
CompactHomeEnabled bool
|
||||
HideCcsImportButton bool
|
||||
RegistrationEnabled bool
|
||||
EmailVerifyEnabled bool
|
||||
ForceEmailOnThirdPartySignup bool
|
||||
RegistrationEmailSuffixWhitelist []string
|
||||
RegistrationEmailDomainQuotaEnabled bool
|
||||
PromoCodeEnabled bool
|
||||
PasswordResetEnabled bool
|
||||
InvitationCodeEnabled bool
|
||||
TotpEnabled bool // TOTP 双因素认证
|
||||
PasskeyEnabled bool
|
||||
LoginAgreementEnabled bool
|
||||
LoginAgreementMode string
|
||||
LoginAgreementUpdatedAt string
|
||||
LoginAgreementRevision string
|
||||
LoginAgreementDocuments []LoginAgreementDocument
|
||||
TurnstileEnabled bool
|
||||
TurnstileSiteKey string
|
||||
TencentCaptchaEnabled bool
|
||||
TencentCaptchaAppID string
|
||||
TencentCaptchaRegion string
|
||||
AliyunCaptchaEnabled bool
|
||||
AliyunCaptchaSceneID string
|
||||
AliyunCaptchaPrefix string
|
||||
AliyunCaptchaRegion string
|
||||
SiteName string
|
||||
SiteLogo string
|
||||
SiteSubtitle string
|
||||
APIBaseURL string
|
||||
ContactInfo string
|
||||
DocURL string
|
||||
HomeContent string
|
||||
CompactHomeEnabled bool
|
||||
HideCcsImportButton bool
|
||||
|
||||
PurchaseSubscriptionEnabled bool
|
||||
PurchaseSubscriptionURL string
|
||||
|
||||
@@ -392,6 +392,7 @@ export interface SystemSettings {
|
||||
registration_enabled: boolean;
|
||||
email_verify_enabled: boolean;
|
||||
registration_email_suffix_whitelist: string[];
|
||||
registration_email_domain_quota_enabled: boolean;
|
||||
promo_code_enabled: boolean;
|
||||
password_reset_enabled: boolean;
|
||||
frontend_url: string;
|
||||
@@ -734,6 +735,7 @@ export interface UpdateSettingsRequest {
|
||||
registration_enabled?: boolean;
|
||||
email_verify_enabled?: boolean;
|
||||
registration_email_suffix_whitelist?: string[];
|
||||
registration_email_domain_quota_enabled?: boolean;
|
||||
promo_code_enabled?: boolean;
|
||||
password_reset_enabled?: boolean;
|
||||
frontend_url?: string;
|
||||
|
||||
@@ -128,9 +128,12 @@ export default {
|
||||
emailVerificationHint: 'Require email verification for new registrations',
|
||||
emailSuffixWhitelist: 'Email Domain Whitelist',
|
||||
emailSuffixWhitelistHint:
|
||||
"Emails from allowlist domains can register without a quota. When the allowlist is not empty, every other registrable domain can register one account. Empty the allowlist to remove the quota for all domains (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn).",
|
||||
"Only email addresses from the specified domains can register; leave empty for no restriction (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)",
|
||||
emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn",
|
||||
emailSuffixWhitelistInputHint: 'Empty the allowlist to remove the registration quota. Use *.edu.cn to match edu.cn and its subdomains.',
|
||||
emailSuffixWhitelistInputHint: 'Leave empty for no restriction. Use *.edu.cn to match edu.cn and its subdomains.',
|
||||
emailDomainQuota: 'Non-allowlist Domain Quota',
|
||||
emailDomainQuotaHint:
|
||||
'When enabled and the allowlist is not empty, every other registrable domain can register one account. When disabled, non-allowlist domains are rejected. Has no effect while the allowlist is empty',
|
||||
promoCode: 'Promo Code',
|
||||
promoCodeHint: 'Allow users to use promo codes during registration',
|
||||
invitationCode: 'Invitation Code Registration',
|
||||
|
||||
@@ -128,9 +128,12 @@ export default {
|
||||
emailVerificationHint: '新用户注册时需要验证邮箱',
|
||||
emailSuffixWhitelist: '邮箱域名白名单',
|
||||
emailSuffixWhitelistHint:
|
||||
"白名单域名的邮箱可无限注册;白名单非空时,其他可注册主域名各限注册一个账户。清空白名单后,所有域名均不限制注册数量(例如 {'@'}qq.com, {'@'}gmail.com, *.edu.cn)",
|
||||
"仅允许使用指定域名的邮箱注册账号;留空则不限制(例如 {'@'}qq.com, {'@'}gmail.com, *.edu.cn)",
|
||||
emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn",
|
||||
emailSuffixWhitelistInputHint: '清空白名单后不限制注册数量。使用 *.edu.cn 可匹配 edu.cn 及其子域名。',
|
||||
emailSuffixWhitelistInputHint: '留空则不限制。使用 *.edu.cn 可匹配 edu.cn 及其子域名。',
|
||||
emailDomainQuota: '非白名单域名限量注册',
|
||||
emailDomainQuotaHint:
|
||||
'开启后,白名单非空时,其他可注册主域名各限注册一个账户;关闭时非白名单域名直接拒绝注册。白名单为空时本开关无效果',
|
||||
promoCode: '优惠码',
|
||||
promoCodeHint: '允许用户在注册时使用优惠码',
|
||||
invitationCode: '邀请码注册',
|
||||
|
||||
@@ -209,6 +209,7 @@ export interface PublicSettings {
|
||||
email_verify_enabled: boolean
|
||||
force_email_on_third_party_signup: boolean
|
||||
registration_email_suffix_whitelist: string[]
|
||||
registration_email_domain_quota_enabled?: boolean
|
||||
promo_code_enabled: boolean
|
||||
password_reset_enabled: boolean
|
||||
invitation_code_enabled: boolean
|
||||
|
||||
@@ -1486,6 +1486,23 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Email Domain Quota -->
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t("admin.settings.registration.emailDomainQuota")
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.registration.emailDomainQuotaHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
v-model="form.registration_email_domain_quota_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Promo Code -->
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
@@ -9401,6 +9418,7 @@ const form = reactive<SettingsForm>({
|
||||
registration_enabled: true,
|
||||
email_verify_enabled: false,
|
||||
registration_email_suffix_whitelist: [],
|
||||
registration_email_domain_quota_enabled: false,
|
||||
promo_code_enabled: true,
|
||||
invitation_code_enabled: false,
|
||||
password_reset_enabled: false,
|
||||
@@ -11023,6 +11041,8 @@ async function saveSettings() {
|
||||
registrationEmailSuffixWhitelistTags.value.map((suffix) =>
|
||||
suffix.startsWith("*.") ? suffix : `@${suffix}`,
|
||||
),
|
||||
registration_email_domain_quota_enabled:
|
||||
form.registration_email_domain_quota_enabled,
|
||||
promo_code_enabled: form.promo_code_enabled,
|
||||
invitation_code_enabled: form.invitation_code_enabled,
|
||||
password_reset_enabled: form.password_reset_enabled,
|
||||
|
||||
@@ -605,12 +605,18 @@ describe("admin SettingsView email domain quota copy", () => {
|
||||
expect(enCommon.auth.emailDomainRegistrationLimit).toContain("mainstream email");
|
||||
expect(enCommon.auth.emailDomainRegistrationLimit).toContain("contact support");
|
||||
|
||||
const zhHint = zhSettings.settings.registration.emailSuffixWhitelistHint;
|
||||
const enHint = enSettings.settings.registration.emailSuffixWhitelistHint;
|
||||
expect(zhHint).toContain("其他可注册主域名各限注册一个账户");
|
||||
expect(zhHint).toContain("清空白名单");
|
||||
expect(enHint).toContain("one account");
|
||||
expect(enHint).toContain("empty");
|
||||
// 白名单 hint 描述严格默认语义;额度语义移入独立开关的 hint。
|
||||
const zhWhitelistHint = zhSettings.settings.registration.emailSuffixWhitelistHint;
|
||||
const enWhitelistHint = enSettings.settings.registration.emailSuffixWhitelistHint;
|
||||
expect(zhWhitelistHint).toContain("留空则不限制");
|
||||
expect(enWhitelistHint).toContain("leave empty for no restriction");
|
||||
|
||||
const zhQuotaHint = zhSettings.settings.registration.emailDomainQuotaHint;
|
||||
const enQuotaHint = enSettings.settings.registration.emailDomainQuotaHint;
|
||||
expect(zhQuotaHint).toContain("其他可注册主域名各限注册一个账户");
|
||||
expect(zhQuotaHint).toContain("关闭时非白名单域名直接拒绝");
|
||||
expect(enQuotaHint).toContain("one account");
|
||||
expect(enQuotaHint).toContain("When disabled");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -196,13 +196,18 @@ import {
|
||||
import { apiClient } from '@/api/client'
|
||||
import { buildAuthErrorMessage } from '@/utils/authError'
|
||||
import { extractApiErrorCode } from '@/utils/apiError'
|
||||
import {
|
||||
formatRegistrationEmailSuffixWhitelistForMessage,
|
||||
isRegistrationEmailSuffixAllowed,
|
||||
normalizeRegistrationEmailSuffixWhitelist
|
||||
} from '@/utils/registrationEmailPolicy'
|
||||
import {
|
||||
clearAllAffiliateReferralCodes,
|
||||
loadAffiliateReferralCode,
|
||||
oauthAffiliatePayload
|
||||
} from '@/utils/oauthAffiliate'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
// ==================== Router & Stores ====================
|
||||
|
||||
@@ -266,6 +271,9 @@ const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
const aliyunCaptchaRegion = ref<string>('cn')
|
||||
const siteName = ref<string>('Sub2API')
|
||||
const registrationEmailSuffixWhitelist = ref<string[]>([])
|
||||
// 域名限量注册开关:开启时非白名单域名可注册 1 个账户(由后端判定),前端不做白名单预检。
|
||||
const emailDomainQuotaEnabled = ref<boolean>(false)
|
||||
|
||||
// Turnstile for resend
|
||||
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
|
||||
@@ -365,6 +373,10 @@ onMounted(async () => {
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
aliyunCaptchaRegion.value = settings.aliyun_captcha_region || 'cn'
|
||||
siteName.value = settings.site_name || 'Sub2API'
|
||||
registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist(
|
||||
settings.registration_email_suffix_whitelist || []
|
||||
)
|
||||
emailDomainQuotaEnabled.value = settings.registration_email_domain_quota_enabled === true
|
||||
} catch (error) {
|
||||
console.error('Failed to load public settings:', error)
|
||||
}
|
||||
@@ -473,6 +485,13 @@ function isPendingOAuthFlow(): boolean {
|
||||
return Boolean(pendingProvider.value.trim())
|
||||
}
|
||||
|
||||
// 域名限量注册开启时交由后端按额度判定;pending OAuth / 换绑流程沿用后端策略,前端不预检。
|
||||
function shouldBypassRegistrationEmailPolicy(): boolean {
|
||||
return (
|
||||
emailDomainQuotaEnabled.value || isPendingOAuthFlow() || Boolean(pendingAuthToken.value.trim())
|
||||
)
|
||||
}
|
||||
|
||||
function resolvePendingOAuthCallbackRoute(provider: string): string {
|
||||
switch (provider.trim().toLowerCase()) {
|
||||
case 'linuxdo':
|
||||
@@ -514,6 +533,12 @@ async function sendCode(): Promise<void> {
|
||||
let captchaProofUsed = false
|
||||
|
||||
try {
|
||||
if (!shouldBypassRegistrationEmailPolicy() && !isRegistrationEmailSuffixAllowed(email.value, registrationEmailSuffixWhitelist.value)) {
|
||||
errorMessage.value = buildEmailSuffixNotAllowedMessage()
|
||||
appStore.showError(errorMessage.value)
|
||||
return
|
||||
}
|
||||
|
||||
const requestPayload = {
|
||||
email: email.value,
|
||||
[pendingAuthTokenField.value]: pendingAuthToken.value || undefined,
|
||||
@@ -635,6 +660,12 @@ async function handleVerify(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (!shouldBypassRegistrationEmailPolicy() && !isRegistrationEmailSuffixAllowed(email.value, registrationEmailSuffixWhitelist.value)) {
|
||||
errorMessage.value = buildEmailSuffixNotAllowedMessage()
|
||||
appStore.showError(errorMessage.value)
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireCreateAccountActionProof())) {
|
||||
return
|
||||
}
|
||||
@@ -735,6 +766,22 @@ function handleBack(): void {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
function buildEmailSuffixNotAllowedMessage(): string {
|
||||
const normalizedWhitelist = normalizeRegistrationEmailSuffixWhitelist(
|
||||
registrationEmailSuffixWhitelist.value
|
||||
)
|
||||
if (normalizedWhitelist.length === 0) {
|
||||
return t('auth.emailSuffixNotAllowed')
|
||||
}
|
||||
const separator = String(locale.value || '').toLowerCase().startsWith('zh') ? '、' : ', '
|
||||
return t('auth.emailSuffixNotAllowedWithAllowed', {
|
||||
suffixes: formatRegistrationEmailSuffixWhitelistForMessage(normalizedWhitelist, {
|
||||
separator,
|
||||
more: (count) => t('auth.emailSuffixAllowedMore', { count })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function buildRegistrationErrorMessage(error: unknown, fallback: string): string {
|
||||
if (extractApiErrorCode(error) === 'EMAIL_DOMAIN_REGISTRATION_LIMIT') {
|
||||
return t('auth.emailDomainRegistrationLimit')
|
||||
|
||||
@@ -354,6 +354,11 @@ import {
|
||||
} from '@/api/auth'
|
||||
import { buildAuthErrorMessage } from '@/utils/authError'
|
||||
import { extractApiErrorCode, extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import {
|
||||
formatRegistrationEmailSuffixWhitelistForMessage,
|
||||
isRegistrationEmailSuffixAllowed,
|
||||
normalizeRegistrationEmailSuffixWhitelist
|
||||
} from '@/utils/registrationEmailPolicy'
|
||||
import {
|
||||
clearAffiliateReferralCode,
|
||||
loadAffiliateReferralCode,
|
||||
@@ -361,7 +366,7 @@ import {
|
||||
} from '@/utils/oauthAffiliate'
|
||||
import type { LoginAgreementDocument } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const LOGIN_AGREEMENT_STORAGE_KEY = 'sub2api_login_agreement_consent'
|
||||
|
||||
// ==================== Router & Stores ====================
|
||||
@@ -400,6 +405,9 @@ const oidcOAuthEnabled = ref<boolean>(false)
|
||||
const oidcOAuthProviderName = ref<string>('OIDC')
|
||||
const githubOAuthEnabled = ref<boolean>(false)
|
||||
const googleOAuthEnabled = ref<boolean>(false)
|
||||
const registrationEmailSuffixWhitelist = ref<string[]>([])
|
||||
// 域名限量注册开关:开启时非白名单域名可注册 1 个账户(由后端判定),前端不做白名单预检。
|
||||
const emailDomainQuotaEnabled = ref<boolean>(false)
|
||||
const loginAgreementEnabled = ref<boolean>(false)
|
||||
const loginAgreementMode = ref<'modal' | 'checkbox' | string>('modal')
|
||||
const loginAgreementUpdatedAt = ref<string>('')
|
||||
@@ -532,6 +540,10 @@ onMounted(async () => {
|
||||
oidcOAuthProviderName.value = settings.oidc_oauth_provider_name || 'OIDC'
|
||||
githubOAuthEnabled.value = settings.github_oauth_enabled
|
||||
googleOAuthEnabled.value = settings.google_oauth_enabled
|
||||
registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist(
|
||||
settings.registration_email_suffix_whitelist || []
|
||||
)
|
||||
emailDomainQuotaEnabled.value = settings.registration_email_domain_quota_enabled === true
|
||||
applyLoginAgreementSettings(settings)
|
||||
|
||||
// Read promo code from URL parameter only if promo code is enabled
|
||||
@@ -850,6 +862,22 @@ function validateEmail(email: string): boolean {
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
function buildEmailSuffixNotAllowedMessage(): string {
|
||||
const normalizedWhitelist = normalizeRegistrationEmailSuffixWhitelist(
|
||||
registrationEmailSuffixWhitelist.value
|
||||
)
|
||||
if (normalizedWhitelist.length === 0) {
|
||||
return t('auth.emailSuffixNotAllowed')
|
||||
}
|
||||
const separator = String(locale.value || '').toLowerCase().startsWith('zh') ? '、' : ', '
|
||||
return t('auth.emailSuffixNotAllowedWithAllowed', {
|
||||
suffixes: formatRegistrationEmailSuffixWhitelistForMessage(normalizedWhitelist, {
|
||||
separator,
|
||||
more: (count) => t('auth.emailSuffixAllowedMore', { count })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
// Reset errors
|
||||
errors.email = ''
|
||||
@@ -874,6 +902,13 @@ function validateForm(): boolean {
|
||||
} else if (!validateEmail(formData.email)) {
|
||||
errors.email = t('auth.invalidEmail')
|
||||
isValid = false
|
||||
} else if (
|
||||
!emailDomainQuotaEnabled.value &&
|
||||
!isRegistrationEmailSuffixAllowed(formData.email, registrationEmailSuffixWhitelist.value)
|
||||
) {
|
||||
// 域名限量注册关闭时保持严格白名单预检;开启时交给后端按域名额度判定
|
||||
errors.email = buildEmailSuffixNotAllowedMessage()
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Password validation
|
||||
|
||||
@@ -316,6 +316,7 @@ describe('EmailVerifyView', () => {
|
||||
turnstile_site_key: '',
|
||||
site_name: 'Sub2API',
|
||||
registration_email_suffix_whitelist: ['allowed.com'],
|
||||
registration_email_domain_quota_enabled: true,
|
||||
})
|
||||
sessionStorage.setItem(
|
||||
'register_data',
|
||||
@@ -350,6 +351,7 @@ describe('EmailVerifyView', () => {
|
||||
turnstile_site_key: '',
|
||||
site_name: 'Sub2API',
|
||||
registration_email_suffix_whitelist: ['allowed.com'],
|
||||
registration_email_domain_quota_enabled: true,
|
||||
})
|
||||
sendVerifyCodeMock.mockRejectedValueOnce({
|
||||
reason: 'EMAIL_DOMAIN_REGISTRATION_LIMIT',
|
||||
@@ -387,6 +389,7 @@ describe('EmailVerifyView', () => {
|
||||
turnstile_site_key: '',
|
||||
site_name: 'Sub2API',
|
||||
registration_email_suffix_whitelist: ['allowed.com'],
|
||||
registration_email_domain_quota_enabled: true,
|
||||
})
|
||||
sessionStorage.setItem(
|
||||
'register_data',
|
||||
@@ -422,6 +425,39 @@ describe('EmailVerifyView', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// 域名限量注册开关默认关闭:恢复 PR5423 之前的客户端白名单预检,非白名单域名不发送验证码。
|
||||
it('blocks sending a verification code for a non-whitelist email domain when the quota switch is disabled', async () => {
|
||||
getPublicSettingsMock.mockResolvedValue({
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: '',
|
||||
site_name: 'Sub2API',
|
||||
registration_email_suffix_whitelist: ['allowed.com'],
|
||||
})
|
||||
sessionStorage.setItem(
|
||||
'register_data',
|
||||
JSON.stringify({
|
||||
email: 'first@custom.example',
|
||||
password: 'secret-123',
|
||||
})
|
||||
)
|
||||
|
||||
mount(EmailVerifyView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AuthLayout: { template: '<div><slot /><slot name="footer" /></div>' },
|
||||
Icon: true,
|
||||
TurnstileWidget: true,
|
||||
transition: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(sendVerifyCodeMock).not.toHaveBeenCalled()
|
||||
expect(showErrorMock).toHaveBeenCalledWith('auth.emailSuffixNotAllowedWithAllowed')
|
||||
})
|
||||
|
||||
it('uses the pending oauth verify-code endpoint when auth store only carries the pending provider', async () => {
|
||||
authStoreState.pendingAuthSession = {
|
||||
token: '',
|
||||
|
||||
@@ -122,7 +122,8 @@ describe('RegisterView invitation layout', () => {
|
||||
getPublicSettingsMock.mockResolvedValueOnce({
|
||||
...publicSettings,
|
||||
turnstile_enabled: false,
|
||||
registration_email_suffix_whitelist: ['allowed.com']
|
||||
registration_email_suffix_whitelist: ['allowed.com'],
|
||||
registration_email_domain_quota_enabled: true
|
||||
})
|
||||
|
||||
const wrapper = mountRegister()
|
||||
@@ -142,7 +143,8 @@ describe('RegisterView invitation layout', () => {
|
||||
getPublicSettingsMock.mockResolvedValueOnce({
|
||||
...publicSettings,
|
||||
turnstile_enabled: false,
|
||||
registration_email_suffix_whitelist: ['allowed.com']
|
||||
registration_email_suffix_whitelist: ['allowed.com'],
|
||||
registration_email_domain_quota_enabled: true
|
||||
})
|
||||
registerMock.mockRejectedValueOnce({
|
||||
reason: 'EMAIL_DOMAIN_REGISTRATION_LIMIT',
|
||||
@@ -160,4 +162,45 @@ describe('RegisterView invitation layout', () => {
|
||||
'该邮箱域名无法注册新账户。请使用主流邮箱注册;如需使用企业邮箱,请联系客服添加域名白名单。'
|
||||
)
|
||||
})
|
||||
|
||||
// 域名限量注册开关默认关闭:恢复 PR5423 之前的客户端白名单预检,非白名单域名不发起注册请求。
|
||||
it('rejects a non-whitelist email domain locally when the domain quota switch is disabled', async () => {
|
||||
getPublicSettingsMock.mockResolvedValueOnce({
|
||||
...publicSettings,
|
||||
turnstile_enabled: false,
|
||||
registration_email_suffix_whitelist: ['allowed.com']
|
||||
})
|
||||
|
||||
const wrapper = mountRegister()
|
||||
await flushPromises()
|
||||
await wrapper.get('#email').setValue('first@custom.example')
|
||||
await wrapper.get('#password').setValue('secret-123')
|
||||
await wrapper.get('form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(registerMock).not.toHaveBeenCalled()
|
||||
// 校验失败通过 validationToastMessage watcher 弹 toast
|
||||
expect(showErrorMock).toHaveBeenCalledWith('auth.emailSuffixNotAllowedWithAllowed')
|
||||
expect(wrapper.get('#email').classes()).toContain('input-error')
|
||||
})
|
||||
|
||||
it('still submits whitelisted email domains when the domain quota switch is disabled', async () => {
|
||||
getPublicSettingsMock.mockResolvedValueOnce({
|
||||
...publicSettings,
|
||||
turnstile_enabled: false,
|
||||
registration_email_suffix_whitelist: ['allowed.com']
|
||||
})
|
||||
|
||||
const wrapper = mountRegister()
|
||||
await flushPromises()
|
||||
await wrapper.get('#email').setValue('user@allowed.com')
|
||||
await wrapper.get('#password').setValue('secret-123')
|
||||
await wrapper.get('form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(registerMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ email: 'user@allowed.com' })
|
||||
)
|
||||
expect(showErrorMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user