Skip to content

Commit 1fb29d5

Browse files
committed
fix(settings): prevent SMTP config overwrite and stabilize test after refresh
1 parent a225a24 commit 1fb29d5

3 files changed

Lines changed: 100 additions & 28 deletions

File tree

backend/internal/handler/admin/setting_handler.go

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,27 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
231231
if req.DefaultBalance < 0 {
232232
req.DefaultBalance = 0
233233
}
234+
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
235+
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
236+
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
237+
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
238+
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
234239
if req.SMTPPort <= 0 {
235240
req.SMTPPort = 587
236241
}
237242
req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
238243

244+
// SMTP 配置保护:如果请求中 smtp_host 为空但数据库中已有配置,则保留已有 SMTP 配置
245+
// 防止前端加载设置失败时空表单覆盖已保存的 SMTP 配置
246+
if req.SMTPHost == "" && previousSettings.SMTPHost != "" {
247+
req.SMTPHost = previousSettings.SMTPHost
248+
req.SMTPPort = previousSettings.SMTPPort
249+
req.SMTPUsername = previousSettings.SMTPUsername
250+
req.SMTPFrom = previousSettings.SMTPFrom
251+
req.SMTPFromName = previousSettings.SMTPFromName
252+
req.SMTPUseTLS = previousSettings.SMTPUseTLS
253+
}
254+
239255
// Turnstile 参数验证
240256
if req.TurnstileEnabled {
241257
// 检查必填字段
@@ -828,7 +844,7 @@ func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool {
828844

829845
// TestSMTPRequest 测试SMTP连接请求
830846
type TestSMTPRequest struct {
831-
SMTPHost string `json:"smtp_host" binding:"required"`
847+
SMTPHost string `json:"smtp_host"`
832848
SMTPPort int `json:"smtp_port"`
833849
SMTPUsername string `json:"smtp_username"`
834850
SMTPPassword string `json:"smtp_password"`
@@ -844,18 +860,35 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
844860
return
845861
}
846862

847-
if req.SMTPPort <= 0 {
848-
req.SMTPPort = 587
863+
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
864+
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
865+
866+
var savedConfig *service.SMTPConfig
867+
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
868+
savedConfig = cfg
849869
}
850870

851-
// 如果未提供密码,从数据库获取已保存的密码
852-
password := req.SMTPPassword
853-
if password == "" {
854-
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
855-
if err == nil && savedConfig != nil {
856-
password = savedConfig.Password
871+
if req.SMTPHost == "" && savedConfig != nil {
872+
req.SMTPHost = savedConfig.Host
873+
}
874+
if req.SMTPPort <= 0 {
875+
if savedConfig != nil && savedConfig.Port > 0 {
876+
req.SMTPPort = savedConfig.Port
877+
} else {
878+
req.SMTPPort = 587
857879
}
858880
}
881+
if req.SMTPUsername == "" && savedConfig != nil {
882+
req.SMTPUsername = savedConfig.Username
883+
}
884+
password := strings.TrimSpace(req.SMTPPassword)
885+
if password == "" && savedConfig != nil {
886+
password = savedConfig.Password
887+
}
888+
if req.SMTPHost == "" {
889+
response.BadRequest(c, "SMTP host is required")
890+
return
891+
}
859892

860893
config := &service.SMTPConfig{
861894
Host: req.SMTPHost,
@@ -877,7 +910,7 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
877910
// SendTestEmailRequest 发送测试邮件请求
878911
type SendTestEmailRequest struct {
879912
Email string `json:"email" binding:"required,email"`
880-
SMTPHost string `json:"smtp_host" binding:"required"`
913+
SMTPHost string `json:"smtp_host"`
881914
SMTPPort int `json:"smtp_port"`
882915
SMTPUsername string `json:"smtp_username"`
883916
SMTPPassword string `json:"smtp_password"`
@@ -895,18 +928,43 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
895928
return
896929
}
897930

898-
if req.SMTPPort <= 0 {
899-
req.SMTPPort = 587
931+
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
932+
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
933+
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
934+
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
935+
936+
var savedConfig *service.SMTPConfig
937+
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
938+
savedConfig = cfg
900939
}
901940

902-
// 如果未提供密码,从数据库获取已保存的密码
903-
password := req.SMTPPassword
904-
if password == "" {
905-
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
906-
if err == nil && savedConfig != nil {
907-
password = savedConfig.Password
941+
if req.SMTPHost == "" && savedConfig != nil {
942+
req.SMTPHost = savedConfig.Host
943+
}
944+
if req.SMTPPort <= 0 {
945+
if savedConfig != nil && savedConfig.Port > 0 {
946+
req.SMTPPort = savedConfig.Port
947+
} else {
948+
req.SMTPPort = 587
908949
}
909950
}
951+
if req.SMTPUsername == "" && savedConfig != nil {
952+
req.SMTPUsername = savedConfig.Username
953+
}
954+
password := strings.TrimSpace(req.SMTPPassword)
955+
if password == "" && savedConfig != nil {
956+
password = savedConfig.Password
957+
}
958+
if req.SMTPFrom == "" && savedConfig != nil {
959+
req.SMTPFrom = savedConfig.From
960+
}
961+
if req.SMTPFromName == "" && savedConfig != nil {
962+
req.SMTPFromName = savedConfig.FromName
963+
}
964+
if req.SMTPHost == "" {
965+
response.BadRequest(c, "SMTP host is required")
966+
return
967+
}
910968

911969
config := &service.SMTPConfig{
912970
Host: req.SMTPHost,

backend/internal/service/email_service.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/smtp"
1313
"net/url"
1414
"strconv"
15+
"strings"
1516
"time"
1617

1718
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
@@ -111,7 +112,7 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
111112
return nil, fmt.Errorf("get smtp settings: %w", err)
112113
}
113114

114-
host := settings[SettingKeySMTPHost]
115+
host := strings.TrimSpace(settings[SettingKeySMTPHost])
115116
if host == "" {
116117
return nil, ErrEmailNotConfigured
117118
}
@@ -128,10 +129,10 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
128129
return &SMTPConfig{
129130
Host: host,
130131
Port: port,
131-
Username: settings[SettingKeySMTPUsername],
132-
Password: settings[SettingKeySMTPPassword],
133-
From: settings[SettingKeySMTPFrom],
134-
FromName: settings[SettingKeySMTPFromName],
132+
Username: strings.TrimSpace(settings[SettingKeySMTPUsername]),
133+
Password: strings.TrimSpace(settings[SettingKeySMTPPassword]),
134+
From: strings.TrimSpace(settings[SettingKeySMTPFrom]),
135+
FromName: strings.TrimSpace(settings[SettingKeySMTPFromName]),
135136
UseTLS: useTLS,
136137
}, nil
137138
}

frontend/src/views/admin/SettingsView.vue

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,7 +1580,7 @@
15801580
<button
15811581
type="button"
15821582
@click="testSmtpConnection"
1583-
:disabled="testingSmtp"
1583+
:disabled="testingSmtp || loadFailed"
15841584
class="btn btn-secondary btn-sm"
15851585
>
15861586
<svg v-if="testingSmtp" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
@@ -1650,6 +1650,11 @@
16501650
v-model="form.smtp_password"
16511651
type="password"
16521652
class="input"
1653+
autocomplete="new-password"
1654+
autocapitalize="off"
1655+
spellcheck="false"
1656+
@keydown="smtpPasswordManuallyEdited = true"
1657+
@paste="smtpPasswordManuallyEdited = true"
16531658
:placeholder="
16541659
form.smtp_password_configured
16551660
? t('admin.settings.smtp.passwordConfiguredPlaceholder')
@@ -1732,7 +1737,7 @@
17321737
<button
17331738
type="button"
17341739
@click="sendTestEmail"
1735-
:disabled="sendingTestEmail || !testEmailAddress"
1740+
:disabled="sendingTestEmail || !testEmailAddress || loadFailed"
17361741
class="btn btn-secondary"
17371742
>
17381743
<svg
@@ -1778,7 +1783,7 @@
17781783

17791784
<!-- Save Button -->
17801785
<div v-show="activeTab !== 'backup' && activeTab !== 'data'" class="flex justify-end">
1781-
<button type="submit" :disabled="saving" class="btn btn-primary">
1786+
<button type="submit" :disabled="saving || loadFailed" class="btn btn-primary">
17821787
<svg v-if="saving" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
17831788
<circle
17841789
class="opacity-25"
@@ -1849,9 +1854,11 @@ const settingsTabs = [
18491854
const { copyToClipboard } = useClipboard()
18501855
18511856
const loading = ref(true)
1857+
const loadFailed = ref(false)
18521858
const saving = ref(false)
18531859
const testingSmtp = ref(false)
18541860
const sendingTestEmail = ref(false)
1861+
const smtpPasswordManuallyEdited = ref(false)
18551862
const testEmailAddress = ref('')
18561863
const registrationEmailSuffixWhitelistTags = ref<string[]>([])
18571864
const registrationEmailSuffixWhitelistDraft = ref('')
@@ -2116,6 +2123,7 @@ function moveMenuItem(index: number, direction: -1 | 1) {
21162123
21172124
async function loadSettings() {
21182125
loading.value = true
2126+
loadFailed.value = false
21192127
try {
21202128
const settings = await adminAPI.settings.getSettings()
21212129
Object.assign(form, settings)
@@ -2133,9 +2141,11 @@ async function loadSettings() {
21332141
)
21342142
registrationEmailSuffixWhitelistDraft.value = ''
21352143
form.smtp_password = ''
2144+
smtpPasswordManuallyEdited.value = false
21362145
form.turnstile_secret_key = ''
21372146
form.linuxdo_connect_client_secret = ''
21382147
} catch (error: any) {
2148+
loadFailed.value = true
21392149
appStore.showError(
21402150
t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError'))
21412151
)
@@ -2257,6 +2267,7 @@ async function saveSettings() {
22572267
)
22582268
registrationEmailSuffixWhitelistDraft.value = ''
22592269
form.smtp_password = ''
2270+
smtpPasswordManuallyEdited.value = false
22602271
form.turnstile_secret_key = ''
22612272
form.linuxdo_connect_client_secret = ''
22622273
// Refresh cached settings so sidebar/header update immediately
@@ -2275,11 +2286,12 @@ async function saveSettings() {
22752286
async function testSmtpConnection() {
22762287
testingSmtp.value = true
22772288
try {
2289+
const smtpPasswordForTest = smtpPasswordManuallyEdited.value ? form.smtp_password : ''
22782290
const result = await adminAPI.settings.testSmtpConnection({
22792291
smtp_host: form.smtp_host,
22802292
smtp_port: form.smtp_port,
22812293
smtp_username: form.smtp_username,
2282-
smtp_password: form.smtp_password,
2294+
smtp_password: smtpPasswordForTest,
22832295
smtp_use_tls: form.smtp_use_tls
22842296
})
22852297
// API returns { message: "..." } on success, errors are thrown as exceptions
@@ -2301,12 +2313,13 @@ async function sendTestEmail() {
23012313
23022314
sendingTestEmail.value = true
23032315
try {
2316+
const smtpPasswordForSend = smtpPasswordManuallyEdited.value ? form.smtp_password : ''
23042317
const result = await adminAPI.settings.sendTestEmail({
23052318
email: testEmailAddress.value,
23062319
smtp_host: form.smtp_host,
23072320
smtp_port: form.smtp_port,
23082321
smtp_username: form.smtp_username,
2309-
smtp_password: form.smtp_password,
2322+
smtp_password: smtpPasswordForSend,
23102323
smtp_from_email: form.smtp_from_email,
23112324
smtp_from_name: form.smtp_from_name,
23122325
smtp_use_tls: form.smtp_use_tls

0 commit comments

Comments
 (0)