fix(security): demo sends nothing, hidden SMTP password, body limits (FLAW-33..38)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
- Demo: senders for mail, Discord and webhooks refuse, no alarm ticker; test alarm, SMTP test, server settings, backups, account and session changes of the shared demo accounts answer 403; the session list only shows the own device. The UI shows the notices and disables the controls. - SMTP password is never sent back (smtpPassSet), empty keeps it. - ?token= on webhook ingest accepts only API keys. - Request bodies capped at 2 MB (sync 8 MB, imports 101 MB). - Deleting an account removes its server backups. - Backup downloads, exports and import uploads get 30 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3709282a4f
commit
9d9698186c
21 files changed
+631
-304
No files matched your search
@@ -146,8 +146,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Read-only in the public demo (FLAW-33)
|
||||
const [demoMode, setDemoMode] = useState(false);
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
api
|
||||
.getAuthStatus()
|
||||
.then((st) => setDemoMode(Boolean(st.demoMode)))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
@@ -519,6 +525,15 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
{/* Right Content Area */}
|
||||
<div className="md:col-span-3 panel-surface p-5 text-xs">
|
||||
{demoMode && (
|
||||
<div className="flex items-center gap-2 alert-err mb-4">
|
||||
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
|
||||
<span>
|
||||
In der Demo deaktiviert: Server-Einstellungen, E-Mail und Konten lassen sich hier nur
|
||||
ansehen.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION 1: BENUTZER, ROLLEN & EINLADUNGEN */}
|
||||
{/* ================================================================= */}
|
||||
@@ -1013,300 +1028,305 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{/* SECTION 3: SERVER & SHARING-ISOLATION (SERVER-ADMIN ONLY) */}
|
||||
{/* ================================================================= */}
|
||||
{section === 'server' && isServerAdmin && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
Server-Grundeinstellungen & Benutzer-Sichtbarkeit
|
||||
</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Als Server-Admin bestimmst du zentral, ob Nutzer komplett isoliert arbeiten, sich nur
|
||||
innerhalb ihrer Organisation sehen oder ob alle Nutzer für alle sichtbar sind.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-muted font-semibold mb-1">
|
||||
Name der MindSift-Instanz
|
||||
</label>
|
||||
<div className="flex gap-2 max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.instanceName}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, instanceName: e.target.value }))
|
||||
}
|
||||
className="flex-1 h-9 px-3 input-field"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveSystemSettings()}
|
||||
className="btn-primary h-9 px-4 text-xs"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<fieldset disabled={demoMode} className="min-w-0 border-0 p-0 m-0">
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
Server-Grundeinstellungen & Benutzer-Sichtbarkeit
|
||||
</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Als Server-Admin bestimmst du zentral, ob Nutzer komplett isoliert arbeiten, sich nur
|
||||
innerhalb ihrer Organisation sehen oder ob alle Nutzer für alle sichtbar sind.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sharing Isolation Cards */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-muted font-semibold flex items-center gap-1.5">
|
||||
<Users className="w-4 h-4 text-accent" />
|
||||
<span>Globale Sharing- & Sichtbarkeits-Isolation</span>
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{SHARING_MODES.map((m) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-muted font-semibold mb-1">
|
||||
Name der MindSift-Instanz
|
||||
</label>
|
||||
<div className="flex gap-2 max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.instanceName}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, instanceName: e.target.value }))
|
||||
}
|
||||
className="flex-1 h-9 px-3 input-field"
|
||||
/>
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectSharingMode(m.id)}
|
||||
className={`p-4 text-left border rounded-card transition-colors ${
|
||||
settings.sharingMode === m.id
|
||||
? 'bg-canvas border-accent text-main shadow-panel'
|
||||
: 'bg-canvas/50 border-border text-muted hover:text-main'
|
||||
}`}
|
||||
onClick={() => handleSaveSystemSettings()}
|
||||
className="btn-primary h-9 px-4 text-xs"
|
||||
>
|
||||
<p className="font-bold text-xs text-main">{m.title}</p>
|
||||
<p className="text-[11px] text-muted mt-1 leading-relaxed">{m.text}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pendingMode && (
|
||||
<div className="p-3 border border-status-warn/40 bg-status-warn/5 rounded-card flex flex-wrap items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-warn shrink-0" />
|
||||
<span className="flex-1 min-w-0 text-main">
|
||||
Beim Wechsel zu „{SHARING_MODES.find((m) => m.id === pendingMode.mode)?.title.replace(/^\d\.\s*/, '')}“
|
||||
fallen {pendingMode.affected} Freigaben und Gruppen-Mitgliedschaften weg, die danach nicht mehr erlaubt sind.
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const mode = pendingMode.mode;
|
||||
setPendingMode(null);
|
||||
handleSaveSystemSettings({ sharingMode: mode });
|
||||
}}
|
||||
className="btn-primary h-7 px-3 text-[11px]"
|
||||
>
|
||||
Trotzdem wechseln
|
||||
</button>
|
||||
<button onClick={() => setPendingMode(null)} className="btn-action h-7 px-2 text-[11px]">
|
||||
Abbrechen
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BackupAdminCard settings={settings} onSave={(patch) => handleSaveSystemSettings(patch)} />
|
||||
|
||||
{/* Alarm webhooks into the local network (SSRF protection) */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
|
||||
<div className="pr-4">
|
||||
<p className="font-bold text-main">Alarm-Webhooks ins lokale Netz erlauben</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
Standard: Discord- und Webhook-Alarme gehen nur an öffentliche https-Adressen. Eingeschaltet sind auch Ziele
|
||||
im Heimnetz erlaubt (z. B. Home Assistant unter 192.168.x.x oder über Tailscale), dort auch per http.
|
||||
Der Server selbst (localhost) und Cloud-Metadaten bleiben immer gesperrt.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={Boolean(settings.allowPrivateWebhooks)}
|
||||
onChange={(allowPrivateWebhooks) => handleSaveSystemSettings({ allowPrivateWebhooks })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Public Registration Toggle Switch */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
|
||||
<div className="pr-4">
|
||||
<p className="font-bold text-main">Öffentliche Selbstregistrierung erlauben</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
Wenn deaktiviert, können sich fremde Personen auf dem Login-Bildschirm nicht selbst registrieren.
|
||||
Neue Konten erfordern dann zwingend einen Einladungscode oder müssen von einem Admin angelegt werden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<span className={`text-xs font-medium hidden sm:inline ${settings.allowRegistration ? 'text-accent' : 'text-muted'}`}>
|
||||
{settings.allowRegistration ? 'Aktiviert (Offen)' : 'Gesperrt (Nur Einladung)'}
|
||||
</span>
|
||||
{/* Sharing Isolation Cards */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-muted font-semibold flex items-center gap-1.5">
|
||||
<Users className="w-4 h-4 text-accent" />
|
||||
<span>Globale Sharing- & Sichtbarkeits-Isolation</span>
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{SHARING_MODES.map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectSharingMode(m.id)}
|
||||
className={`p-4 text-left border rounded-card transition-colors ${
|
||||
settings.sharingMode === m.id
|
||||
? 'bg-canvas border-accent text-main shadow-panel'
|
||||
: 'bg-canvas/50 border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<p className="font-bold text-xs text-main">{m.title}</p>
|
||||
<p className="text-[11px] text-muted mt-1 leading-relaxed">{m.text}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pendingMode && (
|
||||
<div className="p-3 border border-status-warn/40 bg-status-warn/5 rounded-card flex flex-wrap items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-warn shrink-0" />
|
||||
<span className="flex-1 min-w-0 text-main">
|
||||
Beim Wechsel zu „{SHARING_MODES.find((m) => m.id === pendingMode.mode)?.title.replace(/^\d\.\s*/, '')}“
|
||||
fallen {pendingMode.affected} Freigaben und Gruppen-Mitgliedschaften weg, die danach nicht mehr erlaubt sind.
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const mode = pendingMode.mode;
|
||||
setPendingMode(null);
|
||||
handleSaveSystemSettings({ sharingMode: mode });
|
||||
}}
|
||||
className="btn-primary h-7 px-3 text-[11px]"
|
||||
>
|
||||
Trotzdem wechseln
|
||||
</button>
|
||||
<button onClick={() => setPendingMode(null)} className="btn-action h-7 px-2 text-[11px]">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BackupAdminCard settings={settings} onSave={(patch) => handleSaveSystemSettings(patch)} />
|
||||
|
||||
{/* Alarm webhooks into the local network (SSRF protection) */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
|
||||
<div className="pr-4">
|
||||
<p className="font-bold text-main">Alarm-Webhooks ins lokale Netz erlauben</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
Standard: Discord- und Webhook-Alarme gehen nur an öffentliche https-Adressen. Eingeschaltet sind auch Ziele
|
||||
im Heimnetz erlaubt (z. B. Home Assistant unter 192.168.x.x oder über Tailscale), dort auch per http.
|
||||
Der Server selbst (localhost) und Cloud-Metadaten bleiben immer gesperrt.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={settings.allowRegistration}
|
||||
onChange={(allowRegistration) => handleSaveSystemSettings({ allowRegistration })}
|
||||
title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'}
|
||||
checked={Boolean(settings.allowPrivateWebhooks)}
|
||||
onChange={(allowPrivateWebhooks) => handleSaveSystemSettings({ allowPrivateWebhooks })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Public Registration Toggle Switch */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
|
||||
<div className="pr-4">
|
||||
<p className="font-bold text-main">Öffentliche Selbstregistrierung erlauben</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
Wenn deaktiviert, können sich fremde Personen auf dem Login-Bildschirm nicht selbst registrieren.
|
||||
Neue Konten erfordern dann zwingend einen Einladungscode oder müssen von einem Admin angelegt werden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<span className={`text-xs font-medium hidden sm:inline ${settings.allowRegistration ? 'text-accent' : 'text-muted'}`}>
|
||||
{settings.allowRegistration ? 'Aktiviert (Offen)' : 'Gesperrt (Nur Einladung)'}
|
||||
</span>
|
||||
<ToggleSwitch
|
||||
checked={settings.allowRegistration}
|
||||
onChange={(allowRegistration) => handleSaveSystemSettings({ allowRegistration })}
|
||||
title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION 4: E-MAIL / SMTP-SERVER (SERVER-ADMIN ONLY) */}
|
||||
{/* ================================================================= */}
|
||||
{section === 'smtp' && isServerAdmin && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
E-Mail / SMTP-Server für Aufgaben-Alarme
|
||||
</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder
|
||||
direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert).
|
||||
</p>
|
||||
<fieldset disabled={demoMode} className="min-w-0 border-0 p-0 m-0">
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
E-Mail / SMTP-Server für Aufgaben-Alarme
|
||||
</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder
|
||||
direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{settings.smtpConfiguredViaEnv && (
|
||||
<span className="badge-ok text-[11px] flex-shrink-0">
|
||||
Docker Env erkannt
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{settings.smtpConfiguredViaEnv && (
|
||||
<span className="badge-ok text-[11px] flex-shrink-0">
|
||||
Docker Env erkannt
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Host (`SMTP_HOST`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpHost || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpHost: e.target.value }))
|
||||
}
|
||||
placeholder="z.B. smtp.example.com oder mail.deinedomain.de"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Port (`SMTP_PORT`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={settings.smtpPort ?? ''}
|
||||
onChange={(e) =>
|
||||
// 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Benutzername (`SMTP_USER`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpUser || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpUser: e.target.value }))
|
||||
}
|
||||
placeholder="alerts@deinedomain.de"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Passwort (`SMTP_PASS`)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={settings.smtpPass || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpPass: e.target.value }))
|
||||
}
|
||||
placeholder="••••••••••••"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
Absender-Adresse (`SMTP_FROM`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpFrom || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpFrom: e.target.value }))
|
||||
}
|
||||
placeholder="MindSift <alerts@deinedomain.de>"
|
||||
className="w-full h-9 px-3 input-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 space-y-3 pt-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-main font-medium">Verschlüsselung (`SMTP_TLS`)</p>
|
||||
<p className="text-[11px] text-muted">
|
||||
Port 465: TLS ab Verbindungsbeginn, sonst STARTTLS. Ist es an, wird ohne Verschlüsselung nicht gesendet.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={settings.smtpTls ?? true}
|
||||
onChange={(smtpTls) => setSettings((prev) => ({ ...prev, smtpTls }))}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Host (`SMTP_HOST`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpHost || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpHost: e.target.value }))
|
||||
}
|
||||
placeholder="z.B. smtp.example.com oder mail.deinedomain.de"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-main font-medium">Zertifikatsprüfung deaktivieren (`SMTP_SKIP_VERIFY`)</p>
|
||||
<p className="text-[11px] text-muted">
|
||||
Nur für interne Mailserver mit selbstsigniertem Zertifikat oder abweichendem Namen. Die Verbindung
|
||||
bleibt verschlüsselt, der Server wird aber nicht mehr geprüft.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={settings.smtpSkipVerify ?? false}
|
||||
disabled={!(settings.smtpTls ?? true)}
|
||||
onChange={(smtpSkipVerify) => setSettings((prev) => ({ ...prev, smtpSkipVerify }))}
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Port (`SMTP_PORT`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={settings.smtpPort ?? ''}
|
||||
onChange={(e) =>
|
||||
// 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Benutzername (`SMTP_USER`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpUser || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpUser: e.target.value }))
|
||||
}
|
||||
placeholder="alerts@deinedomain.de"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Passwort (`SMTP_PASS`)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={settings.smtpPass || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpPass: e.target.value }))
|
||||
}
|
||||
placeholder={settings.smtpPassSet ? 'Gespeichert – leer lassen für keine Änderung' : '••••••••••••'}
|
||||
autoComplete="new-password"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
Absender-Adresse (`SMTP_FROM`)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpFrom || ''}
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpFrom: e.target.value }))
|
||||
}
|
||||
placeholder="MindSift <alerts@deinedomain.de>"
|
||||
className="w-full h-9 px-3 input-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 space-y-3 pt-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-main font-medium">Verschlüsselung (`SMTP_TLS`)</p>
|
||||
<p className="text-[11px] text-muted">
|
||||
Port 465: TLS ab Verbindungsbeginn, sonst STARTTLS. Ist es an, wird ohne Verschlüsselung nicht gesendet.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={settings.smtpTls ?? true}
|
||||
onChange={(smtpTls) => setSettings((prev) => ({ ...prev, smtpTls }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-main font-medium">Zertifikatsprüfung deaktivieren (`SMTP_SKIP_VERIFY`)</p>
|
||||
<p className="text-[11px] text-muted">
|
||||
Nur für interne Mailserver mit selbstsigniertem Zertifikat oder abweichendem Namen. Die Verbindung
|
||||
bleibt verschlüsselt, der Server wird aber nicht mehr geprüft.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={settings.smtpSkipVerify ?? false}
|
||||
disabled={!(settings.smtpTls ?? true)}
|
||||
onChange={(smtpSkipVerify) => setSettings((prev) => ({ ...prev, smtpSkipVerify }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-2 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveSystemSettings()}
|
||||
className="btn-primary h-9 px-5 text-xs font-semibold"
|
||||
>
|
||||
SMTP-Konfiguration in Datenbank speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Test SMTP Delivery */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-2">
|
||||
<h3 className="font-bold text-main">SMTP-Verbindung testen</h3>
|
||||
<p className="text-[11px] text-muted">
|
||||
Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={testSmtpEmail}
|
||||
onChange={(e) => setTestSmtpEmail(e.target.value)}
|
||||
placeholder="empfaenger@beispiel.de"
|
||||
className="flex-1 h-8 px-3 input-field"
|
||||
/>
|
||||
<div className="flex justify-end pt-2 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestSmtp}
|
||||
disabled={testingSmtp || !testSmtpEmail.trim()}
|
||||
className="btn-action h-8 px-4 text-xs flex items-center justify-center gap-1.5 disabled:opacity-40"
|
||||
onClick={() => handleSaveSystemSettings()}
|
||||
className="btn-primary h-9 px-5 text-xs font-semibold"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5 text-accent" />
|
||||
<span>{testingSmtp ? 'Sende Test-Mail…' : 'Test-E-Mail senden'}</span>
|
||||
SMTP-Konfiguration in Datenbank speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Test SMTP Delivery */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-2">
|
||||
<h3 className="font-bold text-main">SMTP-Verbindung testen</h3>
|
||||
<p className="text-[11px] text-muted">
|
||||
Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={testSmtpEmail}
|
||||
onChange={(e) => setTestSmtpEmail(e.target.value)}
|
||||
placeholder="empfaenger@beispiel.de"
|
||||
className="flex-1 h-8 px-3 input-field"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestSmtp}
|
||||
disabled={testingSmtp || !testSmtpEmail.trim()}
|
||||
className="btn-action h-8 px-4 text-xs flex items-center justify-center gap-1.5 disabled:opacity-40"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5 text-accent" />
|
||||
<span>{testingSmtp ? 'Sende Test-Mail…' : 'Test-E-Mail senden'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user