Commit ae44a943 authored by shaw's avatar shaw
Browse files

fix: 重置密码功能新增UI配置发送邮件域名

parent 8321e4a6
...@@ -80,6 +80,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { ...@@ -80,6 +80,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist, RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
PromoCodeEnabled: settings.PromoCodeEnabled, PromoCodeEnabled: settings.PromoCodeEnabled,
PasswordResetEnabled: settings.PasswordResetEnabled, PasswordResetEnabled: settings.PasswordResetEnabled,
FrontendURL: settings.FrontendURL,
InvitationCodeEnabled: settings.InvitationCodeEnabled, InvitationCodeEnabled: settings.InvitationCodeEnabled,
TotpEnabled: settings.TotpEnabled, TotpEnabled: settings.TotpEnabled,
TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
...@@ -137,6 +138,7 @@ type UpdateSettingsRequest struct { ...@@ -137,6 +138,7 @@ type UpdateSettingsRequest struct {
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"` RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
PromoCodeEnabled bool `json:"promo_code_enabled"` PromoCodeEnabled bool `json:"promo_code_enabled"`
PasswordResetEnabled bool `json:"password_reset_enabled"` PasswordResetEnabled bool `json:"password_reset_enabled"`
FrontendURL string `json:"frontend_url"`
InvitationCodeEnabled bool `json:"invitation_code_enabled"` InvitationCodeEnabled bool `json:"invitation_code_enabled"`
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证 TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
...@@ -326,6 +328,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -326,6 +328,15 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
} }
} }
// Frontend URL 验证
req.FrontendURL = strings.TrimSpace(req.FrontendURL)
if req.FrontendURL != "" {
if err := config.ValidateAbsoluteHTTPURL(req.FrontendURL); err != nil {
response.BadRequest(c, "Frontend URL must be an absolute http(s) URL")
return
}
}
// 自定义菜单项验证 // 自定义菜单项验证
const ( const (
maxCustomMenuItems = 20 maxCustomMenuItems = 20
...@@ -437,6 +448,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -437,6 +448,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist, RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
PromoCodeEnabled: req.PromoCodeEnabled, PromoCodeEnabled: req.PromoCodeEnabled,
PasswordResetEnabled: req.PasswordResetEnabled, PasswordResetEnabled: req.PasswordResetEnabled,
FrontendURL: req.FrontendURL,
InvitationCodeEnabled: req.InvitationCodeEnabled, InvitationCodeEnabled: req.InvitationCodeEnabled,
TotpEnabled: req.TotpEnabled, TotpEnabled: req.TotpEnabled,
SMTPHost: req.SMTPHost, SMTPHost: req.SMTPHost,
...@@ -531,6 +543,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -531,6 +543,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist, RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
PromoCodeEnabled: updatedSettings.PromoCodeEnabled, PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
PasswordResetEnabled: updatedSettings.PasswordResetEnabled, PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
FrontendURL: updatedSettings.FrontendURL,
InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled, InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
TotpEnabled: updatedSettings.TotpEnabled, TotpEnabled: updatedSettings.TotpEnabled,
TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
...@@ -614,6 +627,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, ...@@ -614,6 +627,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.PasswordResetEnabled != after.PasswordResetEnabled { if before.PasswordResetEnabled != after.PasswordResetEnabled {
changed = append(changed, "password_reset_enabled") changed = append(changed, "password_reset_enabled")
} }
if before.FrontendURL != after.FrontendURL {
changed = append(changed, "frontend_url")
}
if before.TotpEnabled != after.TotpEnabled { if before.TotpEnabled != after.TotpEnabled {
changed = append(changed, "totp_enabled") changed = append(changed, "totp_enabled")
} }
......
...@@ -459,9 +459,9 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) { ...@@ -459,9 +459,9 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
return return
} }
frontendBaseURL := strings.TrimSpace(h.cfg.Server.FrontendURL) frontendBaseURL := strings.TrimSpace(h.settingSvc.GetFrontendURL(c.Request.Context()))
if frontendBaseURL == "" { if frontendBaseURL == "" {
slog.Error("server.frontend_url not configured; cannot build password reset link") slog.Error("frontend_url not configured in settings or config; cannot build password reset link")
response.InternalError(c, "Password reset is not configured") response.InternalError(c, "Password reset is not configured")
return return
} }
......
...@@ -22,6 +22,7 @@ type SystemSettings struct { ...@@ -22,6 +22,7 @@ type SystemSettings struct {
RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"` RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
PromoCodeEnabled bool `json:"promo_code_enabled"` PromoCodeEnabled bool `json:"promo_code_enabled"`
PasswordResetEnabled bool `json:"password_reset_enabled"` PasswordResetEnabled bool `json:"password_reset_enabled"`
FrontendURL string `json:"frontend_url"`
InvitationCodeEnabled bool `json:"invitation_code_enabled"` InvitationCodeEnabled bool `json:"invitation_code_enabled"`
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证 TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
TotpEncryptionKeyConfigured bool `json:"totp_encryption_key_configured"` // TOTP 加密密钥是否已配置 TotpEncryptionKeyConfigured bool `json:"totp_encryption_key_configured"` // TOTP 加密密钥是否已配置
......
...@@ -493,6 +493,7 @@ func TestAPIContracts(t *testing.T) { ...@@ -493,6 +493,7 @@ func TestAPIContracts(t *testing.T) {
"registration_email_suffix_whitelist": [], "registration_email_suffix_whitelist": [],
"promo_code_enabled": true, "promo_code_enabled": true,
"password_reset_enabled": false, "password_reset_enabled": false,
"frontend_url": "",
"totp_enabled": false, "totp_enabled": false,
"totp_encryption_key_configured": false, "totp_encryption_key_configured": false,
"smtp_host": "smtp.example.com", "smtp_host": "smtp.example.com",
......
...@@ -80,6 +80,7 @@ const ( ...@@ -80,6 +80,7 @@ const (
SettingKeyRegistrationEmailSuffixWhitelist = "registration_email_suffix_whitelist" // 注册邮箱后缀白名单(JSON 数组) SettingKeyRegistrationEmailSuffixWhitelist = "registration_email_suffix_whitelist" // 注册邮箱后缀白名单(JSON 数组)
SettingKeyPromoCodeEnabled = "promo_code_enabled" // 是否启用优惠码功能 SettingKeyPromoCodeEnabled = "promo_code_enabled" // 是否启用优惠码功能
SettingKeyPasswordResetEnabled = "password_reset_enabled" // 是否启用忘记密码功能(需要先开启邮件验证) SettingKeyPasswordResetEnabled = "password_reset_enabled" // 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyFrontendURL = "frontend_url" // 前端基础URL,用于生成邮件中的重置密码链接
SettingKeyInvitationCodeEnabled = "invitation_code_enabled" // 是否启用邀请码注册 SettingKeyInvitationCodeEnabled = "invitation_code_enabled" // 是否启用邀请码注册
// 邮件服务设置 // 邮件服务设置
......
...@@ -116,6 +116,15 @@ func (s *SettingService) GetAllSettings(ctx context.Context) (*SystemSettings, e ...@@ -116,6 +116,15 @@ func (s *SettingService) GetAllSettings(ctx context.Context) (*SystemSettings, e
return s.parseSettings(settings), nil return s.parseSettings(settings), nil
} }
// GetFrontendURL 获取前端基础URL(数据库优先,fallback 到配置文件)
func (s *SettingService) GetFrontendURL(ctx context.Context) string {
val, err := s.settingRepo.GetValue(ctx, SettingKeyFrontendURL)
if err == nil && strings.TrimSpace(val) != "" {
return strings.TrimSpace(val)
}
return s.cfg.Server.FrontendURL
}
// GetPublicSettings 获取公开设置(无需登录) // GetPublicSettings 获取公开设置(无需登录)
func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings, error) { func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings, error) {
keys := []string{ keys := []string{
...@@ -401,6 +410,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet ...@@ -401,6 +410,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates[SettingKeyRegistrationEmailSuffixWhitelist] = string(registrationEmailSuffixWhitelistJSON) updates[SettingKeyRegistrationEmailSuffixWhitelist] = string(registrationEmailSuffixWhitelistJSON)
updates[SettingKeyPromoCodeEnabled] = strconv.FormatBool(settings.PromoCodeEnabled) updates[SettingKeyPromoCodeEnabled] = strconv.FormatBool(settings.PromoCodeEnabled)
updates[SettingKeyPasswordResetEnabled] = strconv.FormatBool(settings.PasswordResetEnabled) updates[SettingKeyPasswordResetEnabled] = strconv.FormatBool(settings.PasswordResetEnabled)
updates[SettingKeyFrontendURL] = settings.FrontendURL
updates[SettingKeyInvitationCodeEnabled] = strconv.FormatBool(settings.InvitationCodeEnabled) updates[SettingKeyInvitationCodeEnabled] = strconv.FormatBool(settings.InvitationCodeEnabled)
updates[SettingKeyTotpEnabled] = strconv.FormatBool(settings.TotpEnabled) updates[SettingKeyTotpEnabled] = strconv.FormatBool(settings.TotpEnabled)
...@@ -767,6 +777,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin ...@@ -767,6 +777,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
RegistrationEmailSuffixWhitelist: ParseRegistrationEmailSuffixWhitelist(settings[SettingKeyRegistrationEmailSuffixWhitelist]), RegistrationEmailSuffixWhitelist: ParseRegistrationEmailSuffixWhitelist(settings[SettingKeyRegistrationEmailSuffixWhitelist]),
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用 PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
PasswordResetEnabled: emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true", PasswordResetEnabled: emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true",
FrontendURL: settings[SettingKeyFrontendURL],
InvitationCodeEnabled: settings[SettingKeyInvitationCodeEnabled] == "true", InvitationCodeEnabled: settings[SettingKeyInvitationCodeEnabled] == "true",
TotpEnabled: settings[SettingKeyTotpEnabled] == "true", TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
SMTPHost: settings[SettingKeySMTPHost], SMTPHost: settings[SettingKeySMTPHost],
......
...@@ -6,6 +6,7 @@ type SystemSettings struct { ...@@ -6,6 +6,7 @@ type SystemSettings struct {
RegistrationEmailSuffixWhitelist []string RegistrationEmailSuffixWhitelist []string
PromoCodeEnabled bool PromoCodeEnabled bool
PasswordResetEnabled bool PasswordResetEnabled bool
FrontendURL string
InvitationCodeEnabled bool InvitationCodeEnabled bool
TotpEnabled bool // TOTP 双因素认证 TotpEnabled bool // TOTP 双因素认证
......
...@@ -21,6 +21,7 @@ export interface SystemSettings { ...@@ -21,6 +21,7 @@ export interface SystemSettings {
registration_email_suffix_whitelist: string[] registration_email_suffix_whitelist: string[]
promo_code_enabled: boolean promo_code_enabled: boolean
password_reset_enabled: boolean password_reset_enabled: boolean
frontend_url: string
invitation_code_enabled: boolean invitation_code_enabled: boolean
totp_enabled: boolean // TOTP 双因素认证 totp_enabled: boolean // TOTP 双因素认证
totp_encryption_key_configured: boolean // TOTP 加密密钥是否已配置 totp_encryption_key_configured: boolean // TOTP 加密密钥是否已配置
...@@ -91,6 +92,7 @@ export interface UpdateSettingsRequest { ...@@ -91,6 +92,7 @@ export interface UpdateSettingsRequest {
registration_email_suffix_whitelist?: string[] registration_email_suffix_whitelist?: string[]
promo_code_enabled?: boolean promo_code_enabled?: boolean
password_reset_enabled?: boolean password_reset_enabled?: boolean
frontend_url?: string
invitation_code_enabled?: boolean invitation_code_enabled?: boolean
totp_enabled?: boolean // TOTP 双因素认证 totp_enabled?: boolean // TOTP 双因素认证
default_balance?: number default_balance?: number
......
...@@ -3966,6 +3966,9 @@ export default { ...@@ -3966,6 +3966,9 @@ export default {
invitationCodeHint: 'When enabled, users must enter a valid invitation code to register', invitationCodeHint: 'When enabled, users must enter a valid invitation code to register',
passwordReset: 'Password Reset', passwordReset: 'Password Reset',
passwordResetHint: 'Allow users to reset their password via email', passwordResetHint: 'Allow users to reset their password via email',
frontendUrl: 'Frontend URL',
frontendUrlPlaceholder: 'https://example.com',
frontendUrlHint: 'Used to generate password reset links in emails. Example: https://example.com',
totp: 'Two-Factor Authentication (2FA)', totp: 'Two-Factor Authentication (2FA)',
totpHint: 'Allow users to use authenticator apps like Google Authenticator', totpHint: 'Allow users to use authenticator apps like Google Authenticator',
totpKeyNotConfigured: totpKeyNotConfigured:
......
...@@ -4140,6 +4140,9 @@ export default { ...@@ -4140,6 +4140,9 @@ export default {
invitationCodeHint: '开启后,用户注册时需要填写有效的邀请码', invitationCodeHint: '开启后,用户注册时需要填写有效的邀请码',
passwordReset: '忘记密码', passwordReset: '忘记密码',
passwordResetHint: '允许用户通过邮箱重置密码', passwordResetHint: '允许用户通过邮箱重置密码',
frontendUrl: '前端地址',
frontendUrlPlaceholder: 'https://example.com',
frontendUrlHint: '用于生成邮件中的密码重置链接,例如 https://example.com',
totp: '双因素认证 (2FA)', totp: '双因素认证 (2FA)',
totpHint: '允许用户使用 Google Authenticator 等应用进行二次验证', totpHint: '允许用户使用 Google Authenticator 等应用进行二次验证',
totpKeyNotConfigured: totpKeyNotConfigured:
......
...@@ -653,6 +653,24 @@ ...@@ -653,6 +653,24 @@
</div> </div>
<Toggle v-model="form.password_reset_enabled" /> <Toggle v-model="form.password_reset_enabled" />
</div> </div>
<!-- Frontend URL - Only show when password reset is enabled -->
<div
v-if="form.email_verify_enabled && form.password_reset_enabled"
class="border-t border-gray-100 pt-4 dark:border-dark-700"
>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.registration.frontendUrl') }}
</label>
<input
v-model="form.frontend_url"
type="url"
class="input"
:placeholder="t('admin.settings.registration.frontendUrlPlaceholder')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.registration.frontendUrlHint') }}
</p>
</div>
<!-- TOTP 2FA --> <!-- TOTP 2FA -->
<div <div
...@@ -1586,6 +1604,7 @@ ...@@ -1586,6 +1604,7 @@
</div> </div>
<Toggle v-model="form.smtp_use_tls" /> <Toggle v-model="form.smtp_use_tls" />
</div> </div>
</div> </div>
</div> </div>
...@@ -1820,6 +1839,7 @@ const form = reactive<SettingsForm>({ ...@@ -1820,6 +1839,7 @@ const form = reactive<SettingsForm>({
purchase_subscription_url: '', purchase_subscription_url: '',
sora_client_enabled: false, sora_client_enabled: false,
custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>, custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>,
frontend_url: '',
smtp_host: '', smtp_host: '',
smtp_port: 587, smtp_port: 587,
smtp_username: '', smtp_username: '',
...@@ -2097,6 +2117,7 @@ async function saveSettings() { ...@@ -2097,6 +2117,7 @@ async function saveSettings() {
purchase_subscription_url: form.purchase_subscription_url, purchase_subscription_url: form.purchase_subscription_url,
sora_client_enabled: form.sora_client_enabled, sora_client_enabled: form.sora_client_enabled,
custom_menu_items: form.custom_menu_items, custom_menu_items: form.custom_menu_items,
frontend_url: form.frontend_url,
smtp_host: form.smtp_host, smtp_host: form.smtp_host,
smtp_port: form.smtp_port, smtp_port: form.smtp_port,
smtp_username: form.smtp_username, smtp_username: form.smtp_username,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment