From a3a709440f1f71a9eec04101619d026ce488e765 Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 07:13:29 +0100 Subject: [PATCH] =?UTF-8?q?fix(admin):=20SMTP=20port=20can=20be=20cleared?= =?UTF-8?q?=20and=20saves=20without=20'Ung=C3=BCltiges=20JSON'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI sent the port as a number while the server expects text, so every save failed; the field also jumped back to 587 when emptied. The port is now edited as text (digits only), and the server accepts number or text. Co-Authored-By: Claude Opus 5.5 --- .../features/settings/AdminSettingsView.tsx | 12 ++++--- frontend/src/lib/types.ts | 2 +- internal/domain/models.go | 31 +++++++++++++++++++ internal/domain/tags_test.go | 17 ++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/frontend/src/features/settings/AdminSettingsView.tsx b/frontend/src/features/settings/AdminSettingsView.tsx index 05fc8a4..772be45 100644 --- a/frontend/src/features/settings/AdminSettingsView.tsx +++ b/frontend/src/features/settings/AdminSettingsView.tsx @@ -96,7 +96,7 @@ export const AdminSettingsView: React.FC = ({ allowRegistration: false, instanceName: 'MindSift', smtpHost: '', - smtpPort: 587, + smtpPort: '587', smtpUser: '', smtpPass: '', smtpFrom: '', @@ -126,7 +126,7 @@ export const AdminSettingsView: React.FC = ({ allowRegistration: Boolean(sys.allowRegistration), instanceName: sys.instanceName || 'MindSift', smtpHost: sys.smtpHost || '', - smtpPort: sys.smtpPort || 587, + smtpPort: sys.smtpPort ? String(sys.smtpPort) : '587', smtpUser: sys.smtpUser || '', smtpPass: sys.smtpPass || '', smtpFrom: sys.smtpFrom || '', @@ -1150,10 +1150,12 @@ export const AdminSettingsView: React.FC = ({ SMTP Port (`SMTP_PORT`) - setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) })) + // Digits only; an empty field stays empty while typing (saved as 587) + setSettings((prev) => ({ ...prev, smtpPort: e.target.value.replace(/\D/g, '').slice(0, 5) })) } placeholder="587" className="w-full h-9 px-3 input-field font-mono" diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index fe48e41..488d8e4 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -161,7 +161,7 @@ export interface SystemSettings { allowRegistration: boolean; instanceName: string; smtpHost?: string; - smtpPort?: number; + smtpPort?: string; // sent as text, like the server stores it smtpUser?: string; smtpPass?: string; smtpFrom?: string; diff --git a/internal/domain/models.go b/internal/domain/models.go index 0b21eb2..307a311 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -3,6 +3,7 @@ package domain import ( "crypto/rand" "encoding/hex" + "encoding/json" "fmt" "regexp" "strings" @@ -289,6 +290,36 @@ type SystemSettings struct { SMTPConfiguredViaEnv bool `json:"smtpConfiguredViaEnv,omitempty"` } +// UnmarshalJSON accepts smtpPort as text or number: older clients sent a number, which made the whole +// settings request fail with "Ungültiges JSON". +func (s *SystemSettings) UnmarshalJSON(data []byte) error { + type plain SystemSettings + aux := struct { + *plain + SMTPPort json.RawMessage `json:"smtpPort"` + }{plain: (*plain)(s)} + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + port := strings.TrimSpace(string(aux.SMTPPort)) + switch { + case port == "" || port == "null": + case strings.HasPrefix(port, `"`): + var str string + if err := json.Unmarshal(aux.SMTPPort, &str); err != nil { + return err + } + s.SMTPPort = strings.TrimSpace(str) + default: + var n json.Number + if err := json.Unmarshal(aux.SMTPPort, &n); err != nil { + return err + } + s.SMTPPort = n.String() + } + return nil +} + type DocumentBlock struct { ID string `json:"id"` DocumentID string `json:"documentId,omitempty"` diff --git a/internal/domain/tags_test.go b/internal/domain/tags_test.go index 37ab2f2..4be0d9d 100644 --- a/internal/domain/tags_test.go +++ b/internal/domain/tags_test.go @@ -1,6 +1,7 @@ package domain import ( + "encoding/json" "reflect" "testing" ) @@ -32,3 +33,19 @@ func TestStaleAnchorTags(t *testing.T) { t.Fatalf("only tags that exist solely as anchors are stale, got %v", got) } } + +func TestSystemSettingsAcceptsPortAsNumberOrText(t *testing.T) { + for _, body := range []string{`{"smtpPort": 465, "smtpHost": "mail"}`, `{"smtpPort": "465", "smtpHost": "mail"}`} { + var s SystemSettings + if err := json.Unmarshal([]byte(body), &s); err != nil { + t.Fatalf("%s: %v", body, err) + } + if s.SMTPPort != "465" || s.SMTPHost != "mail" { + t.Errorf("%s: got port %q host %q", body, s.SMTPPort, s.SMTPHost) + } + } + var empty SystemSettings + if err := json.Unmarshal([]byte(`{"smtpPort": ""}`), &empty); err != nil || empty.SMTPPort != "" { + t.Errorf("empty port: %q (%v)", empty.SMTPPort, err) + } +}