Skip to content

Commit 8e834fd

Browse files
authored
Merge pull request Wei-Shaw#1204 from Eilen6316/fix/smtp-config-stability-and-refresh-test
fix(settings): prevent SMTP config overwrite and stabilize SMTP test after refresh
2 parents 0204674 + 1fb29d5 commit 8e834fd

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
@@ -233,11 +233,27 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
233233
if req.DefaultBalance < 0 {
234234
req.DefaultBalance = 0
235235
}
236+
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
237+
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
238+
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
239+
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
240+
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
236241
if req.SMTPPort <= 0 {
237242
req.SMTPPort = 587
238243
}
239244
req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
240245

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

882898
// TestSMTPRequest 测试SMTP连接请求
883899
type TestSMTPRequest struct {
884-
SMTPHost string `json:"smtp_host" binding:"required"`
900+
SMTPHost string `json:"smtp_host"`
885901
SMTPPort int `json:"smtp_port"`
886902
SMTPUsername string `json:"smtp_username"`
887903
SMTPPassword string `json:"smtp_password"`
@@ -897,18 +913,35 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
897913
return
898914
}
899915

900-
if req.SMTPPort <= 0 {
901-
req.SMTPPort = 587
916+
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
917+
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
918+
919+
var savedConfig *service.SMTPConfig
920+
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
921+
savedConfig = cfg
902922
}
903923

