mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-30 06:57:14 +00:00
feat(ui): tag settings that sit at their shipped default value (#6128)
* feat(ui): tag settings that sit at their shipped default value A field showing 2096 reads identically whether the install never set it or the operator saved 2096 — newcomers cannot tell which knobs they have touched, and after the cleared-port fix (#6121) a port can never visually return to an unset state. Add a small grey tag next to numeric settings whose current value equals the shipped default. The tag deliberately compares values, not provenance: a stored 2096 and a fallback 2096 behave identically, so they read identically, and the tag reacts live as the user types. The backing endpoint filters defaultValueMap through the AllSetting field set, so per-install material (secret, panelGuid, node mTLS keys) and redacted credential fields never leave the server; a test pins that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): keep the default tag out of the accessible name, pin the defaults contract From review, in order of severity: The badge was rendered inside the element whose id feeds the control's aria-labelledby, so a visible tag changed every field's accessible name ('Panel Port Default'). The title text now carries the id on its own span and the badge sits beside it. The same default values live in three places: the Go defaultValueMap, the frontend AllSetting class, and the tag's verdict. A new contract test parses the Go map's string literals and asserts every shared key matches the AllSetting class default through the tag's own comparison — and on first run it caught two real drifts (tgEnabledEvents / smtpEnabledEvents defaulted to '' in the class but 'login.attempt,cpu.high' on the server), now aligned. matchesFactoryDefault no longer coerces blank or unparsable defaults (Number('') is 0; a junk string is not false). The Go tests are table-driven t.Run subtests and gained the structural invariant: every returned key is an AllSetting json tag outside the credential deny-list. The service doc comment now describes the projection mechanism instead of overclaiming; the i18n key is re-indented and placed at the head of pages.settings in all 13 locales; the fetch falls back to {} when validation fails; and smtpPort gets the tag so plain numeric settings-list fields are covered uniformly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.POST("/all", a.getAllSetting)
|
||||
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||
g.POST("/factoryDefaults", a.getFactoryDefaults)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/validateRegex", a.validateRegex)
|
||||
g.POST("/updateUser", a.updateUser)
|
||||
@@ -112,6 +113,10 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) getFactoryDefaults(c *gin.Context) {
|
||||
jsonObj(c, a.settingService.GetFactoryDefaults(), nil)
|
||||
}
|
||||
|
||||
// updateSetting updates all settings with the provided data.
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
form, ok := middleware.BindAndValidate[updateSettingForm](c)
|
||||
|
||||
@@ -1445,3 +1445,33 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var factoryDefaultSecretKeys = map[string]bool{
|
||||
"tgBotToken": true,
|
||||
"twoFactorToken": true,
|
||||
"ldapPassword": true,
|
||||
"smtpPassword": true,
|
||||
}
|
||||
|
||||
/*
|
||||
GetFactoryDefaults returns the shipped default value per setting, keyed by
|
||||
the AllSetting json field name. Unlike GetDefaultSettings (which reports
|
||||
current effective values), this is defaultValueMap projected through the
|
||||
AllSetting field set: only keys that exist as an AllSetting json tag are
|
||||
returned, minus the credential fields in factoryDefaultSecretKeys. Keys
|
||||
with no AllSetting field (secret, panelGuid, the node mTLS material,
|
||||
xrayTemplateConfig) are excluded structurally rather than by deny-list.
|
||||
*/
|
||||
func (s *SettingService) GetFactoryDefaults() map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, field := range reflect_util.GetFields(reflect.TypeFor[entity.AllSetting]()) {
|
||||
key := field.Tag.Get("json")
|
||||
if key == "" || factoryDefaultSecretKeys[key] {
|
||||
continue
|
||||
}
|
||||
if value, ok := defaultValueMap[key]; ok {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/reflect_util"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
||||
)
|
||||
|
||||
func allSettingJSONTags(t *testing.T) map[string]bool {
|
||||
t.Helper()
|
||||
tags := make(map[string]bool)
|
||||
for _, field := range reflect_util.GetFields(reflect.TypeFor[entity.AllSetting]()) {
|
||||
if tag := field.Tag.Get("json"); tag != "" {
|
||||
tags[tag] = true
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func TestGetFactoryDefaultsExposesBrowserSafeKeys(t *testing.T) {
|
||||
defaults := (&SettingService{}).GetFactoryDefaults()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{key: "webPort", want: "2053"},
|
||||
{key: "subPort", want: "2096"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.key, func(t *testing.T) {
|
||||
got, ok := defaults[tc.key]
|
||||
if !ok {
|
||||
t.Fatalf("expected key %q in factory defaults", tc.key)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("factory default for %q = %q, want %q", tc.key, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFactoryDefaultsOmitsSensitiveMaterial(t *testing.T) {
|
||||
defaults := (&SettingService{}).GetFactoryDefaults()
|
||||
|
||||
for _, key := range []string{
|
||||
"secret",
|
||||
"panelGuid",
|
||||
"nodeMtlsCaCertPem",
|
||||
"nodeMtlsCaKeyPem",
|
||||
"nodeMtlsClientCertPem",
|
||||
"nodeMtlsClientKeyPem",
|
||||
"xrayTemplateConfig",
|
||||
"tgBotToken",
|
||||
"twoFactorToken",
|
||||
"ldapPassword",
|
||||
"smtpPassword",
|
||||
} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
if _, ok := defaults[key]; ok {
|
||||
t.Errorf("factory defaults must not expose %q", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFactoryDefaultsInvariant(t *testing.T) {
|
||||
defaults := (&SettingService{}).GetFactoryDefaults()
|
||||
tags := allSettingJSONTags(t)
|
||||
|
||||
for key := range defaults {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
if !tags[key] {
|
||||
t.Errorf("key %q is not an entity.AllSetting json tag", key)
|
||||
}
|
||||
if factoryDefaultSecretKeys[key] {
|
||||
t.Errorf("key %q is in the credential deny-list and must not be returned", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "تعذّر جلب الشهادة"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "افتراضي",
|
||||
"title": "إعدادات البانل",
|
||||
"save": "حفظ",
|
||||
"infoDesc": "كل تغيير هتعمله هنا لازم يتخزن. ياريت تعيد تشغيل البانل عشان التعديلات تتفعل.",
|
||||
|
||||
@@ -1115,6 +1115,7 @@
|
||||
"pinFetchFailed": "Could not fetch the certificate"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Default",
|
||||
"title": "Panel Settings",
|
||||
"save": "Save",
|
||||
"infoDesc": "Every change made here needs to be saved. Please restart the panel to apply changes.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "No se pudo obtener el certificado"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Predeterminado",
|
||||
"title": "Configuraciones",
|
||||
"save": "Guardar",
|
||||
"infoDesc": "Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "دریافت گواهی ممکن نشد"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "پیشفرض",
|
||||
"title": "تنظیمات پنل",
|
||||
"save": "ذخیره",
|
||||
"infoDesc": "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "Tidak dapat mengambil sertifikat"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Bawaan",
|
||||
"title": "Pengaturan Panel",
|
||||
"save": "Simpan",
|
||||
"infoDesc": "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "証明書を取得できませんでした"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "デフォルト",
|
||||
"title": "パネル設定",
|
||||
"save": "保存",
|
||||
"infoDesc": "ここでのすべての変更は、保存してパネルを再起動する必要があります",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "Não foi possível obter o certificado"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Padrão",
|
||||
"title": "Configurações do Painel",
|
||||
"save": "Salvar",
|
||||
"infoDesc": "Toda alteração feita aqui precisa ser salva. Reinicie o painel para aplicar as alterações.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "Не удалось получить сертификат"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "По умолчанию",
|
||||
"title": "Настройки",
|
||||
"save": "Сохранить",
|
||||
"infoDesc": "Сохраните изменения и перезапустите панель для их применения.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "Sertifika alınamadı"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Varsayılan",
|
||||
"title": "Panel Ayarları",
|
||||
"save": "Kaydet",
|
||||
"infoDesc": "Burada yapılan her değişikliğin kaydedilmesi gerekir. Değişikliklerin uygulanması için paneli yeniden başlatın.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "Не вдалося отримати сертифікат"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Типово",
|
||||
"title": "Параметри панелі",
|
||||
"save": "Зберегти",
|
||||
"infoDesc": "Кожна внесена тут зміна повинна бути збережена. Перезапустіть панель, щоб застосувати зміни.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "Không thể lấy chứng chỉ"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Mặc định",
|
||||
"title": "Cài đặt",
|
||||
"save": "Lưu",
|
||||
"infoDesc": "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi.",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "无法获取证书"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "默认",
|
||||
"title": "面板设置",
|
||||
"save": "保存",
|
||||
"infoDesc": "此处的所有更改都需要保存并重启面板才能生效",
|
||||
|
||||
@@ -998,6 +998,7 @@
|
||||
"pinFetchFailed": "無法取得憑證"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "預設",
|
||||
"title": "面板設定",
|
||||
"save": "儲存",
|
||||
"infoDesc": "此處的所有更改都需要儲存並重啟面板才能生效",
|
||||
|
||||
Reference in New Issue
Block a user