Commit 1fb29d59 authored by Eilen6316's avatar Eilen6316
Browse files

fix(settings): prevent SMTP config overwrite and stabilize test after refresh

parent a225a241
...@@ -231,11 +231,27 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -231,11 +231,27 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
if req.DefaultBalance < 0 { if req.DefaultBalance < 0 {
req.DefaultBalance = 0 req.DefaultBalance = 0
} }
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
if req.SMTPPort <= 0 { if req.SMTPPort <= 0 {
req.SMTPPort = 587 req.SMTPPort = 587
} }
req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions) req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
// SMTP 配置保护:如果请求中 smtp_host 为空但数据库中已有配置,则保留已有 SMTP 配置
// 防止前端加载设置失败时空表单覆盖已保存的 SMTP 配置
if req.SMTPHost == "" && previousSettings.SMTPHost != "" {
req.SMTPHost = previousSettings.SMTPHost
req.SMTPPort = previousSettings.SMTPPort
req.SMTPUsername = previousSettings.SMTPUsername
req.SMTPFrom = previousSettings.SMTPFrom
req.SMTPFromName = previousSettings.SMTPFromName
req.SMTPUseTLS = previousSettings.SMTPUseTLS
}
// Turnstile 参数验证 // Turnstile 参数验证
if req.TurnstileEnabled { if req.TurnstileEnabled {
// 检查必填字段 // 检查必填字段
...@@ -828,7 +844,7 @@ func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool { ...@@ -828,7 +844,7 @@ func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool {
// TestSMTPRequest 测试SMTP连接请求 // TestSMTPRequest 测试SMTP连接请求
type TestSMTPRequest struct { type TestSMTPRequest struct {
SMTPHost string `json:"smtp_host" binding:"required"` SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"` SMTPPassword string `json:"smtp_password"`
...@@ -844,17 +860,34 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) { ...@@ -844,17 +860,34 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
return return
} }
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
var savedConfig *service.SMTPConfig
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
savedConfig = cfg
}
if req.SMTPHost == "" && savedConfig != nil {
req.SMTPHost = savedConfig.Host
}
if req.SMTPPort <= 0 { if req.SMTPPort <= 0 {
if savedConfig != nil && savedConfig.Port > 0 {
req.SMTPPort = savedConfig.Port
} else {
req.SMTPPort = 587 req.SMTPPort = 587
} }
}
// 如果未提供密码,从数据库获取已保存的密码 if req.SMTPUsername == "" && savedConfig != nil {
password := req.SMTPPassword req.SMTPUsername = savedConfig.Username
if password == "" { }
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context()) password := strings.TrimSpace(req.SMTPPassword)
if err == nil && savedConfig != nil { if password == "" && savedConfig != nil {
password = savedConfig.Password password = savedConfig.Password
} }
if req.SMTPHost == "" {
response.BadRequest(c, "SMTP host is required")
return
} }
config := &service.SMTPConfig{ config := &service.SMTPConfig{
...@@ -877,7 +910,7 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) { ...@@ -877,7 +910,7 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
// SendTestEmailRequest 发送测试邮件请求 // SendTestEmailRequest 发送测试邮件请求
type SendTestEmailRequest struct { type SendTestEmailRequest struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email"`
SMTPHost string `json:"smtp_host" binding:"required"` SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"` SMTPPassword string `json:"smtp_password"`
...@@ -895,17 +928,42 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) { ...@@ -895,17 +928,42 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
return return
} }
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
var savedConfig *service.SMTPConfig
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
savedConfig = cfg
}
if req.SMTPHost == "" && savedConfig != nil {
req.SMTPHost = savedConfig.Host
}
if req.SMTPPort <= 0 { if req.SMTPPort <= 0 {
if savedConfig != nil && savedConfig.Port > 0 {
req.SMTPPort = savedConfig.Port
} else {
req.SMTPPort = 587 req.SMTPPort = 587
} }
}
// 如果未提供密码,从数据库获取已保存的密码 if req.SMTPUsername == "" && savedConfig != nil {
password := req.SMTPPassword req.SMTPUsername = savedConfig.Username
if password == "" { }
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context()) password := strings.TrimSpace(req.SMTPPassword)
if err == nil && savedConfig != nil { if password == "" && savedConfig != nil {
password = savedConfig.Password password = savedConfig.Password
} }
if req.SMTPFrom == "" && savedConfig != nil {
req.SMTPFrom = savedConfig.From
}
if req.SMTPFromName == "" && savedConfig != nil {
req.SMTPFromName = savedConfig.FromName
}
if req.SMTPHost == "" {
response.BadRequest(c, "SMTP host is required")
return
} }
config := &service.SMTPConfig{ config := &service.SMTPConfig{
......
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"net/smtp" "net/smtp"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
...@@ -111,7 +112,7 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) { ...@@ -111,7 +112,7 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
return nil, fmt.Errorf("get smtp settings: %w", err) return nil, fmt.Errorf("get smtp settings: %w", err)
} }
host := settings[SettingKeySMTPHost] host := strings.TrimSpace(settings[SettingKeySMTPHost])
if host == "" { if host == "" {
return nil, ErrEmailNotConfigured return nil, ErrEmailNotConfigured
} }
...@@ -128,10 +129,10 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) { ...@@ -128,10 +129,10 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
return &SMTPConfig{ return &SMTPConfig{
Host: host, Host: host,
Port: port, Port: port,
Username: settings[SettingKeySMTPUsername], Username: strings.TrimSpace(settings[SettingKeySMTPUsername]),
Password: settings[SettingKeySMTPPassword], Password: strings.TrimSpace(settings[SettingKeySMTPPassword]),
From: settings[SettingKeySMTPFrom], From: strings.TrimSpace(settings[SettingKeySMTPFrom]),
FromName: settings[SettingKeySMTPFromName], FromName: strings.TrimSpace(settings[SettingKeySMTPFromName]),
UseTLS: useTLS, UseTLS: useTLS,
}, nil }, nil
} }
......
...@@ -1580,7 +1580,7 @@ ...@@ -1580,7 +1580,7 @@
<button <button
type="button" type="button"
@click="testSmtpConnection" @click="testSmtpConnection"
:disabled="testingSmtp" :disabled="testingSmtp || loadFailed"
class="btn btn-secondary btn-sm" class="btn btn-secondary btn-sm"
> >
<svg v-if="testingSmtp" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"> <svg v-if="testingSmtp" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
...@@ -1650,6 +1650,11 @@ ...@@ -1650,6 +1650,11 @@
v-model="form.smtp_password" v-model="form.smtp_password"
type="password" type="password"
class="input" class="input"
autocomplete="new-password"
autocapitalize="off"
spellcheck="false"
@keydown="smtpPasswordManuallyEdited = true"
@paste="smtpPasswordManuallyEdited = true"
:placeholder=" :placeholder="
form.smtp_password_configured form.smtp_password_configured
? t('admin.settings.smtp.passwordConfiguredPlaceholder') ? t('admin.settings.smtp.passwordConfiguredPlaceholder')
...@@ -1732,7 +1737,7 @@ ...@@ -1732,7 +1737,7 @@
<button <button
type="button" type="button"
@click="sendTestEmail" @click="sendTestEmail"
:disabled="sendingTestEmail || !testEmailAddress" :disabled="sendingTestEmail || !testEmailAddress || loadFailed"
class="btn btn-secondary" class="btn btn-secondary"
> >
<svg <svg
...@@ -1778,7 +1783,7 @@ ...@@ -1778,7 +1783,7 @@
<!-- Save Button --> <!-- Save Button -->
<div v-show="activeTab !== 'backup' && activeTab !== 'data'" class="flex justify-end"> <div v-show="activeTab !== 'backup' && activeTab !== 'data'" class="flex justify-end">
<button type="submit" :disabled="saving" class="btn btn-primary"> <button type="submit" :disabled="saving || loadFailed" class="btn btn-primary">
<svg v-if="saving" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"> <svg v-if="saving" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle <circle
class="opacity-25" class="opacity-25"
...@@ -1849,9 +1854,11 @@ const settingsTabs = [ ...@@ -1849,9 +1854,11 @@ const settingsTabs = [
const { copyToClipboard } = useClipboard() const { copyToClipboard } = useClipboard()
const loading = ref(true) const loading = ref(true)
const loadFailed = ref(false)
const saving = ref(false) const saving = ref(false)
const testingSmtp = ref(false) const testingSmtp = ref(false)
const sendingTestEmail = ref(false) const sendingTestEmail = ref(false)
const smtpPasswordManuallyEdited = ref(false)
const testEmailAddress = ref('') const testEmailAddress = ref('')
const registrationEmailSuffixWhitelistTags = ref<string[]>([]) const registrationEmailSuffixWhitelistTags = ref<string[]>([])
const registrationEmailSuffixWhitelistDraft = ref('') const registrationEmailSuffixWhitelistDraft = ref('')
...@@ -2116,6 +2123,7 @@ function moveMenuItem(index: number, direction: -1 | 1) { ...@@ -2116,6 +2123,7 @@ function moveMenuItem(index: number, direction: -1 | 1) {
async function loadSettings() { async function loadSettings() {
loading.value = true loading.value = true
loadFailed.value = false
try { try {
const settings = await adminAPI.settings.getSettings() const settings = await adminAPI.settings.getSettings()
Object.assign(form, settings) Object.assign(form, settings)
...@@ -2133,9 +2141,11 @@ async function loadSettings() { ...@@ -2133,9 +2141,11 @@ async function loadSettings() {
) )
registrationEmailSuffixWhitelistDraft.value = '' registrationEmailSuffixWhitelistDraft.value = ''
form.smtp_password = '' form.smtp_password = ''
smtpPasswordManuallyEdited.value = false
form.turnstile_secret_key = '' form.turnstile_secret_key = ''
form.linuxdo_connect_client_secret = '' form.linuxdo_connect_client_secret = ''
} catch (error: any) { } catch (error: any) {
loadFailed.value = true
appStore.showError( appStore.showError(
t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError')) t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError'))
) )
...@@ -2257,6 +2267,7 @@ async function saveSettings() { ...@@ -2257,6 +2267,7 @@ async function saveSettings() {
) )
registrationEmailSuffixWhitelistDraft.value = '' registrationEmailSuffixWhitelistDraft.value = ''
form.smtp_password = '' form.smtp_password = ''
smtpPasswordManuallyEdited.value = false
form.turnstile_secret_key = '' form.turnstile_secret_key = ''
form.linuxdo_connect_client_secret = '' form.linuxdo_connect_client_secret = ''
// Refresh cached settings so sidebar/header update immediately // Refresh cached settings so sidebar/header update immediately
...@@ -2275,11 +2286,12 @@ async function saveSettings() { ...@@ -2275,11 +2286,12 @@ async function saveSettings() {
async function testSmtpConnection() { async function testSmtpConnection() {
testingSmtp.value = true testingSmtp.value = true
try { try {
const smtpPasswordForTest = smtpPasswordManuallyEdited.value ? form.smtp_password : ''
const result = await adminAPI.settings.testSmtpConnection({ const result = await adminAPI.settings.testSmtpConnection({
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,
smtp_password: form.smtp_password, smtp_password: smtpPasswordForTest,
smtp_use_tls: form.smtp_use_tls smtp_use_tls: form.smtp_use_tls
}) })
// API returns { message: "..." } on success, errors are thrown as exceptions // API returns { message: "..." } on success, errors are thrown as exceptions
...@@ -2301,12 +2313,13 @@ async function sendTestEmail() { ...@@ -2301,12 +2313,13 @@ async function sendTestEmail() {
sendingTestEmail.value = true sendingTestEmail.value = true
try { try {
const smtpPasswordForSend = smtpPasswordManuallyEdited.value ? form.smtp_password : ''
const result = await adminAPI.settings.sendTestEmail({ const result = await adminAPI.settings.sendTestEmail({
email: testEmailAddress.value, email: testEmailAddress.value,
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,
smtp_password: form.smtp_password, smtp_password: smtpPasswordForSend,
smtp_from_email: form.smtp_from_email, smtp_from_email: form.smtp_from_email,
smtp_from_name: form.smtp_from_name, smtp_from_name: form.smtp_from_name,
smtp_use_tls: form.smtp_use_tls smtp_use_tls: form.smtp_use_tls
......
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