added backup and email client
This commit is contained in:
@@ -7,7 +7,7 @@ import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import Modal from '../../components/ui/Modal';
|
||||
import Badge from '../../components/ui/Badge';
|
||||
import { Plus, Edit, Trash2, ArrowLeft, GripVertical, Zap, Flame, Wifi, Cable, Smartphone, Tv, Car, FileText } from 'lucide-react';
|
||||
import { Plus, Edit, Trash2, ArrowLeft, GripVertical, Zap, Flame, Wifi, Cable, Smartphone, Tv, Car, FileText, Network } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { ContractCategory } from '../../types';
|
||||
|
||||
@@ -17,6 +17,7 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
Flame: <Flame className="w-5 h-5" />,
|
||||
Wifi: <Wifi className="w-5 h-5" />,
|
||||
Cable: <Cable className="w-5 h-5" />,
|
||||
Network: <Network className="w-5 h-5" />,
|
||||
Smartphone: <Smartphone className="w-5 h-5" />,
|
||||
Tv: <Tv className="w-5 h-5" />,
|
||||
Car: <Car className="w-5 h-5" />,
|
||||
@@ -27,7 +28,8 @@ const availableIcons = [
|
||||
{ value: 'Zap', label: 'Blitz (Strom)' },
|
||||
{ value: 'Flame', label: 'Flamme (Gas)' },
|
||||
{ value: 'Wifi', label: 'WLAN (DSL)' },
|
||||
{ value: 'Cable', label: 'Kabel (Glasfaser)' },
|
||||
{ value: 'Cable', label: 'Kabel' },
|
||||
{ value: 'Network', label: 'Netzwerk (Glasfaser)' },
|
||||
{ value: 'Smartphone', label: 'Smartphone (Mobilfunk)' },
|
||||
{ value: 'Tv', label: 'TV' },
|
||||
{ value: 'Car', label: 'Auto (KFZ)' },
|
||||
@@ -88,7 +90,7 @@ export default function ContractCategoryList() {
|
||||
</Button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold flex-1">Vertragstypen</h1>
|
||||
{hasPermission('platforms:create') && (
|
||||
{hasPermission('developer:access') && (
|
||||
<Button onClick={() => setShowModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Neuer Vertragstyp
|
||||
@@ -142,12 +144,12 @@ export default function ContractCategoryList() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 ml-4">
|
||||
{hasPermission('platforms:update') && (
|
||||
{hasPermission('developer:access') && (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(category)} title="Bearbeiten">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('platforms:delete') && (
|
||||
{hasPermission('developer:access') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Database, Download, Upload, Trash2, RefreshCw, HardDrive, Clock, FileText, FolderOpen, Archive, AlertTriangle, RotateCcw } from 'lucide-react';
|
||||
import { backupApi, BackupInfo } from '../../services/api';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import Button from '../../components/ui/Button';
|
||||
|
||||
export default function DatabaseBackup() {
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||
const [showFactoryResetConfirm, setShowFactoryResetConfirm] = useState(false);
|
||||
const [factoryResetConfirmText, setFactoryResetConfirmText] = useState('');
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { logout } = useAuth();
|
||||
|
||||
// Backups laden
|
||||
const { data: backupsData, isLoading } = useQuery({
|
||||
queryKey: ['backups'],
|
||||
queryFn: () => backupApi.list(),
|
||||
});
|
||||
|
||||
const backups = backupsData?.data || [];
|
||||
|
||||
// Backup erstellen
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => backupApi.create(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Backup wiederherstellen
|
||||
const restoreMutation = useMutation({
|
||||
mutationFn: (name: string) => backupApi.restore(name),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
setShowRestoreConfirm(null);
|
||||
},
|
||||
});
|
||||
|
||||
// Backup löschen
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (name: string) => backupApi.delete(name),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
setShowDeleteConfirm(null);
|
||||
},
|
||||
});
|
||||
|
||||
// Backup hochladen
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: (file: File) => backupApi.upload(file),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['backups'] });
|
||||
setUploadError(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setUploadError(error.message || 'Upload fehlgeschlagen');
|
||||
},
|
||||
});
|
||||
|
||||
// Werkseinstellungen
|
||||
const factoryResetMutation = useMutation({
|
||||
mutationFn: () => backupApi.factoryReset(),
|
||||
onSuccess: () => {
|
||||
setShowFactoryResetConfirm(false);
|
||||
setFactoryResetConfirmText('');
|
||||
// Nach Factory Reset ausloggen
|
||||
logout();
|
||||
},
|
||||
});
|
||||
|
||||
// Datei-Upload Handler
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
setUploadError('Nur ZIP-Dateien sind erlaubt');
|
||||
return;
|
||||
}
|
||||
setUploadError(null);
|
||||
uploadMutation.mutate(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Download mit Auth-Token
|
||||
const handleDownload = async (name: string) => {
|
||||
const token = localStorage.getItem('token');
|
||||
const url = backupApi.getDownloadUrl(name);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Download fehlgeschlagen');
|
||||
}
|
||||
|
||||
// Blob erstellen und herunterladen
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = `opencrm-backup-${name}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
} catch (error: any) {
|
||||
console.error('Download error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Formatierung
|
||||
const formatDate = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<Database className="w-5 h-5" />
|
||||
Datenbank & Zurücksetzen
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Backups erstellen, wiederherstellen oder auf Werkseinstellungen zurücksetzen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Upload Button */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
accept=".zip"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadMutation.isPending}
|
||||
>
|
||||
{uploadMutation.isPending ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Hochladen...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Backup hochladen
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{/* Create Button */}
|
||||
<Button
|
||||
onClick={() => createMutation.mutate()}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Wird erstellt...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Neues Backup
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload Error */}
|
||||
{uploadError && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
|
||||
{uploadError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info-Box */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium text-blue-800 mb-2">Hinweise zur Datensicherung</h4>
|
||||
<ul className="text-sm text-blue-700 space-y-1 list-disc list-inside">
|
||||
<li>Backups enthalten alle Datenbankdaten und hochgeladene Dokumente</li>
|
||||
<li>Erstellen Sie vor Datenbankmigrationen immer ein Backup</li>
|
||||
<li>Backups können als ZIP heruntergeladen und auf einem anderen System wiederhergestellt werden</li>
|
||||
<li>Bei der Wiederherstellung werden bestehende Daten mit dem Backup-Stand überschrieben</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Backup-Liste */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<h3 className="text-sm font-medium text-gray-700">Verfügbare Backups</h3>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : backups.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
||||
<HardDrive className="w-12 h-12 mb-2 opacity-30" />
|
||||
<p>Keine Backups vorhanden</p>
|
||||
<p className="text-sm mt-1">Erstellen Sie Ihr erstes Backup</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{backups.map((backup: BackupInfo) => (
|
||||
<div key={backup.name} className="p-4 hover:bg-gray-50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="font-mono text-sm bg-gray-100 px-2 py-1 rounded">
|
||||
{backup.name}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{formatDate(backup.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
{backup.totalRecords.toLocaleString('de-DE')} Datensätze
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<HardDrive className="w-4 h-4" />
|
||||
{formatSize(backup.sizeBytes)}
|
||||
</span>
|
||||
{backup.hasUploads && (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
Dokumente ({formatSize(backup.uploadSizeBytes)})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Tabellen-Details (kollabiert) */}
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs text-gray-500 cursor-pointer hover:text-gray-700">
|
||||
Tabellen anzeigen ({backup.tables.filter(t => t.count > 0).length} mit Daten)
|
||||
</summary>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{backup.tables
|
||||
.filter(t => t.count > 0)
|
||||
.map(t => (
|
||||
<span
|
||||
key={t.table}
|
||||
className="text-xs bg-gray-100 px-2 py-0.5 rounded"
|
||||
>
|
||||
{t.table}: {t.count}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* Aktionen */}
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleDownload(backup.name)}
|
||||
title="Als ZIP herunterladen"
|
||||
>
|
||||
<Archive className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowRestoreConfirm(backup.name)}
|
||||
disabled={restoreMutation.isPending}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-1" />
|
||||
Wiederherstellen
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteConfirm(backup.name)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Wiederherstellungs-Bestätigung */}
|
||||
{showRestoreConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Backup wiederherstellen?
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Möchten Sie das Backup <strong>{showRestoreConfirm}</strong> wirklich wiederherstellen?
|
||||
</p>
|
||||
<p className="text-amber-600 text-sm mb-4 bg-amber-50 p-3 rounded-lg">
|
||||
<strong>Achtung:</strong> Bestehende Daten und Dokumente werden mit dem Backup-Stand überschrieben.
|
||||
Dies kann nicht rückgängig gemacht werden.
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowRestoreConfirm(null)}
|
||||
disabled={restoreMutation.isPending}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => restoreMutation.mutate(showRestoreConfirm)}
|
||||
disabled={restoreMutation.isPending}
|
||||
>
|
||||
{restoreMutation.isPending ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Wird wiederhergestellt...
|
||||
</>
|
||||
) : (
|
||||
'Ja, wiederherstellen'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lösch-Bestätigung */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Backup löschen?
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Möchten Sie das Backup <strong>{showDeleteConfirm}</strong> wirklich löschen?
|
||||
Dies kann nicht rückgängig gemacht werden.
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowDeleteConfirm(null)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(showDeleteConfirm)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? 'Wird gelöscht...' : 'Ja, löschen'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Werkseinstellungen */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mt-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 bg-red-100 rounded-lg">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">
|
||||
Werkseinstellungen
|
||||
</h3>
|
||||
<p className="text-sm text-red-700 mb-4">
|
||||
Setzt das System auf den Ausgangszustand zurück. <strong>Alle Daten werden unwiderruflich gelöscht</strong> -
|
||||
Kunden, Verträge, Benutzer, Dokumente und Einstellungen. Nur die hier gespeicherten Backups bleiben erhalten.
|
||||
</p>
|
||||
<ul className="text-sm text-red-700 mb-4 list-disc list-inside space-y-1">
|
||||
<li>Alle Kunden und Verträge werden gelöscht</li>
|
||||
<li>Alle Benutzer werden gelöscht</li>
|
||||
<li>Alle hochgeladenen Dokumente werden gelöscht</li>
|
||||
<li>Ein neuer Admin-Benutzer wird erstellt (admin@admin.com / admin)</li>
|
||||
<li><strong>Backups bleiben erhalten</strong> und können danach wiederhergestellt werden</li>
|
||||
</ul>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => setShowFactoryResetConfirm(true)}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Werkseinstellungen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Werkseinstellungen-Bestätigung */}
|
||||
{showFactoryResetConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl p-6 max-w-lg mx-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-red-100 rounded-lg">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Wirklich auf Werkseinstellungen zurücksetzen?
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Diese Aktion löscht <strong>alle Daten unwiderruflich</strong>. Es gibt kein Zurück!
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Geben Sie zur Bestätigung <strong className="font-mono bg-gray-100 px-1">LÖSCHEN</strong> ein:
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={factoryResetConfirmText}
|
||||
onChange={(e) => setFactoryResetConfirmText(e.target.value)}
|
||||
placeholder="LÖSCHEN"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg mb-4 focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowFactoryResetConfirm(false);
|
||||
setFactoryResetConfirmText('');
|
||||
}}
|
||||
disabled={factoryResetMutation.isPending}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => factoryResetMutation.mutate()}
|
||||
disabled={factoryResetConfirmText !== 'LÖSCHEN' || factoryResetMutation.isPending}
|
||||
>
|
||||
{factoryResetMutation.isPending ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Wird zurückgesetzt...
|
||||
</>
|
||||
) : (
|
||||
'Ja, alles löschen'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,15 @@ const PROVIDER_TYPES = [
|
||||
{ value: 'DIRECTADMIN', label: 'DirectAdmin' },
|
||||
];
|
||||
|
||||
// Verschlüsselungstyp
|
||||
type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||
|
||||
const ENCRYPTION_OPTIONS = [
|
||||
{ value: 'SSL', label: 'SSL/TLS', description: 'Verschlüsselung von Anfang an' },
|
||||
{ value: 'STARTTLS', label: 'STARTTLS', description: 'Startet unverschlüsselt, dann Upgrade' },
|
||||
{ value: 'NONE', label: 'Keine', description: 'Keine Verschlüsselung' },
|
||||
];
|
||||
|
||||
interface ProviderFormData {
|
||||
name: string;
|
||||
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
|
||||
@@ -23,6 +32,10 @@ interface ProviderFormData {
|
||||
password: string;
|
||||
domain: string;
|
||||
defaultForwardEmail: string;
|
||||
// Verschlüsselungs-Einstellungen
|
||||
imapEncryption: MailEncryption;
|
||||
smtpEncryption: MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
@@ -36,6 +49,9 @@ const emptyForm: ProviderFormData = {
|
||||
password: '',
|
||||
domain: 'stressfrei-wechseln.de',
|
||||
defaultForwardEmail: '',
|
||||
imapEncryption: 'SSL',
|
||||
smtpEncryption: 'SSL',
|
||||
allowSelfSignedCerts: false,
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
};
|
||||
@@ -109,6 +125,9 @@ export default function EmailProviders() {
|
||||
password: '', // Passwort wird nicht geladen
|
||||
domain: config.domain,
|
||||
defaultForwardEmail: config.defaultForwardEmail || '',
|
||||
imapEncryption: config.imapEncryption ?? 'SSL',
|
||||
smtpEncryption: config.smtpEncryption ?? 'SSL',
|
||||
allowSelfSignedCerts: config.allowSelfSignedCerts ?? false,
|
||||
isActive: config.isActive,
|
||||
isDefault: config.isDefault,
|
||||
});
|
||||
@@ -200,6 +219,9 @@ export default function EmailProviders() {
|
||||
username: formData.username,
|
||||
domain: formData.domain,
|
||||
defaultForwardEmail: formData.defaultForwardEmail,
|
||||
imapEncryption: formData.imapEncryption,
|
||||
smtpEncryption: formData.smtpEncryption,
|
||||
allowSelfSignedCerts: formData.allowSelfSignedCerts,
|
||||
isActive: formData.isActive,
|
||||
isDefault: formData.isDefault,
|
||||
};
|
||||
@@ -462,6 +484,69 @@ export default function EmailProviders() {
|
||||
Diese E-Mail-Adresse wird zusätzlich zur Kunden-E-Mail als Weiterleitungsziel hinzugefügt.
|
||||
</p>
|
||||
|
||||
{/* Verschlüsselungs-Einstellungen */}
|
||||
<div className="pt-4 border-t">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">E-Mail-Verbindungseinstellungen</h4>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* IMAP Verschlüsselung */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
IMAP Verschlüsselung
|
||||
<span className="text-gray-400 font-normal ml-1">
|
||||
(Port {formData.imapEncryption === 'SSL' ? '993' : '143'})
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.imapEncryption}
|
||||
onChange={(e) => setFormData({ ...formData, imapEncryption: e.target.value as MailEncryption })}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
>
|
||||
{ENCRYPTION_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label} - {opt.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* SMTP Verschlüsselung */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
SMTP Verschlüsselung
|
||||
<span className="text-gray-400 font-normal ml-1">
|
||||
(Port {formData.smtpEncryption === 'SSL' ? '465' : formData.smtpEncryption === 'STARTTLS' ? '587' : '25'})
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.smtpEncryption}
|
||||
onChange={(e) => setFormData({ ...formData, smtpEncryption: e.target.value as MailEncryption })}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
>
|
||||
{ENCRYPTION_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label} - {opt.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.allowSelfSignedCerts}
|
||||
onChange={(e) => setFormData({ ...formData, allowSelfSignedCerts: e.target.checked })}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm">Selbstsignierte Zertifikate erlauben</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500">
|
||||
Aktivieren Sie diese Option für Testumgebungen mit selbstsignierten SSL-Zertifikaten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user