diff --git a/backend/src/controllers/gdpr.controller.ts b/backend/src/controllers/gdpr.controller.ts
index 4680a508..8db90cd7 100644
--- a/backend/src/controllers/gdpr.controller.ts
+++ b/backend/src/controllers/gdpr.controller.ts
@@ -421,6 +421,50 @@ export async function updateAuthorizationTemplate(req: AuthRequest, res: Respons
}
}
+// ==================== IMPRESSUM & WEBSITE-DATENSCHUTZ ====================
+
+export async function getImprint(req: AuthRequest, res: Response) {
+ try {
+ const html = await appSettingService.getSetting('imprintHtml');
+ res.json({ success: true, data: { html: html || '' } });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
+ }
+}
+
+export async function updateImprint(req: AuthRequest, res: Response) {
+ try {
+ const { html } = req.body;
+ if (typeof html !== 'string') return res.status(400).json({ success: false, error: 'HTML-Inhalt erforderlich' });
+ await appSettingService.setSetting('imprintHtml', html);
+ await logChange({ req, action: 'UPDATE', resourceType: 'AppSetting', label: 'Impressum aktualisiert' });
+ res.json({ success: true, message: 'Impressum gespeichert' });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
+ }
+}
+
+export async function getWebsitePrivacyPolicy(req: AuthRequest, res: Response) {
+ try {
+ const html = await appSettingService.getSetting('websitePrivacyPolicyHtml');
+ res.json({ success: true, data: { html: html || '' } });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Fehler beim Abrufen' });
+ }
+}
+
+export async function updateWebsitePrivacyPolicy(req: AuthRequest, res: Response) {
+ try {
+ const { html } = req.body;
+ if (typeof html !== 'string') return res.status(400).json({ success: false, error: 'HTML-Inhalt erforderlich' });
+ await appSettingService.setSetting('websitePrivacyPolicyHtml', html);
+ await logChange({ req, action: 'UPDATE', resourceType: 'AppSetting', label: 'Website-Datenschutzerklärung aktualisiert' });
+ res.json({ success: true, message: 'Website-Datenschutzerklärung gespeichert' });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
+ }
+}
+
// ==================== SEND CONSENT LINK ====================
/**
diff --git a/backend/src/routes/gdpr.routes.ts b/backend/src/routes/gdpr.routes.ts
index 9dc1b5e4..6129b94a 100644
--- a/backend/src/routes/gdpr.routes.ts
+++ b/backend/src/routes/gdpr.routes.ts
@@ -60,6 +60,14 @@ router.put('/privacy-policy', requirePermission('gdpr:admin'), gdprController.up
router.get('/authorization-template', requirePermission('gdpr:admin'), gdprController.getAuthorizationTemplate);
router.put('/authorization-template', requirePermission('gdpr:admin'), gdprController.updateAuthorizationTemplate);
+// Impressum (Editor + Portal-Anzeige)
+router.get('/imprint', gdprController.getImprint);
+router.put('/imprint', requirePermission('gdpr:admin'), gdprController.updateImprint);
+
+// Website-Datenschutzerklärung (Editor + Portal-Anzeige)
+router.get('/website-privacy-policy', gdprController.getWebsitePrivacyPolicy);
+router.put('/website-privacy-policy', requirePermission('gdpr:admin'), gdprController.updateWebsitePrivacyPolicy);
+
// Consent-Link senden
router.post('/customer/:customerId/send-consent-link', requirePermission('customers:update'), gdprController.sendConsentLink);
diff --git a/backend/todo.md b/backend/todo.md
index a3435f5c..4f97b9c7 100644
--- a/backend/todo.md
+++ b/backend/todo.md
@@ -22,7 +22,7 @@ Alle Datumsfelder mit 0 davor wenn es ne einstellige Zahl ist
Jetzt : 1.1.2026
Und gewollt 01.01.2026
-
+#erledigt
Die Auditmeldungen aussagekräftig
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index dae96e9e..c980bda7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -35,6 +35,10 @@ import ConsentPage from './pages/public/ConsentPage';
import PrivacyPolicyEditor from './pages/settings/PrivacyPolicyEditor';
import PortalPrivacy from './pages/portal/PortalPrivacy';
import AuthorizationTemplateEditor from './pages/settings/AuthorizationTemplateEditor';
+import ImprintEditor from './pages/settings/ImprintEditor';
+import WebsitePrivacyPolicyEditor from './pages/settings/WebsitePrivacyPolicyEditor';
+import PortalImprint from './pages/portal/PortalImprint';
+import PortalWebsitePrivacy from './pages/portal/PortalWebsitePrivacy';
import PortalAuthorizations from './pages/portal/PortalAuthorizations';
import PortalProfile from './pages/portal/PortalProfile';
import PortalMeters from './pages/portal/PortalMeters';
@@ -170,6 +174,8 @@ function App() {
} />
} />
} />
+ } />
+ } />
} />
{/* Settings */}
@@ -190,6 +196,8 @@ function App() {
} />
} />
} />
+ } />
+ } />
{/* Redirect old users route */}
} />
diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx
index 33322b69..0d33087e 100644
--- a/frontend/src/components/layout/Layout.tsx
+++ b/frontend/src/components/layout/Layout.tsx
@@ -4,7 +4,7 @@ import { useAuth } from '../../context/AuthContext';
import { gdprApi } from '../../services/api';
import Sidebar from './Sidebar';
import ScrollToTopButton from '../ScrollToTopButton';
-import { AlertTriangle, ArrowRight } from 'lucide-react';
+import { AlertTriangle, ArrowRight, Building, Shield } from 'lucide-react';
function ConsentBanner() {
const { user, isCustomerPortal } = useAuth();
@@ -41,6 +41,27 @@ function ConsentBanner() {
);
}
+function PortalFooter() {
+ const { isCustomerPortal } = useAuth();
+ if (!isCustomerPortal) return null;
+
+ return (
+
+ );
+}
+
export default function Layout() {
return (
@@ -50,6 +71,7 @@ export default function Layout() {
+
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index 574a8c0d..3abb6320 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -273,6 +273,44 @@ export default function Settings() {
)}
+ {hasPermission('gdpr:admin') && (
+
+
+
+
+
+
+
+ Impressum
+
+
+
Impressum für das Kundenportal bearbeiten.
+
+
+
+ )}
+ {hasPermission('gdpr:admin') && (
+
+
+
+
+
+
+
+ Website-Datenschutzerklärung
+
+
+
Datenschutzerklärung für das Kundenportal bearbeiten.
+
+
+
+ )}
)}
diff --git a/frontend/src/pages/portal/PortalImprint.tsx b/frontend/src/pages/portal/PortalImprint.tsx
new file mode 100644
index 00000000..0e659c3f
--- /dev/null
+++ b/frontend/src/pages/portal/PortalImprint.tsx
@@ -0,0 +1,29 @@
+import { useQuery } from '@tanstack/react-query';
+import { gdprApi } from '../../services/api';
+import Card from '../../components/ui/Card';
+import { Building } from 'lucide-react';
+
+export default function PortalImprint() {
+ const { data, isLoading, isError } = useQuery({
+ queryKey: ['imprint'],
+ queryFn: () => gdprApi.getImprint(),
+ retry: 1,
+ });
+
+ if (isLoading) return Laden...
;
+ if (isError) return Impressum konnte nicht geladen werden. Bitte Server neu starten.
;
+
+ const html = data?.data?.html || 'Noch kein Impressum hinterlegt.
';
+
+ return (
+
+
+
+
Impressum
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/portal/PortalWebsitePrivacy.tsx b/frontend/src/pages/portal/PortalWebsitePrivacy.tsx
new file mode 100644
index 00000000..378e3213
--- /dev/null
+++ b/frontend/src/pages/portal/PortalWebsitePrivacy.tsx
@@ -0,0 +1,29 @@
+import { useQuery } from '@tanstack/react-query';
+import { gdprApi } from '../../services/api';
+import Card from '../../components/ui/Card';
+import { Shield } from 'lucide-react';
+
+export default function PortalWebsitePrivacy() {
+ const { data, isLoading, isError } = useQuery({
+ queryKey: ['website-privacy-policy'],
+ queryFn: () => gdprApi.getWebsitePrivacyPolicy(),
+ retry: 1,
+ });
+
+ if (isLoading) return Laden...
;
+ if (isError) return Datenschutzerklärung konnte nicht geladen werden. Bitte Server neu starten.
;
+
+ const html = data?.data?.html || 'Noch keine Datenschutzerklärung hinterlegt.
';
+
+ return (
+
+
+
+
Datenschutzerklärung
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/settings/ImprintEditor.tsx b/frontend/src/pages/settings/ImprintEditor.tsx
new file mode 100644
index 00000000..fdc8f57e
--- /dev/null
+++ b/frontend/src/pages/settings/ImprintEditor.tsx
@@ -0,0 +1,131 @@
+import { useState, useCallback } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useEditor, EditorContent } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import TiptapLink from '@tiptap/extension-link';
+import { gdprApi } from '../../services/api';
+import Button from '../../components/ui/Button';
+import Card from '../../components/ui/Card';
+import { ArrowLeft, Save, Eye, Bold, Italic, List, ListOrdered, Heading1, Heading2, Heading3, Link as LinkIcon, Undo, Redo, Type } from 'lucide-react';
+import { Link as RouterLink } from 'react-router-dom';
+
+const DEFAULT_IMPRINT = `Impressum
+
+Angaben gemäß § 5 TMG
+
+Hacker-Net Telekommunikation
+Stefan Hacker
+Am Wunderburgpark 5b
+26135 Oldenburg
+
+Kontakt
+
+Telefon: [Ihre Telefonnummer]
+E-Mail: info@hacker-net.de
+
+Umsatzsteuer-ID
+
+Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:
+[Ihre USt-IdNr.]
+
+Berufsbezeichnung und berufsrechtliche Regelungen
+
+Berufsbezeichnung: Telekommunikationsdienstleister
+Zuständige Kammer: [IHK Oldenburg]
+Verliehen in: Deutschland
+
+Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
+
+Stefan Hacker
+Am Wunderburgpark 5b
+26135 Oldenburg
+
+EU-Streitschlichtung
+
+Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
+https://ec.europa.eu/consumers/odr/ .
+Unsere E-Mail-Adresse finden Sie oben im Impressum.
+
+Verbraucherstreitbeilegung / Universalschlichtungsstelle
+
+Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
`;
+
+function MenuBar({ editor }: { editor: ReturnType }) {
+ if (!editor) return null;
+ const setLink = useCallback(() => {
+ const previousUrl = editor.getAttributes('link').href;
+ const url = window.prompt('URL eingeben:', previousUrl);
+ if (url === null) return;
+ if (url === '') { editor.chain().focus().extendMarkRange('link').unsetLink().run(); return; }
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
+ }, [editor]);
+ const btnClass = (active: boolean) => `p-1.5 rounded hover:bg-gray-200 transition-colors ${active ? 'bg-gray-200 text-blue-600' : 'text-gray-600'}`;
+ return (
+
+
editor.chain().focus().toggleBold().run()} className={btnClass(editor.isActive('bold'))} title="Fett">
+
editor.chain().focus().toggleItalic().run()} className={btnClass(editor.isActive('italic'))} title="Kursiv">
+
+
editor.chain().focus().toggleHeading({ level: 1 }).run()} className={btnClass(editor.isActive('heading', { level: 1 }))} title="Überschrift 1">
+
editor.chain().focus().toggleHeading({ level: 2 }).run()} className={btnClass(editor.isActive('heading', { level: 2 }))} title="Überschrift 2">
+
editor.chain().focus().toggleHeading({ level: 3 }).run()} className={btnClass(editor.isActive('heading', { level: 3 }))} title="Überschrift 3">
+
editor.chain().focus().setParagraph().run()} className={btnClass(editor.isActive('paragraph'))} title="Absatz">
+
+
editor.chain().focus().toggleBulletList().run()} className={btnClass(editor.isActive('bulletList'))} title="Aufzählung">
+
editor.chain().focus().toggleOrderedList().run()} className={btnClass(editor.isActive('orderedList'))} title="Nummerierung">
+
+
+
+
editor.chain().focus().undo().run()} disabled={!editor.can().undo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Rückgängig">
+
editor.chain().focus().redo().run()} disabled={!editor.can().redo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Wiederherstellen">
+
+ );
+}
+
+export default function ImprintEditor() {
+ const queryClient = useQueryClient();
+ const [showPreview, setShowPreview] = useState(false);
+ const [saved, setSaved] = useState(false);
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['imprint'],
+ queryFn: () => gdprApi.getImprint(),
+ });
+
+ const saveMutation = useMutation({
+ mutationFn: (html: string) => gdprApi.updateImprint(html),
+ onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['imprint'] }); setSaved(true); setTimeout(() => setSaved(false), 2000); },
+ });
+
+ const initialContent = data?.data?.html || DEFAULT_IMPRINT;
+ const editor = useEditor({
+ extensions: [StarterKit, TiptapLink.configure({ openOnClick: false, HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer' } })],
+ content: initialContent,
+ editorProps: { attributes: { class: 'prose prose-sm max-w-none p-4 min-h-[400px] focus:outline-none' } },
+ }, [initialContent]);
+
+ const handleSave = () => { if (editor) saveMutation.mutate(editor.getHTML()); };
+ const handleReset = () => { if (confirm('Auf Standardtext zurücksetzen?')) editor?.commands.setContent(DEFAULT_IMPRINT); };
+
+ if (isLoading) return Laden...
;
+
+ return (
+
+
+
+
Impressum bearbeiten
+
Standardtext laden
+
setShowPreview(!showPreview)}> {showPreview ? 'Editor' : 'Vorschau'}
+
{saved ? 'Gespeichert!' : saveMutation.isPending ? 'Speichern...' : 'Speichern'}
+
+ {showPreview ? (
+
+ ) : (
+
+
+
+
+ )}
+ {saveMutation.isError &&
Fehler beim Speichern.
}
+
+ );
+}
diff --git a/frontend/src/pages/settings/WebsitePrivacyPolicyEditor.tsx b/frontend/src/pages/settings/WebsitePrivacyPolicyEditor.tsx
new file mode 100644
index 00000000..27418364
--- /dev/null
+++ b/frontend/src/pages/settings/WebsitePrivacyPolicyEditor.tsx
@@ -0,0 +1,163 @@
+import { useState, useCallback } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useEditor, EditorContent } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+import TiptapLink from '@tiptap/extension-link';
+import { gdprApi } from '../../services/api';
+import Button from '../../components/ui/Button';
+import Card from '../../components/ui/Card';
+import { ArrowLeft, Save, Eye, Bold, Italic, List, ListOrdered, Heading1, Heading2, Heading3, Link as LinkIcon, Undo, Redo, Type } from 'lucide-react';
+import { Link as RouterLink } from 'react-router-dom';
+
+const DEFAULT_POLICY = `Datenschutzerklärung
+
+1. Datenschutz auf einen Blick
+
+Allgemeine Hinweise
+Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie dieses Kundenportal nutzen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.
+
+Datenerfassung in diesem Portal
+Wer ist verantwortlich für die Datenerfassung?
+Die Datenverarbeitung erfolgt durch den Betreiber des Portals. Dessen Kontaktdaten können Sie dem Impressum entnehmen.
+
+Wie erfassen wir Ihre Daten?
+Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z.B. um Daten handeln, die Sie in ein Kontaktformular eingeben. Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch des Portals durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z.B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs).
+
+Wofür nutzen wir Ihre Daten?
+Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung des Portals zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden. Hauptsächlich dienen Ihre Daten der Vertragsverwaltung und Kundenbetreuung.
+
+2. Verantwortliche Stelle
+
+Die verantwortliche Stelle für die Datenverarbeitung ist:
+
+Hacker-Net Telekommunikation
+Stefan Hacker
+Am Wunderburgpark 5b
+26135 Oldenburg
+E-Mail: info@hacker-net.de
+
+Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten entscheidet.
+
+3. Datenerfassung im Portal
+
+Cookies
+Dieses Portal verwendet Cookies ausschließlich für die Authentifizierung (Login) und Sitzungsverwaltung. Es werden keine Tracking- oder Marketing-Cookies eingesetzt.
+
+Server-Log-Dateien
+Der Provider des Portals erhebt und speichert automatisch Informationen in sogenannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
+
+ Browsertyp und Browserversion
+ Verwendetes Betriebssystem
+ Referrer URL
+ Hostname des zugreifenden Rechners
+ Uhrzeit der Serveranfrage
+ IP-Adresse
+
+Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen. Die Erfassung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO.
+
+Registrierung und Anmeldung
+Sie können sich in diesem Portal mit den Ihnen mitgeteilten Zugangsdaten anmelden. Die bei der Anmeldung abgefragten Daten werden für die Bereitstellung des Portals und die Vertragsverwaltung verwendet. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung).
+
+4. Ihre Rechte
+
+Sie haben jederzeit das Recht:
+
+ Auskunft über Ihre bei uns gespeicherten personenbezogenen Daten zu erhalten (Art. 15 DSGVO)
+ Berichtigung unrichtiger Daten zu verlangen (Art. 16 DSGVO)
+ Löschung Ihrer Daten zu verlangen (Art. 17 DSGVO)
+ Einschränkung der Verarbeitung zu verlangen (Art. 18 DSGVO)
+ Datenübertragbarkeit zu verlangen (Art. 20 DSGVO)
+ Eine erteilte Einwilligung zu widerrufen (Art. 7 Abs. 3 DSGVO)
+ Sich bei einer Aufsichtsbehörde zu beschweren (Art. 77 DSGVO)
+
+
+Zuständige Aufsichtsbehörde:
+Die Landesbeauftragte für den Datenschutz Niedersachsen
+Prinzenstraße 5, 30159 Hannover
+www.lfd.niedersachsen.de
+
+5. Datensicherheit
+
+Dieses Portal nutzt aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher Inhalte eine SSL-/TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von "http://" auf "https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.
+
+Stand: März 2026
`;
+
+function MenuBar({ editor }: { editor: ReturnType }) {
+ if (!editor) return null;
+ const setLink = useCallback(() => {
+ const previousUrl = editor.getAttributes('link').href;
+ const url = window.prompt('URL eingeben:', previousUrl);
+ if (url === null) return;
+ if (url === '') { editor.chain().focus().extendMarkRange('link').unsetLink().run(); return; }
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
+ }, [editor]);
+ const btnClass = (active: boolean) => `p-1.5 rounded hover:bg-gray-200 transition-colors ${active ? 'bg-gray-200 text-blue-600' : 'text-gray-600'}`;
+ return (
+
+
editor.chain().focus().toggleBold().run()} className={btnClass(editor.isActive('bold'))} title="Fett">
+
editor.chain().focus().toggleItalic().run()} className={btnClass(editor.isActive('italic'))} title="Kursiv">
+
+
editor.chain().focus().toggleHeading({ level: 1 }).run()} className={btnClass(editor.isActive('heading', { level: 1 }))} title="Überschrift 1">
+
editor.chain().focus().toggleHeading({ level: 2 }).run()} className={btnClass(editor.isActive('heading', { level: 2 }))} title="Überschrift 2">
+
editor.chain().focus().toggleHeading({ level: 3 }).run()} className={btnClass(editor.isActive('heading', { level: 3 }))} title="Überschrift 3">
+
editor.chain().focus().setParagraph().run()} className={btnClass(editor.isActive('paragraph'))} title="Absatz">
+
+
editor.chain().focus().toggleBulletList().run()} className={btnClass(editor.isActive('bulletList'))} title="Aufzählung">
+
editor.chain().focus().toggleOrderedList().run()} className={btnClass(editor.isActive('orderedList'))} title="Nummerierung">
+
+
+
+
editor.chain().focus().undo().run()} disabled={!editor.can().undo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Rückgängig">
+
editor.chain().focus().redo().run()} disabled={!editor.can().redo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Wiederherstellen">
+
+ );
+}
+
+export default function WebsitePrivacyPolicyEditor() {
+ const queryClient = useQueryClient();
+ const [showPreview, setShowPreview] = useState(false);
+ const [saved, setSaved] = useState(false);
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['website-privacy-policy'],
+ queryFn: () => gdprApi.getWebsitePrivacyPolicy(),
+ });
+
+ const saveMutation = useMutation({
+ mutationFn: (html: string) => gdprApi.updateWebsitePrivacyPolicy(html),
+ onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['website-privacy-policy'] }); setSaved(true); setTimeout(() => setSaved(false), 2000); },
+ });
+
+ const initialContent = data?.data?.html || DEFAULT_POLICY;
+ const editor = useEditor({
+ extensions: [StarterKit, TiptapLink.configure({ openOnClick: false, HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer' } })],
+ content: initialContent,
+ editorProps: { attributes: { class: 'prose prose-sm max-w-none p-4 min-h-[400px] focus:outline-none' } },
+ }, [initialContent]);
+
+ const handleSave = () => { if (editor) saveMutation.mutate(editor.getHTML()); };
+ const handleReset = () => { if (confirm('Auf Standardtext zurücksetzen?')) editor?.commands.setContent(DEFAULT_POLICY); };
+
+ if (isLoading) return Laden...
;
+
+ return (
+
+
+
+
Website-Datenschutzerklärung bearbeiten
+
Standardtext laden
+
setShowPreview(!showPreview)}> {showPreview ? 'Editor' : 'Vorschau'}
+
{saved ? 'Gespeichert!' : saveMutation.isPending ? 'Speichern...' : 'Speichern'}
+
+ {showPreview ? (
+
+ ) : (
+
+
+
+
+ )}
+ {saveMutation.isError &&
Fehler beim Speichern.
}
+
+ );
+}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index b1787509..874cc0c4 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -1396,6 +1396,24 @@ export const gdprApi = {
const res = await api.put>('/gdpr/authorization-template', { html });
return res.data;
},
+ // Impressum
+ getImprint: async () => {
+ const res = await api.get>('/gdpr/imprint');
+ return res.data;
+ },
+ updateImprint: async (html: string) => {
+ const res = await api.put>('/gdpr/imprint', { html });
+ return res.data;
+ },
+ // Website-Datenschutzerklärung
+ getWebsitePrivacyPolicy: async () => {
+ const res = await api.get>('/gdpr/website-privacy-policy');
+ return res.data;
+ },
+ updateWebsitePrivacyPolicy: async (html: string) => {
+ const res = await api.put>('/gdpr/website-privacy-policy', { html });
+ return res.data;
+ },
// Consent-Link senden
sendConsentLink: async (customerId: number, channel: string) => {
const res = await api.post>(`/gdpr/customer/${customerId}/send-consent-link`, { channel });
diff --git a/plesktest/docker-compose.yml b/plesktest/docker-compose.yml
index 21ba9d1c..351c9664 100644
--- a/plesktest/docker-compose.yml
+++ b/plesktest/docker-compose.yml
@@ -8,7 +8,7 @@ services:
ports:
- "8443:8443" # Plesk Panel (HTTPS)
- "8880:8880" # Plesk Panel (HTTP)
- #- "25:25" # SMTP
+ - "25:25" # SMTP
- "465:465" # SMTPS
- "587:587" # Submission
- "143:143" # IMAP