Merge pull request #1204 from Eilen6316/fix/smtp-config-stability-and-refresh-test
fix(settings): prevent SMTP config overwrite and stabilize SMTP test after refresh
This commit is contained in:
@@ -233,11 +233,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 {
|
||||||
// 检查必填字段
|
// 检查必填字段
|
||||||
@@ -881,7 +897,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"`
|
||||||
@@ -897,18 +913,35 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.SMTPPort <= 0 {
|
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
||||||
req.SMTPPort = 587
|
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 {
|
||||||
password := req.SMTPPassword
|
req.SMTPHost = savedConfig.Host
|
||||||
if password == "" {
|
}
|
||||||
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
|
if req.SMTPPort <= 0 {
|
||||||
if err == nil && savedConfig != nil {
|
if savedConfig != nil && savedConfig.Port > 0 {
|
||||||
password = savedConfig.Password
|
req.SMTPPort = savedConfig.Port
|
||||||
|
} else {
|
||||||
|
req.SMTPPort = 587
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if req.SMTPUsername == "" && savedConfig != nil {
|
||||||
|
req.SMTPUsername = savedConfig.Username
|
||||||
|
}
|
||||||
|
password := strings.TrimSpace(req.SMTPPassword)
|
||||||
|
if password == "" && savedConfig != nil {
|
||||||
|
password = savedConfig.Password
|
||||||
|
}
|
||||||
|
if req.SMTPHost == "" {
|
||||||
|
response.BadRequest(c, "SMTP host is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
config := &service.SMTPConfig{
|
config := &service.SMTPConfig{
|
||||||
Host: req.SMTPHost,
|
Host: req.SMTPHost,
|
||||||
@@ -930,7 +963,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"`
|
||||||
@@ -948,18 +981,43 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.SMTPPort <= 0 {
|
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
||||||
req.SMTPPort = 587
|
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 {
|
||||||
password := req.SMTPPassword
|
req.SMTPHost = savedConfig.Host
|
||||||
if password == "" {
|
}
|
||||||
savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
|
if req.SMTPPort <= 0 {
|
||||||
if err == nil && savedConfig != nil {
|
if savedConfig != nil && savedConfig.Port > 0 {
|
||||||
password = savedConfig.Password
|
req.SMTPPort = savedConfig.Port
|
||||||
|
} else {
|
||||||
|
req.SMTPPort = 587
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if req.SMTPUsername == "" && savedConfig != nil {
|
||||||
|
req.SMTPUsername = savedConfig.Username
|
||||||
|
}
|
||||||
|
password := strings.TrimSpace(req.SMTPPassword)
|
||||||
|
if password == "" && savedConfig != nil {
|
||||||
|
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{
|
||||||
Host: req.SMTPHost,
|
Host: req.SMTPHost,
|
||||||
|
|||||||
@@ -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) {
|
|||||||
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) {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1655,7 +1655,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">
|
||||||
@@ -1725,6 +1725,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')
|
||||||
@@ -1807,7 +1812,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
|
||||||
@@ -1853,7 +1858,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"
|
||||||
@@ -1924,9 +1929,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('')
|
||||||
@@ -2201,6 +2208,7 @@ function removeEndpoint(index: number) {
|
|||||||
|
|
||||||
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)
|
||||||
@@ -2218,9 +2226,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'))
|
||||||
)
|
)
|
||||||
@@ -2372,6 +2382,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
|
||||||
@@ -2390,11 +2401,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
|
||||||
@@ -2416,12 +2428,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
|
||||||
|
|||||||
Reference in New Issue
Block a user