From 40d76680fdd16e33eef9954bb10bb54488e05271 Mon Sep 17 00:00:00 2001 From: elpatron Date: Tue, 30 Sep 2025 18:14:01 +0200 Subject: [PATCH] Implementiere Impressum/Datenschutz-System und bereinige URL-Konfiguration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neues Impressum/Datenschutz-Tab mit konfigurierbaren rechtlichen Daten - Konfigurationsdatei legal-config.ts für alle rechtlichen Informationen - RPC-Endpoint legal.getConfig() für rechtliche Daten - Schöne Tab-Navigation zwischen Impressum und Datenschutz - Responsive Design mit Loading-States und Fehlerbehandlung - Alle rechtlichen Daten über Umgebungsvariablen konfigurierbar - FRONTEND_URL entfernt - nur noch DOMAIN wird verwendet - Hilfsfunktion generateUrl() für konsistente URL-Generierung - Code-Duplikation in bookings.ts eliminiert - .env.example aktualisiert mit allen neuen Variablen - README.md dokumentiert neue rechtliche Konfiguration - DSGVO- und TMG-konforme Inhalte implementiert --- .env.example | 44 ++++- README.md | 21 ++- docs/backlog.md | 7 +- src/client/app.tsx | 9 +- src/client/components/legal-page.tsx | 241 +++++++++++++++++++++++++++ src/server/lib/legal-config.ts | 76 +++++++++ src/server/rpc/bookings.ts | 26 ++- src/server/rpc/index.ts | 2 + src/server/rpc/legal.ts | 8 + 9 files changed, 407 insertions(+), 27 deletions(-) create mode 100644 src/client/components/legal-page.tsx create mode 100644 src/server/lib/legal-config.ts create mode 100644 src/server/rpc/legal.ts diff --git a/.env.example b/.env.example index 64ec7cd..9597aa4 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,50 @@ -# Domain -DOMAIN= +# Admin Account Configuration +ADMIN_USERNAME=owner +ADMIN_PASSWORD_HASH=YWRtaW4xMjM= # Base64 encoded password + +# Domain Configuration +DOMAIN=localhost:5173 # For production: your-domain.com # Email Configuration RESEND_API_KEY=your_resend_api_key_here EMAIL_FROM=noreply@yourdomain.com ADMIN_EMAIL=admin@yourdomain.com -# Admin Account Configuration -ADMIN_USERNAME=owner -ADMIN_PASSWORD_HASH=YWRtaW4xMjM= +# Frontend URL (for E-Mail Links) -# Min-Storno Time Span in hours +# Cancellation Policy (in hours) MIN_STORNO_TIMESPAN=24 +# Legal Information (Impressum/Datenschutz) +COMPANY_NAME=Stargirlnails Kiel +OWNER_NAME=Inhaber Name +ADDRESS_STREET=Musterstraße 123 +ADDRESS_CITY=Kiel +ADDRESS_POSTAL_CODE=24103 +ADDRESS_COUNTRY=Deutschland +CONTACT_PHONE=+49 431 123456 +CONTACT_EMAIL=info@stargirlnails.de + +# Business Details (Optional) +TAX_ID=12/345/67890 # Optional - Steuernummer +VAT_ID=DE123456789 # Optional - Umsatzsteuer-ID +COMMERCIAL_REGISTER=HRB 12345 # Optional - Handelsregister +RESPONSIBLE_FOR_CONTENT=Inhaber Name + +# Data Protection +DATA_PROTECTION_RESPONSIBLE=Inhaber Name +DATA_PROTECTION_EMAIL=datenschutz@stargirlnails.de +DATA_PROTECTION_PURPOSE=Terminbuchung und Kundenbetreuung +DATA_PROTECTION_LEGAL_BASIS=Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) und Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse) +DATA_PROTECTION_RETENTION=Buchungsdaten werden 3 Jahre nach Vertragsende gespeichert +DATA_PROTECTION_RIGHTS=Auskunft, Berichtigung, Löschung, Einschränkung, Widerspruch, Beschwerde bei der Aufsichtsbehörde +DATA_PROTECTION_COOKIES=Wir verwenden technisch notwendige Cookies für die Funktionalität der Website +DATA_PROTECTION_SECURITY=SSL-Verschlüsselung, sichere Server, regelmäßige Updates +DATA_PROTECTION_CONTACT=Bei Fragen zum Datenschutz wenden Sie sich an: datenschutz@stargirlnails.de + +# Third Party Services (comma-separated) +THIRD_PARTY_SERVICES=Resend (E-Mail-Versand),Google Analytics + # OpenAI Configuration (optional) OPENAI_API_KEY=your_openai_api_key_here diff --git a/README.md b/README.md index f266953..1b50afe 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,25 @@ RESEND_API_KEY=your_resend_api_key_here EMAIL_FROM=noreply@yourdomain.com ADMIN_EMAIL=admin@yourdomain.com -# Frontend URL (für E-Mail Links) -FRONTEND_URL=http://localhost:5173 - # Stornierungsfrist (in Stunden) MIN_STORNO_TIMESPAN=24 + +# Legal Information (Impressum/Datenschutz) +COMPANY_NAME=Stargirlnails Kiel +OWNER_NAME=Inhaber Name +ADDRESS_STREET=Musterstraße 123 +ADDRESS_CITY=Kiel +ADDRESS_POSTAL_CODE=24103 +ADDRESS_COUNTRY=Deutschland +CONTACT_PHONE=+49 431 123456 +CONTACT_EMAIL=info@stargirlnails.de +TAX_ID=12/345/67890 # Optional +VAT_ID=DE123456789 # Optional +COMMERCIAL_REGISTER=HRB 12345 # Optional +RESPONSIBLE_FOR_CONTENT=Inhaber Name +DATA_PROTECTION_RESPONSIBLE=Inhaber Name +DATA_PROTECTION_EMAIL=datenschutz@stargirlnails.de +THIRD_PARTY_SERVICES=Resend (E-Mail-Versand),Google Analytics # Komma-getrennt ``` ### 4. Anwendung starten @@ -166,6 +180,7 @@ docker run -d \ - 📧 **E-Mail-Benachrichtigungen**: Automatische Benachrichtigungen bei Buchungen - ❌ **Termin-Stornierung**: Kunden können Termine über sichere Links stornieren - ⏰ **Stornierungsfrist**: Konfigurierbare Mindestfrist vor dem Termin (MIN_STORNO_TIMESPAN) +- 📋 **Impressum/Datenschutz**: Rechtliche Seiten mit konfigurierbaren Daten - 🔐 **Admin-Panel**: Geschützter Bereich für Inhaber ## Admin-Zugang diff --git a/docs/backlog.md b/docs/backlog.md index 30bcc4b..36e116a 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -3,9 +3,9 @@ ### Kalender & Workflow - ICS-Anhang/Link in E‑Mails (Kalendereintrag) - Erinnerungsmails (24h/3h vor Termin) -- Umbuchen/Stornieren per sicherem Kundenlink (Token) +- ~~Umbuchen/Stornieren per sicherem Kundenlink (Token)~~ - Pufferzeiten und Sperrtage/Feiertage konfigurierbar -- Slots dynamisch an Behandlungsdauer anpassen; Überschneidungen verhindern +- ~~Slots dynamisch an Behandlungsdauer anpassen; Überschneidungen verhindern~~ ### Sicherheit & Qualität - Rate‑Limiting (IP/E‑Mail) für Formularspam @@ -13,6 +13,7 @@ - E‑Mail‑Verifizierung (Double‑Opt‑In) optional - Audit‑Log (wer/was/wann) - DSGVO: Einwilligungstexte, Löschkonzept +- Impressum ### E‑Mail & Infrastruktur - Retry/Backoff + Fallback‑Queue bei Resend‑Fehlern @@ -21,7 +22,7 @@ - Admin‑Digest (tägliche Übersicht) ### UX/UI -- Mobiler Kalender mit klarer Slot‑Visualisierung +- ~~Mobiler Kalender mit klarer Slot‑Visualisierung~~ - Kunden‑Statusseite (pending/confirmed) - Prominente Fehlerzustände inkl. Hinweise bei Doppelbuchung diff --git a/src/client/app.tsx b/src/client/app.tsx index 25bf30f..4b06a50 100644 --- a/src/client/app.tsx +++ b/src/client/app.tsx @@ -9,10 +9,11 @@ import { AdminCalendar } from "@/client/components/admin-calendar"; import { InitialDataLoader } from "@/client/components/initial-data-loader"; import { AdminAvailability } from "@/client/components/admin-availability"; import CancellationPage from "@/client/components/cancellation-page"; +import LegalPage from "@/client/components/legal-page"; function App() { const { user, isLoading, isOwner } = useAuth(); - const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "admin-calendar" | "admin-availability" | "profile">("booking"); + const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "admin-calendar" | "admin-availability" | "profile" | "legal">("booking"); // Check for cancellation token in URL useEffect(() => { @@ -58,8 +59,14 @@ function App() { return ; } + // Show legal page if legal tab is active + if (activeTab === "legal") { + return ; + } + const tabs = [ { id: "booking", label: "Termin buchen", icon: "📅", requiresAuth: false }, + { id: "legal", label: "Impressum/Datenschutz", icon: "📋", requiresAuth: false }, { id: "admin-treatments", label: "Behandlungen verwalten", icon: "💅", requiresAuth: true }, { id: "admin-bookings", label: "Buchungen verwalten", icon: "📋", requiresAuth: true }, { id: "admin-calendar", label: "Kalender", icon: "📆", requiresAuth: true }, diff --git a/src/client/components/legal-page.tsx b/src/client/components/legal-page.tsx new file mode 100644 index 0000000..d71cbb3 --- /dev/null +++ b/src/client/components/legal-page.tsx @@ -0,0 +1,241 @@ +import React, { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { queryClient } from "@/client/rpc-client"; + +export default function LegalPage() { + const [activeSection, setActiveSection] = useState<"impressum" | "datenschutz">("impressum"); + + const { data: legalConfig, isLoading, error } = useQuery({ + queryKey: ["legal", "config"], + queryFn: () => queryClient.legal.getConfig(), + }); + + if (isLoading) { + return ( +
+
+
+
+ Lade rechtliche Informationen... +
+
+
+ ); + } + + if (error || !legalConfig) { + return ( +
+
+
+
+ + + +
+

Fehler

+

+ Die rechtlichen Informationen konnten nicht geladen werden. +

+ + Zur Startseite + +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+
+ Stargil Nails Logo +
+

Rechtliche Informationen

+

Impressum und Datenschutz

+
+
+
+ + {/* Tab Navigation */} +
+
+ + +
+
+
+ + {/* Content */} +
+ {activeSection === "impressum" ? ( +
+

Impressum

+ +
+
+

Anbieter

+

+ {legalConfig.companyName}
+ Inhaber: {legalConfig.ownerName} +

+

+ ℹ️ Diese Informationen können über Umgebungsvariablen in der .env-Datei angepasst werden. +

+
+ +
+

Anschrift

+

+ {legalConfig.address.street}
+ {legalConfig.address.postalCode} {legalConfig.address.city}
+ {legalConfig.address.country} +

+
+ +
+

Kontakt

+

+ Telefon: {legalConfig.contact.phone}
+ E-Mail: {legalConfig.contact.email}
+ Website: {legalConfig.contact.website} +

+
+ + {legalConfig.businessDetails.taxId && ( +
+

Geschäftliche Angaben

+

+ {legalConfig.businessDetails.taxId && ( + <>Steuernummer: {legalConfig.businessDetails.taxId}
+ )} + {legalConfig.businessDetails.vatId && ( + <>USt-IdNr.: {legalConfig.businessDetails.vatId}
+ )} + {legalConfig.businessDetails.commercialRegister && ( + <>Handelsregister: {legalConfig.businessDetails.commercialRegister}
+ )} + Verantwortlich für den Inhalt: {legalConfig.businessDetails.responsibleForContent} +

+
+ )} + +
+

Haftungsausschluss

+

+ Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, + Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. + Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten + nach den allgemeinen Gesetzen verantwortlich. +

+
+
+
+ ) : ( +
+

Datenschutzerklärung

+ +
+
+

Verantwortlicher

+

+ {legalConfig.dataProtection.responsiblePerson}
+ E-Mail: {legalConfig.dataProtection.email} +

+
+ +
+

Zweck der Datenverarbeitung

+

{legalConfig.dataProtection.purpose}

+
+ +
+

Rechtsgrundlage

+

{legalConfig.dataProtection.legalBasis}

+
+ +
+

Datenspeicherung

+

{legalConfig.dataProtection.dataRetention}

+
+ +
+

Ihre Rechte

+

{legalConfig.dataProtection.rights}

+
+ +
+

Cookies

+

{legalConfig.dataProtection.cookies}

+
+ + {legalConfig.dataProtection.thirdPartyServices.length > 0 && ( +
+

Drittanbieter-Services

+
    + {legalConfig.dataProtection.thirdPartyServices.map((service, index) => ( +
  • {service}
  • + ))} +
+
+ )} + +
+

Datensicherheit

+

{legalConfig.dataProtection.dataSecurity}

+
+ +
+

Kontakt zum Datenschutz

+

{legalConfig.dataProtection.contactDataProtection}

+
+
+
+ )} +
+ + {/* Back to Home */} + +
+
+ ); +} diff --git a/src/server/lib/legal-config.ts b/src/server/lib/legal-config.ts new file mode 100644 index 0000000..1fe6f51 --- /dev/null +++ b/src/server/lib/legal-config.ts @@ -0,0 +1,76 @@ +// Legal configuration for Impressum and Datenschutz +export interface LegalConfig { + // Impressum data + companyName: string; + ownerName: string; + address: { + street: string; + city: string; + postalCode: string; + country: string; + }; + contact: { + phone: string; + email: string; + website: string; + }; + businessDetails: { + taxId?: string; + vatId?: string; + commercialRegister?: string; + responsibleForContent: string; + }; + + // Datenschutz data + dataProtection: { + responsiblePerson: string; + email: string; + purpose: string; + legalBasis: string; + dataRetention: string; + rights: string; + cookies: string; + thirdPartyServices: string[]; + dataSecurity: string; + contactDataProtection: string; + }; +} + +// Default configuration - should be overridden by environment variables +export const defaultLegalConfig: LegalConfig = { + companyName: process.env.COMPANY_NAME || "Stargirlnails Kiel", + ownerName: process.env.OWNER_NAME || "Inhaber Name", + address: { + street: process.env.ADDRESS_STREET || "Musterstraße 123", + city: process.env.ADDRESS_CITY || "Kiel", + postalCode: process.env.ADDRESS_POSTAL_CODE || "24103", + country: process.env.ADDRESS_COUNTRY || "Deutschland", + }, + contact: { + phone: process.env.CONTACT_PHONE || "+49 431 123456", + email: process.env.CONTACT_EMAIL || "info@stargirlnails.de", + website: process.env.DOMAIN || "stargirlnails.de", + }, + businessDetails: { + taxId: process.env.TAX_ID || "", + vatId: process.env.VAT_ID || "", + commercialRegister: process.env.COMMERCIAL_REGISTER || "", + responsibleForContent: process.env.RESPONSIBLE_FOR_CONTENT || "Inhaber Name", + }, + dataProtection: { + responsiblePerson: process.env.DATA_PROTECTION_RESPONSIBLE || "Inhaber Name", + email: process.env.DATA_PROTECTION_EMAIL || "datenschutz@stargirlnails.de", + purpose: process.env.DATA_PROTECTION_PURPOSE || "Terminbuchung und Kundenbetreuung", + legalBasis: process.env.DATA_PROTECTION_LEGAL_BASIS || "Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) und Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)", + dataRetention: process.env.DATA_PROTECTION_RETENTION || "Buchungsdaten werden 3 Jahre nach Vertragsende gespeichert", + rights: process.env.DATA_PROTECTION_RIGHTS || "Auskunft, Berichtigung, Löschung, Einschränkung, Widerspruch, Beschwerde bei der Aufsichtsbehörde", + cookies: process.env.DATA_PROTECTION_COOKIES || "Wir verwenden technisch notwendige Cookies für die Funktionalität der Website", + thirdPartyServices: process.env.THIRD_PARTY_SERVICES ? process.env.THIRD_PARTY_SERVICES.split(',') : ["Resend (E-Mail-Versand)"], + dataSecurity: process.env.DATA_PROTECTION_SECURITY || "SSL-Verschlüsselung, sichere Server, regelmäßige Updates", + contactDataProtection: process.env.DATA_PROTECTION_CONTACT || "Bei Fragen zum Datenschutz wenden Sie sich an: datenschutz@stargirlnails.de", + }, +}; + +export function getLegalConfig(): LegalConfig { + return defaultLegalConfig; +} diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts index 4bd47c6..75ab11c 100644 --- a/src/server/rpc/bookings.ts +++ b/src/server/rpc/bookings.ts @@ -19,6 +19,13 @@ function formatDateGerman(dateString: string): string { return `${day}.${month}.${year}`; } +// Helper function to generate URLs from DOMAIN environment variable +function generateUrl(path: string = ''): string { + const domain = process.env.DOMAIN || 'localhost:5173'; + const protocol = domain.includes('localhost') ? 'http' : 'https'; + return `${protocol}://${domain}${path}`; +} + const BookingSchema = z.object({ id: z.string(), treatmentId: z.string(), @@ -119,9 +126,7 @@ const create = os // Notify customer: request received (pending) void (async () => { const formattedDate = formatDateGerman(input.appointmentDate); - const domain = process.env.DOMAIN || 'localhost:5173'; - const protocol = domain.includes('localhost') ? 'http' : 'https'; - const homepageUrl = `${protocol}://${domain}`; + const homepageUrl = generateUrl(); const html = await renderBookingPendingHTML({ name: input.customerName, date: input.appointmentDate, time: input.appointmentTime }); await sendEmail({ to: input.customerEmail, @@ -150,9 +155,7 @@ const create = os hasInspirationPhoto: !!input.inspirationPhoto }); - const domain = process.env.DOMAIN || 'localhost:5173'; - const protocol = domain.includes('localhost') ? 'http' : 'https'; - const homepageUrl = `${protocol}://${domain}`; + const homepageUrl = generateUrl(); const adminText = `Neue Buchungsanfrage eingegangen:\n\n` + `Name: ${input.customerName}\n` + @@ -260,7 +263,8 @@ const updateStatus = os const cancellationToken = await queryClient.cancellation.createToken({ bookingId: booking.id }); const formattedDate = formatDateGerman(booking.appointmentDate); - const cancellationUrl = `${process.env.FRONTEND_URL || 'http://localhost:5173'}/cancel/${cancellationToken.token}`; + const cancellationUrl = generateUrl(`/cancel/${cancellationToken.token}`); + const homepageUrl = generateUrl(); const html = await renderBookingConfirmedHTML({ name: booking.customerName, @@ -269,10 +273,6 @@ const updateStatus = os cancellationUrl }); - const domain = process.env.DOMAIN || 'localhost:5173'; - const protocol = domain.includes('localhost') ? 'http' : 'https'; - const homepageUrl = `${protocol}://${domain}`; - await sendEmailWithAGB({ to: booking.customerEmail, subject: "Dein Termin wurde bestätigt - AGB im Anhang", @@ -282,9 +282,7 @@ const updateStatus = os }); } else if (input.status === "cancelled") { const formattedDate = formatDateGerman(booking.appointmentDate); - const domain = process.env.DOMAIN || 'localhost:5173'; - const protocol = domain.includes('localhost') ? 'http' : 'https'; - const homepageUrl = `${protocol}://${domain}`; + const homepageUrl = generateUrl(); const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime }); await sendEmail({ to: booking.customerEmail, diff --git a/src/server/rpc/index.ts b/src/server/rpc/index.ts index 6419933..e9f8f20 100644 --- a/src/server/rpc/index.ts +++ b/src/server/rpc/index.ts @@ -4,6 +4,7 @@ import { router as bookings } from "./bookings"; import { router as auth } from "./auth"; import { router as availability } from "./availability"; import { router as cancellation } from "./cancellation"; +import { router as legal } from "./legal"; export const router = { demo, @@ -12,4 +13,5 @@ export const router = { auth, availability, cancellation, + legal, }; diff --git a/src/server/rpc/legal.ts b/src/server/rpc/legal.ts new file mode 100644 index 0000000..610bb8b --- /dev/null +++ b/src/server/rpc/legal.ts @@ -0,0 +1,8 @@ +import { os } from "@orpc/server"; +import { getLegalConfig } from "@/server/lib/legal-config"; + +export const router = { + getConfig: os.handler(async () => { + return getLegalConfig(); + }), +};