mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-17 16:50:58 +00:00
feat(settings): let users clear stored secrets from the UI
Redacted secrets (SMTP password, Telegram bot token, LDAP password) are always served blank to the browser, so the update path treats a blank submission as "unchanged" and silently restores the stored value. That made a once-set secret impossible to remove without editing the database — e.g. switching to a passwordless localhost SMTP relay kept sending the old credentials forever. Blank stays "unchanged"; clearing is now its own signal. The update request carries explicit clear flags (request-scoped fields on the controller form, so they are never persisted as settings rows), and preserveRedactedSecrets skips the restore for a flagged secret. Each secret field gets a Clear/Undo button that arms the flag; typing a new value disarms it. The 2FA token keeps its existing behavior: it is already clearable by disabling 2FA. Closes #5724
This commit is contained in:
@@ -1085,8 +1085,17 @@ func (s *SettingService) SetSmtpMemory(value int) error {
|
||||
return s.setInt("smtpMemory", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := s.preserveRedactedSecrets(allSetting); err != nil {
|
||||
// SecretClears marks redacted secrets the user explicitly emptied. Without a
|
||||
// flag, a blank submitted secret means "unchanged" (the field is always served
|
||||
// blank to the browser) and the stored value is preserved.
|
||||
type SecretClears struct {
|
||||
TgBotToken bool
|
||||
LdapPassword bool
|
||||
SmtpPassword bool
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting, clears SecretClears) error {
|
||||
if err := s.preserveRedactedSecrets(allSetting, clears); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSettingsURLs(allSetting); err != nil {
|
||||
@@ -1132,15 +1141,15 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting) error {
|
||||
if strings.TrimSpace(allSetting.TgBotToken) == "" {
|
||||
func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting, clears SecretClears) error {
|
||||
if !clears.TgBotToken && strings.TrimSpace(allSetting.TgBotToken) == "" {
|
||||
value, err := s.GetTgBotToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allSetting.TgBotToken = value
|
||||
}
|
||||
if strings.TrimSpace(allSetting.LdapPassword) == "" {
|
||||
if !clears.LdapPassword && strings.TrimSpace(allSetting.LdapPassword) == "" {
|
||||
value, err := s.GetLdapPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1154,7 +1163,7 @@ func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting)
|
||||
}
|
||||
allSetting.TwoFactorToken = value
|
||||
}
|
||||
if strings.TrimSpace(allSetting.SmtpPassword) == "" {
|
||||
if !clears.SmtpPassword && strings.TrimSpace(allSetting.SmtpPassword) == "" {
|
||||
value, err := s.GetSmtpPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestUpdateAllSettingPreservesRedactedSecrets(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := &view.AllSetting
|
||||
if err := s.UpdateAllSetting(settings); err != nil {
|
||||
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := s.GetTgBotToken(); got != "telegram-secret" {
|
||||
@@ -94,6 +94,54 @@ func TestUpdateAllSettingPreservesRedactedSecrets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAllSettingClearsFlaggedSecrets(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
s := &SettingService{}
|
||||
if err := s.saveSetting("tgBotToken", "telegram-secret"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.saveSetting("ldapPassword", "ldap-secret"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.saveSetting("smtpPassword", "smtp-secret"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
view, err := s.GetAllSettingView()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.UpdateAllSetting(&view.AllSetting, SecretClears{SmtpPassword: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := s.GetSmtpPassword(); got != "" {
|
||||
t.Fatalf("smtp password = %q, want cleared", got)
|
||||
}
|
||||
if got, _ := s.GetTgBotToken(); got != "telegram-secret" {
|
||||
t.Fatalf("tg token = %q, unflagged secret must stay preserved", got)
|
||||
}
|
||||
if got, _ := s.GetLdapPassword(); got != "ldap-secret" {
|
||||
t.Fatalf("ldap password = %q, unflagged secret must stay preserved", got)
|
||||
}
|
||||
|
||||
view, err = s.GetAllSettingView()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if view.HasSmtpPassword {
|
||||
t.Fatal("hasSmtpPassword must report false after clearing")
|
||||
}
|
||||
if err := s.UpdateAllSetting(&view.AllSetting, SecretClears{TgBotToken: true, LdapPassword: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := s.GetTgBotToken(); got != "" {
|
||||
t.Fatalf("tg token = %q, want cleared", got)
|
||||
}
|
||||
if got, _ := s.GetLdapPassword(); got != "" {
|
||||
t.Fatalf("ldap password = %q, want cleared", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizePublicHTTPURLBlocksPrivateAddressUnlessAllowed(t *testing.T) {
|
||||
if _, err := SanitizePublicHTTPURL("http://127.0.0.1:8080/hook", false); err == nil {
|
||||
t.Fatal("expected localhost URL to be blocked")
|
||||
|
||||
Reference in New Issue
Block a user