Files
MindSift/frontend/src/features/settings/AdminSettingsView.tsx
T
sandrodach 444442fdf0
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
feat: integrate user profile pictures with circular avatar display and upload in settings
2026-10-25 21:37:06 +01:00

948 lines
39 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Shield,
Users,
Server,
Mail,
Plus,
Trash2,
KeyRound,
Copy,
Check,
ArrowLeft,
Send,
AlertCircle,
UserPlus,
} from 'lucide-react';
import {
SystemSettings,
User,
UserInvite,
UserRole,
} from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
import { UserAvatar } from '../../components/UserAvatar';
interface AdminSettingsViewProps {
currentUser: User;
onBack: () => void;
}
type AdminSection = 'users' | 'server' | 'smtp';
export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
currentUser,
onBack,
}) => {
const isServerAdmin = currentUser.role === 'admin';
const [section, setSection] = useState<AdminSection>('users');
// --- Users & Invites State ---
const [users, setUsers] = useState<User[]>([]);
const [invites, setInvites] = useState<UserInvite[]>([]);
const [newUserName, setNewUserName] = useState('');
const [newUserEmail, setNewUserEmail] = useState('');
const [newUserPassword, setNewUserPassword] = useState('');
const [newUserRole, setNewUserRole] = useState<UserRole>('member');
const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState<UserRole>('member');
const [copiedInviteId, setCopiedInviteId] = useState<string | null>(null);
const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null);
const [resetPwdVal, setResetPwdVal] = useState('');
// --- Server & SMTP Settings State ---
const [settings, setSettings] = useState<SystemSettings>({
sharingMode: 'teams_only',
allowRegistration: false,
instanceName: 'MindSift',
smtpHost: '',
smtpPort: 587,
smtpUser: '',
smtpPass: '',
smtpFrom: '',
smtpTls: true,
smtpConfiguredViaEnv: false,
});
const [testSmtpEmail, setTestSmtpEmail] = useState(currentUser.email || '');
const [testingSmtp, setTestingSmtp] = useState(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const loadAll = async () => {
try {
const [uList, invList] = await Promise.all([
api.adminGetUsers(),
api.adminGetInvites(),
]);
setUsers(uList);
setInvites(invList);
if (isServerAdmin) {
const sys = await api.adminGetSettings();
setSettings({
sharingMode: sys.sharingMode || 'teams_only',
allowRegistration: Boolean(sys.allowRegistration),
instanceName: sys.instanceName || 'MindSift',
smtpHost: sys.smtpHost || '',
smtpPort: sys.smtpPort || 587,
smtpUser: sys.smtpUser || '',
smtpPass: sys.smtpPass || '',
smtpFrom: sys.smtpFrom || '',
smtpTls: sys.smtpTls ?? true,
smtpConfiguredViaEnv: Boolean(sys.smtpConfiguredViaEnv),
});
}
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Laden der Administrationsdaten' });
}
};
useEffect(() => {
loadAll();
}, []);
// ============================================================================
// HANDLERS: USERS & INVITES
// ============================================================================
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault();
setStatusMsg(null);
triggerHaptic(15);
try {
const created = await api.adminCreateUser({
email: newUserEmail.trim(),
password: newUserPassword,
displayName: newUserName.trim() || newUserEmail.split('@')[0],
role: newUserRole,
});
setUsers((prev) => [...prev, created]);
setNewUserName('');
setNewUserEmail('');
setNewUserPassword('');
setNewUserRole('member');
setStatusMsg({ type: 'ok', text: `Benutzer "${created.displayName}" wurde angelegt.` });
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Anlegen des Benutzers' });
}
};
const handleCreateInvite = async (e: React.FormEvent) => {
e.preventDefault();
setStatusMsg(null);
triggerHaptic(15);
try {
const inv = await api.adminCreateInvite({
email: inviteEmail.trim(),
role: inviteRole,
});
setInvites((prev) => [inv, ...prev]);
setInviteEmail('');
setStatusMsg({
type: 'ok',
text: `Einladungscode ${inv.code} erfolgreich erstellt.`,
});
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Erstellen der Einladung' });
}
};
const handleDeleteInvite = async (id: string) => {
try {
await api.adminDeleteInvite(id);
setInvites((prev) => prev.filter((i) => i.id !== id));
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message });
}
};
const handleCopyInviteCode = (inv: UserInvite) => {
navigator.clipboard.writeText(inv.code);
setCopiedInviteId(inv.id);
setTimeout(() => setCopiedInviteId(null), 2000);
};
const handleChangeUserRole = async (u: User, role: UserRole) => {
setStatusMsg(null);
try {
const updated = await api.adminUpdateUser(u.id, { role });
setUsers((prev) => prev.map((item) => (item.id === u.id ? updated : item)));
setStatusMsg({
type: 'ok',
text: `Rolle für ${updated.displayName} auf "${role}" aktualisiert.`,
});
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message });
}
};
const handleToggleUserActive = async (u: User) => {
setStatusMsg(null);
try {
const updated = await api.adminUpdateUser(u.id, { isActive: !u.isActive });
setUsers((prev) => prev.map((item) => (item.id === u.id ? updated : item)));
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message });
}
};
const handleConfirmResetPassword = async (userId: string) => {
if (!resetPwdVal.trim()) return;
setStatusMsg(null);
try {
await api.adminResetPassword(userId, resetPwdVal.trim());
setResetPwdUserId(null);
setResetPwdVal('');
setStatusMsg({ type: 'ok', text: 'Passwort erfolgreich zurückgesetzt.' });
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message });
}
};
const handleDeleteUser = async (u: User) => {
if (
!window.confirm(
`Benutzer "${u.displayName}" (${u.email}) wirklich unwiderruflich löschen?`
)
)
return;
setStatusMsg(null);
try {
await api.adminDeleteUser(u.id);
setUsers((prev) => prev.filter((item) => item.id !== u.id));
setStatusMsg({ type: 'ok', text: 'Benutzer gelöscht.' });
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message });
}
};
// ============================================================================
// HANDLERS: SERVER & SMTP SETTINGS
// ============================================================================
const handleSaveSystemSettings = async (patch?: Partial<SystemSettings>) => {
setStatusMsg(null);
const next = { ...settings, ...(patch || {}) };
setSettings(next);
try {
const saved = await api.adminUpdateSettings(next);
setSettings((prev) => ({ ...prev, ...saved }));
setStatusMsg({ type: 'ok', text: 'Server-Einstellungen erfolgreich gespeichert.' });
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Speichern' });
}
};
const handleTestSmtp = async () => {
if (!testSmtpEmail.trim()) return;
setTestingSmtp(true);
setStatusMsg(null);
try {
await api.adminUpdateSettings(settings);
await api.adminTestSMTP(testSmtpEmail.trim());
setStatusMsg({
type: 'ok',
text: `Test-E-Mail erfolgreich an ${testSmtpEmail.trim()} gesendet!`,
});
} catch (err: any) {
setStatusMsg({
type: 'err',
text: `SMTP-Test fehlgeschlagen: ${err.message}`,
});
} finally {
setTestingSmtp(false);
}
};
return (
<div className="w-full space-y-4">
{/* Top Page Header */}
<div className="flex items-center justify-between border-b border-border pb-3">
<div className="flex items-center gap-3">
<button
onClick={onBack}
className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5"
title="Zurück zur Übersicht"
>
<ArrowLeft className="w-3.5 h-3.5" />
<span>Zurück</span>
</button>
<div>
<h1 className="text-base font-bold text-main tracking-tight flex items-center gap-2">
<Shield className="w-4 h-4 text-accent" />
<span>Server-Administration</span>
</h1>
<p className="text-xs text-muted">
Benutzerverwaltung, Einladungscodes, Isolationsmodus & Mail-Server
</p>
</div>
</div>
<span className="badge-accent">Server-Administrator</span>
</div>
{statusMsg && (
<div
className={`p-3 text-xs flex items-center justify-between gap-2 rounded-card ${
statusMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
}`}
>
<div className="flex items-center gap-2">
{statusMsg.type === 'ok' ? (
<Check className="w-4 h-4 text-accent flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
)}
<span>{statusMsg.text}</span>
</div>
<button
onClick={() => setStatusMsg(null)}
className="text-muted hover:text-main text-[11px]"
>
Schließen
</button>
</div>
)}
{/* Main 2-Column Layout */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start">
{/* Left Navigation Sidebar */}
<aside className="panel-surface divide-y divide-border overflow-hidden">
<button
onClick={() => setSection('users')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'users'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-canvas/50'
}`}
>
<Users className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Benutzer & Einladungen</p>
<p className="text-[10px] text-faint">Konten, Rollen & Invite-Codes</p>
</div>
</button>
{isServerAdmin && (
<>
<button
onClick={() => setSection('server')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'server'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-canvas/50'
}`}
>
<Server className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Server & Isolation</p>
<p className="text-[10px] text-faint">Sichtbarkeit & Registrierung</p>
</div>
</button>
<button
onClick={() => setSection('smtp')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'smtp'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-canvas/50'
}`}
>
<Mail className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">E-Mail / SMTP-Server</p>
<p className="text-[10px] text-faint">Mailversand für Alarme</p>
</div>
</button>
</>
)}
</aside>
{/* Right Content Area */}
<div className="md:col-span-3 panel-surface p-5 text-xs">
{/* ================================================================= */}
{/* SECTION 1: BENUTZER, ROLLEN & EINLADUNGEN */}
{/* ================================================================= */}
{section === 'users' && (
<div className="space-y-6">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">Benutzersteuerung, Rollen & Einladungen</h2>
<p className="text-muted mt-0.5">
Lege Benutzerkonten direkt an oder erzeuge Einladungscodes, mit denen sich eingeladene
Personen selbst registrieren können, auch wenn die öffentliche Registrierung gesperrt ist.
</p>
</div>
{/* Direct User Creation Form */}
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-main flex items-center gap-1.5">
<UserPlus className="w-3.5 h-3.5 text-accent" />
<span>Neues Benutzerkonto direkt anlegen</span>
</h3>
<form onSubmit={handleCreateUser} className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div className="min-w-0">
<label className="block text-muted mb-1">Anzeigename</label>
<input
type="text"
value={newUserName}
onChange={(e) => setNewUserName(e.target.value)}
placeholder="z.B. Anna Beispiel"
required
className="w-full min-w-0 h-8 px-2.5 input-field"
/>
</div>
<div className="min-w-0">
<label className="block text-muted mb-1">E-Mail-Adresse</label>
<input
type="email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="anna@beispiel.de"
required
className="w-full min-w-0 h-8 px-2.5 input-field"
/>
</div>
<div className="min-w-0">
<label className="block text-muted mb-1">Initial-Passwort</label>
<input
type="password"
value={newUserPassword}
onChange={(e) => setNewUserPassword(e.target.value)}
placeholder="Mind. 4 Zeichen"
minLength={4}
required
className="w-full min-w-0 h-8 px-2.5 input-field"
/>
</div>
<div className="min-w-0">
<label className="block text-muted mb-1">Benutzerrolle</label>
<select
value={newUserRole}
onChange={(e) => setNewUserRole(e.target.value as UserRole)}
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
>
<option value="member">Mitglied (Standard)</option>
<option value="admin">Server-Admin (Vollzugriff)</option>
</select>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
className="btn-primary h-8 px-4 text-xs font-semibold flex items-center gap-1.5"
>
<Plus className="w-3.5 h-3.5" />
<span>Benutzer jetzt anlegen</span>
</button>
</div>
</form>
</div>
{/* Invite Code Generator */}
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
<h3 className="font-bold text-main flex items-center gap-1.5">
<KeyRound className="w-3.5 h-3.5 text-accent" />
<span>Einladungscode erzeugen (gültig für 7 Tage)</span>
</h3>
<form onSubmit={handleCreateInvite} className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="min-w-0">
<label className="block text-muted mb-1">
Gebundene E-Mail (optional)
</label>
<input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Leer lassen für beliebige E-Mail"
className="w-full min-w-0 h-8 px-2.5 input-field"
/>
</div>
<div className="min-w-0">
<label className="block text-muted mb-1">Zugewiesene Server-Rolle</label>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as UserRole)}
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
>
<option value="member">Mitglied</option>
<option value="admin">Server-Admin</option>
</select>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
className="btn-action h-8 px-4 text-xs font-semibold flex items-center gap-1.5 border-accent text-main"
>
<KeyRound className="w-3.5 h-3.5 text-accent" />
<span>Einladungscode generieren</span>
</button>
</div>
</form>
{/* Active Invites List */}
{invites.length > 0 && (
<div className="pt-2 border-t border-border space-y-1.5">
<p className="text-[11px] font-semibold text-muted">
Aktive & bisherige Einladungscodes:
</p>
<div className="divide-y divide-border border border-border bg-panel rounded-card overflow-hidden">
{invites.map((inv) => (
<div
key={inv.id}
className="p-2 flex flex-wrap items-center justify-between gap-2"
>
<div className="flex items-center gap-2.5">
<code className="px-2 py-0.5 bg-canvas border border-border font-mono font-bold text-accent rounded-card">
{inv.code}
</code>
<span className="text-muted">
Rolle: <strong className="text-main">{inv.role}</strong>
{inv.email ? ` · Für: ${inv.email}` : ''}
</span>
{inv.usedBy ? (
<span className="badge-neutral text-[10px]">
Eingelöst
</span>
) : (
<span className="badge-ok text-[10px]">
Offen
</span>
)}
</div>
<div className="flex items-center gap-1.5">
{!inv.usedBy && (
<button
onClick={() => handleCopyInviteCode(inv)}
className="btn-action h-7 px-2 text-[11px] flex items-center gap-1"
>
<Copy className="w-3 h-3" />
<span>{copiedInviteId === inv.id ? 'Kopiert!' : 'Code kopieren'}</span>
</button>
)}
<button
onClick={() => handleDeleteInvite(inv.id)}
className="btn-icon btn-icon-danger"
title="Einladung löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* Users List */}
<div className="space-y-2">
<h3 className="font-bold text-main">
Registrierte Benutzer ({users.length})
</h3>
<div className="border border-border divide-y divide-border bg-canvas rounded-card overflow-hidden">
{users.map((u) => (
<div key={u.id} className="p-3 space-y-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div className="flex items-center gap-2.5">
<UserAvatar user={u} size="sm" className="border border-border/80" />
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-main">{u.displayName}</span>
{!u.isActive && (
<span className="badge-err text-[10px]">
Gesperrt
</span>
)}
</div>
<p className="text-muted text-[11px]">{u.email}</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
{/* Role selector */}
<select
value={u.role}
disabled={!isServerAdmin && u.role === 'admin'}
onChange={(e) => handleChangeUserRole(u, e.target.value as UserRole)}
className="h-7 px-2 input-field text-[11px] bg-panel"
title="Benutzerrolle ändern"
>
<option value="member">Mitglied</option>
<option value="admin">Server-Admin</option>
</select>
{isServerAdmin && (
<>
<button
onClick={() =>
setResetPwdUserId(resetPwdUserId === u.id ? null : u.id)
}
className="btn-action h-7 px-2 text-[11px]"
>
Passwort
</button>
{u.id !== currentUser.id && (
<>
<button
onClick={() => handleToggleUserActive(u)}
className="btn-action h-7 px-2 text-[11px]"
>
{u.isActive ? 'Sperren' : 'Aktivieren'}
</button>
<button
onClick={() => handleDeleteUser(u)}
className="btn-icon btn-icon-danger"
title="Benutzer löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</>
)}
</>
)}
</div>
</div>
{resetPwdUserId === u.id && (
<div className="flex items-center gap-2 pt-2 border-t border-border">
<input
type="password"
value={resetPwdVal}
onChange={(e) => setResetPwdVal(e.target.value)}
placeholder="Neues Passwort eingeben…"
className="flex-1 h-7 px-2 input-field"
/>
<button
onClick={() => handleConfirmResetPassword(u.id)}
className="btn-primary h-7 px-3 text-[11px]"
>
Speichern
</button>
<button
onClick={() => setResetPwdUserId(null)}
className="btn-action h-7 px-2 text-[11px]"
>
Abbrechen
</button>
</div>
)}
</div>
))}
</div>
</div>
</div>
)}
{/* ================================================================= */}
{/* 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 für alle Nutzer, ob sie komplett isoliert
arbeiten, sich nur innerhalb ihrer Teams 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>
</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">
<button
type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'strict_private' })}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'strict_private'
? '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">1. Strikt Privat (Keiner sieht andere)</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">
Jeder Benutzer arbeitet komplett für sich allein. Keine geteilten Spaces und keine
Benutzersuche möglich.
</p>
</button>
<button
type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'teams_only' })}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'teams_only'
? '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">2. Teams isoliert (Empfohlen)</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">
Benutzer sehen ausschließlich Personen, mit denen sie mindestens ein gemeinsames Team
teilen.
</p>
</button>
<button
type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'open' })}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'open'
? '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">3. Offen (Alle sehen alle)</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">
Alle registrierten Benutzer dieser Instanz können sich gegenseitig sehen und Spaces
miteinander teilen.
</p>
</button>
</div>
</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>
<button
type="button"
role="switch"
aria-checked={settings.allowRegistration}
onClick={() =>
handleSaveSystemSettings({ allowRegistration: !settings.allowRegistration })
}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-pill border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
settings.allowRegistration ? 'bg-accent' : 'bg-panel-elevated border border-border'
}`}
title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'}
>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-5 w-5 transform rounded-pill bg-white shadow-md ring-0 transition duration-200 ease-in-out ${
settings.allowRegistration ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
</div>
</div>
</div>
)}
{/* ================================================================= */}
{/* 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>
</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="number"
value={settings.smtpPort || 587}
onChange={(e) =>
setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) }))
}
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="flex items-center pt-5">
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={settings.smtpTls ?? true}
onChange={(e) =>
setSettings((prev) => ({ ...prev, smtpTls: e.target.checked }))
}
className="w-4 h-4 checkbox-square cursor-pointer"
/>
<span className="text-main font-medium">
STARTTLS / TLS-Verschlüsselung verwenden (`SMTP_TLS=true`)
</span>
</label>
</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"
/>
<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>
</div>
</div>
);
};