904-
// 如果未提供密码,从数据库获取已保存的密码
905-
password := req.SMTPPassword
906-
if password == "" {
907-
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
908-
if err == nil && savedConfig != nil {
909-
password = savedConfig.Password
924+
if req.SMTPHost == "" && savedConfig != nil {
925+
req.SMTPHost = savedConfig.Host
926+
}
927+
if req.SMTPPort <= 0 {
928+
if savedConfig != nil && savedConfig.Port > 0 {
929+
req.SMTPPort = savedConfig.Port
930+
} else {
931+
req.SMTPPort = 587
910932
}
911933
}
934+
if req.SMTPUsername == "" && savedConfig != nil {
935+
req.SMTPUsername = savedConfig.Username
936+
}
937+
password := strings.TrimSpace(req.SMTPPassword)
938+
if password == "" && savedConfig != nil {
939+
password = savedConfig.Password
940+
}
941+
if req.SMTPHost == "" {
942+
response.BadRequest(c, "SMTP host is required")
943+
return
944+
}
912945

913946
config := &service.SMTPConfig{
914947
Host: req.SMTPHost,
@@ -930,7 +963,7 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
930963
// SendTestEmailRequest 发送测试邮件请求
931964
type SendTestEmailRequest struct {
932965
Email string `json:"email" binding:"required,email"`
933-
SMTPHost string `json:"smtp_host" binding:"required"`
966+
SMTPHost string `json:"smtp_host"`
934967
SMTPPort int `json:"smtp_port"`
935968
SMTPUsername string `json:"smtp_username"`
936969
SMTPPassword string `json:"smtp_password"`
@@ -948,18 +981,43 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
948981
return
949982
}
950983

951-
if req.SMTPPort <= 0 {
952-
req.SMTPPort = 587
984+
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
985+
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
986+
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
987+
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
988+
989+
var savedConfig *service.SMTPConfig
990+
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
991+
savedConfig = cfg
953992
}
954993

955-
// 如果未提供密码,从数据库获取已保存的密码
956-
password := req.SMTPPassword
957-
if password == "" {
958-
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
959-
if err == nil && savedConfig != nil {
960-
password = savedConfig.Password
994+
if req.SMTPHost == "" && savedConfig != nil {
995+
req.SMTPHost = savedConfig.Host
996+
}
997+
if req.SMTPPort <= 0 {
998+
if savedConfig != nil && savedConfig.Port > 0 {
999+
req.SMTPPort = savedConfig.Port
1000+
} else {
1001+
req.SMTPPort = 587
9611002
}
9621003
}
1004+
if req.SMTPUsername == "" && savedConfig != nil {
1005+
req.SMTPUsername = savedConfig.Username
1006+
}
1007+
password := strings.TrimSpace(req.SMTPPassword)
1008+
if password == "" && savedConfig != nil {
1009+
password = savedConfig.Password
1010+
}
1011+
if req.SMTPFrom == "" && savedConfig != nil {
1012+
req.SMTPFrom = savedConfig.From
1013+
}
1014+
if req.SMTPFromName == "" && savedConfig != nil {
1015+
req.SMTPFromName = savedConfig.FromName
1016+
}
1017+
if req.SMTPHost == "" {
1018+
response.BadRequest(c, "SMTP host is required")
1019+
return
1020+
}
9631021

9641022
config := &service.SMTPConfig{
9651023
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
@@ -1655,7 +1655,7 @@
16551655
<button
16561656
type="button"
16571657
@click="testSmtpConnection"
1658-
:disabled="testingSmtp"
1658+
:disabled="testingSmtp || loadFailed"
16591659
class="btn btn-secondary btn-sm"
16601660
>
16611661
<svg v-if="testingSmtp" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
@@ -1725,6 +1725,11 @@
17251725
v-model="form.smtp_password"
17261726
type="password"
17271727
class="input"
1728+
autocomplete="new-password"
1729+
autocapitalize="off"
1730+
spellcheck="false"
1731+
@keydown="smtpPasswordManuallyEdited = true"
1732+
@paste="smtpPasswordManuallyEdited = true"
17281733
:placeholder="
17291734
form.smtp_password_configured
17301735
? t('admin.settings.smtp.passwordConfiguredPlaceholder')
@@ -1807,7 +1812,7 @@
18071812
<button
18081813
type="button"
18091814
@click="sendTestEmail"
1810-
:disabled="sendingTestEmail || !testEmailAddress"
1815+
:disabled="sendingTestEmail || !testEmailAddress || loadFailed"
18111816
class="btn btn-secondary"
18121817
>
18131818
<svg
@@ -1853,7 +1858,7 @@
18531858

18541859
<!-- Save Button -->
18551860
<div v-show="activeTab !== 'backup' && activeTab !== 'data'" class="flex justify-end">
1856-
<button type="submit" :disabled="saving" class="btn btn-primary">
1861+
<button type="submit" :disabled="saving || loadFailed" class="btn btn-primary">
18571862
<svg v-if="saving" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
18581863
<circle
18591864
class="opacity-25"
@@ -1924,9 +1929,11 @@ const settingsTabs = [
19241929
const { copyToClipboard } = useClipboard()
19251930
19261931
const loading = ref(true)
1932+
const loadFailed = ref(false)
19271933
const saving = ref(false)
19281934
const testingSmtp = ref(false)
19291935
const sendingTestEmail = ref(false)
1936+
const smtpPasswordManuallyEdited = ref(false)
19301937
const testEmailAddress = ref('')
19311938
const registrationEmailSuffixWhitelistTags = ref<string[]>([])
19321939
const registrationEmailSuffixWhitelistDraft = ref('')
@@ -2201,6 +2208,7 @@ function removeEndpoint(index: number) {
22012208
22022209
async function loadSettings() {
22032210
loading.value = true
2211+
loadFailed.value = false
22042212
try {
22052213
const settings = await adminAPI.settings.getSettings()
22062214
Object.assign(form, settings)
@@ -2218,9 +2226,11 @@ async function loadSettings() {
22182226
)
22192227
registrationEmailSuffixWhitelistDraft.value = ''
22202228
form.smtp_password = ''
2229+
smtpPasswordManuallyEdited.value = false
22212230
form.turnstile_secret_key = ''
22222231
form.linuxdo_connect_client_secret = ''
22232232
} catch (error: any) {
2233+
loadFailed.value = true
22242234
appStore.showError(
22252235
t('admin.settings.failedToLoad') + ': ' + (error.message || t('common.unknownError'))
22262236
)
@@ -2372,6 +2382,7 @@ async function saveSettings() {
23722382
)
23732383
registrationEmailSuffixWhitelistDraft.value = ''
23742384
form.smtp_password = ''
2385+
smtpPasswordManuallyEdited.value = false
23752386
form.turnstile_secret_key = ''
23762387
form.linuxdo_connect_client_secret = ''
23772388
// Refresh cached settings so sidebar/header update immediately
@@ -2390,11 +2401,12 @@ async function saveSettings() {
23902401
async function testSmtpConnection() {
23912402
testingSmtp.value = true
23922403
try {
2404+
const smtpPasswordForTest = smtpPasswordManuallyEdited.value ? form.smtp_password : ''
23932405
const result = await adminAPI.settings.testSmtpConnection({
23942406
smtp_host: form.smtp_host,
23952407
smtp_port: form.smtp_port,
23962408
smtp_username: form.smtp_username,
2397-
smtp_password: form.smtp_password,
2409+
smtp_password: smtpPasswordForTest,
23982410
smtp_use_tls: form.smtp_use_tls
23992411
})
24002412
// API returns { message: "..." } on success, errors are thrown as exceptions
@@ -2416,12 +2428,13 @@ async function sendTestEmail() {
24162428
24172429
sendingTestEmail.value = true
24182430
try {
2431+
const smtpPasswordForSend = smtpPasswordManuallyEdited.value ? form.smtp_password : ''
24192432
const result = await adminAPI.settings.sendTestEmail({
24202433
email: testEmailAddress.value,
24212434
smtp_host: form.smtp_host,
24222435
smtp_port: form.smtp_port,
24232436
smtp_username: form.smtp_username,
2424-
smtp_password: form.smtp_password,
2437+
smtp_password: smtpPasswordForSend,
24252438
smtp_from_email: form.smtp_from_email,
24262439
smtp_from_name: form.smtp_from_name,
24272440
smtp_use_tls: form.smtp_use_tls

0 commit comments

Comments
 (0)