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

- 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:
sandrodachandClaude Opus 5.5 committed 2026-10-29 19:43:23 +01:00
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>
@@ -97,8 +97,16 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
}
};
const [showAbout, setShowAbout] = useState(false);
// The public demo sends nothing and keeps the shared accounts intact (FLAW-33)
const [demoMode, setDemoMode] = useState(false);
useEffect(() => {
api.getAuthStatus().then((st) => setLegal(st.legal)).catch(() => {});
api
.getAuthStatus()
.then((st) => {
setLegal(st.legal);
setDemoMode(Boolean(st.demoMode));
})
.catch(() => {});
}, []);
// --- Custom Theme Editor State ---
@@ -628,7 +636,9 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full h-9 px-3 input-field"
disabled={demoMode}
title={demoMode ? 'In der Demo deaktiviert' : undefined}
className="w-full h-9 px-3 input-field disabled:opacity-60"
/>
</div>
</div>
@@ -641,9 +651,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Mindestens 8 Zeichen"
placeholder={demoMode ? 'In der Demo deaktiviert' : 'Mindestens 8 Zeichen'}
autoComplete="new-password"
className="w-full h-9 px-3 input-field"
disabled={demoMode}
className="w-full h-9 px-3 input-field disabled:opacity-60"
/>
</div>
@@ -693,8 +704,9 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<button
type="button"
onClick={handleRevokeAllSessions}
disabled={revokingSessions}
className="btn-danger h-8 px-3 text-xs shrink-0 cursor-pointer"
disabled={demoMode || revokingSessions}
title={demoMode ? 'In der Demo deaktiviert' : undefined}
className="btn-danger h-8 px-3 text-xs shrink-0 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
>
{revokingSessions ? 'Beende…' : 'Alle Sitzungen beenden'}
</button>
@@ -735,7 +747,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<button
type="button"
onClick={() => openDanger('account')}
disabled={isLastAdmin}
disabled={isLastAdmin || demoMode}
title={demoMode ? 'In der Demo deaktiviert' : undefined}
className="btn-danger h-11 sm:h-8 px-3 text-xs shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
>
Konto löschen
@@ -968,6 +981,13 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</p>
</div>
{demoMode && (
<div className="flex items-center gap-2 alert-err">
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
<span>In der Demo deaktiviert: Alarme und Tests werden nicht verschickt.</span>
</div>
)}
{alarmMsg && (
<div
className={`flex items-center gap-2 ${
@@ -1034,8 +1054,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<button
type="button"
onClick={() => handleTestChannel('email')}
disabled={testingChannel === 'email'}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 flex-shrink-0"
disabled={demoMode || testingChannel === 'email'}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 flex-shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
>
<Send className="w-3 h-3 text-accent" />
<span>{testingChannel === 'email' ? 'Sende…' : 'E-Mail testen'}</span>
@@ -1083,8 +1103,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<button
type="button"
onClick={() => handleTestChannel('discord')}
disabled={testingChannel === 'discord'}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 flex-shrink-0"
disabled={demoMode || testingChannel === 'discord'}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 flex-shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
>
<Send className="w-3 h-3 text-accent" />
<span>{testingChannel === 'discord' ? 'Sende…' : 'Discord testen'}</span>
@@ -1132,8 +1152,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<button
type="button"
onClick={() => handleTestChannel('webhook')}
disabled={testingChannel === 'webhook'}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 flex-shrink-0"
disabled={demoMode || testingChannel === 'webhook'}
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5 flex-shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
>
<Send className="w-3 h-3 text-accent" />
<span>{testingChannel === 'webhook' ? 'Sende…' : 'Webhook testen'}</span>