feat(legal): Claims-Korrektur, zentrale Legal-Config & öffentliche Rechtsseiten (v1.7.0)

Teil 1 des Compliance-Updates (nach vollständiger Repo-Analyse):

- src/config/legal.ts: zentrale, typisierte Betreiber-/Rechtskonfiguration mit sichtbaren
  Platzhaltern, ENV-Overrides (LEGAL_*), Build-Warnung bei offenen Pflichtangaben, echte
  Subprozessorenliste (aus dem Code abgeleitet)
- Claims-Korrektur (20 Aussagen): 24/7, 100% Swiss Hosted, DSG/DSGVO-konform, 100% der Spende,
  Kein Unternehmen, "bleibt kostenlos", "sicher via Stripe", "keine Weitergabe/keine
  Übermittlung ins Ausland", "verschlüsselt"→"gehasht" — in page.tsx, spenden, impressum,
  datenschutz (Neuaufbau), settings-tab
- Neue Legal-Seiten (druckbar, responsiv, versioniert, als Entwurf gekennzeichnet):
  /nutzungsbedingungen, /verantwortungsvolle-nutzung, /sicherheit, /unterstuetzen,
  /organisationen/datenschutzvereinbarung + wiederverwendbare LegalPage-Shell
- Datenschutz komplett neu aufgebaut: Subprozessoren-Tabelle (#dienstleister), Auslandbekanntgabe,
  gehashte Passwörter, Backups/Löschung, Zahlung (Stripe), Karten/Geodienste, Rechte/EDÖB
- Footer um alle Pflicht-Rechtslinks erweitert; sitemap.ts ergänzt
- docs/legal/website-claims-audit.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 11:57:39 +02:00
parent 6aa70e1504
commit 872b8927cc
15 changed files with 1069 additions and 207 deletions

164
src/config/legal.ts Normal file
View File

@@ -0,0 +1,164 @@
/**
* Zentrale, typisierte Betreiber- und Rechtskonfiguration für Lageplan.
*
* WARUM: Rechtliche Angaben (Betreiber, Kontakt, Serverstandort, Dokumentversionen)
* dürfen NICHT an mehreren Stellen widersprüchlich hartcodiert werden. Alle Legal-Seiten,
* das Impressum, die Datenschutzerklärung, der Consent-Flow und die Export-Footer lesen
* ihre Angaben aus dieser einen Quelle.
*
* PLATZHALTER: Felder, die der Betreiber rechtlich verbindlich festlegen muss, tragen als
* Default einen deutlich sichtbaren Platzhalter im Format `[NAME]`. Diese Platzhalter dürfen
* NICHT stillschweigend durch erfundene Werte ersetzt werden. Setze die echten Werte über
* Umgebungsvariablen (siehe unten) oder trage sie hier direkt ein, sobald sie feststehen.
*
* VALIDIERUNG: `assertLegalConfig()` warnt in Development und beim Build, wenn Pflichtangaben
* noch Platzhalter sind. In Produktion werden Platzhalter in der UI als solche kenntlich
* gemacht (siehe `isPlaceholder()`), damit nie ein erfundener Wert als echt erscheint.
*/
/** Präfix, an dem ein noch nicht ausgefüllter Pflichtwert erkannt wird. */
const PLACEHOLDER_PREFIX = '['
/** True, wenn der Wert noch ein unausgefüllter Platzhalter ist. */
export function isPlaceholder(value: string | null | undefined): boolean {
return !value || (value.trim().startsWith(PLACEHOLDER_PREFIX) && value.trim().endsWith(']'))
}
/** Liest eine Umgebungsvariable, fällt sonst auf den Platzhalter/Default zurück. */
function env(name: string, fallback: string): string {
const v = process.env[name]
return v && v.trim().length > 0 ? v.trim() : fallback
}
export interface ExternalProcessor {
/** Anzeigename des Dienstleisters */
name: string
/** Zweck der Bearbeitung */
purpose: string
/** Verarbeitete Datenarten (grob) */
data: string
/** Sitz / relevante Verarbeitungsregion, soweit bekannt; sonst Platzhalter */
region: string
/** true, wenn der Dienst nur bei Bedarf/optionaler Konfiguration aktiv ist */
optional?: boolean
}
export interface LegalDocumentMeta {
/** Interne, bewusst gesetzte Versionsangabe (nicht automatisch aus Dateiänderung ableiten!) */
version: string
/** Veröffentlichungsdatum ISO (YYYY-MM-DD) */
publishedAt: string
}
export const legal = {
// ─── Betreiber ─────────────────────────────────────────────
/** Anzeigename des Projekts */
projectName: 'Lageplan',
/**
* Rechtlich verbindlicher Name des Betreibers. Im Repository wird das Projekt öffentlich
* "Pepe Ziberi / Purepixel" zugeschrieben; die rechtsverbindliche Bezeichnung inkl.
* Rechtsform muss der Betreiber bestätigen.
*/
operatorName: env('LEGAL_OPERATOR_NAME', '[RECHTLICHER NAME DES BETREIBERS]'),
/** Rechtsform (z.B. Einzelunternehmen, Verein, GmbH) muss festgelegt werden */
operatorLegalForm: env('LEGAL_OPERATOR_FORM', '[RECHTSFORM]'),
/** Vollständige Postadresse des Betreibers */
postalAddress: env('LEGAL_POSTAL_ADDRESS', '[POSTADRESSE]'),
// ─── Kontakt ───────────────────────────────────────────────
/** Allgemeine Support-/Kontaktadresse (im Repo öffentlich verwendet) */
supportEmail: env('LEGAL_SUPPORT_EMAIL', 'app@lageplan.ch'),
/**
* Datenschutz-Kontakt. Default = im Repository bereits verwendete Adresse
* (nicht erfunden). Der Betreiber muss bestätigen, dass dieses Postfach aktiv überwacht wird.
*/
privacyContactEmail: env('LEGAL_PRIVACY_EMAIL', 'datenschutz@lageplan.ch'),
// ─── Domains ───────────────────────────────────────────────
siteDomain: env('LEGAL_SITE_DOMAIN', 'lageplan.ch'),
appDomain: env('LEGAL_APP_DOMAIN', 'app.lageplan.ch'),
// ─── Recht & Gerichtsstand ────────────────────────────────
country: 'Schweiz',
governingLaw: 'Schweizer Recht',
/** Gerichtsstand nur angeben, wenn juristisch sinnvoll/bestätigt; sonst Platzhalter. */
jurisdiction: env('LEGAL_JURISDICTION', '[GERICHTSSTAND]'),
// ─── Infrastruktur ────────────────────────────────────────
/** Hostinganbieter muss bestätigt werden (im Repo nicht eindeutig belegt) */
hostingProvider: env('LEGAL_HOSTING_PROVIDER', '[HOSTINGANBIETER]'),
/** Serverstandort muss für ALLE Anbieter, Backups und Logs bestätigt werden */
serverLocation: env('LEGAL_SERVER_LOCATION', '[SERVERSTANDORT]'),
/** Aufbewahrungsfrist von Sicherungskopien muss festgelegt werden */
backupRetention: env('LEGAL_BACKUP_RETENTION', '[BACKUP-AUFBEWAHRUNGSFRIST]'),
// ─── Externe Dienstleister (Ist-Zustand aus dem Code abgeleitet) ──
// Nichts erfinden: nur tatsächlich im Code kontaktierte Dienste. Sitz/Region, die nicht
// sicher bekannt ist, bleibt Platzhalter.
processors: [
{ name: 'OpenStreetMap (Tiles)', purpose: 'Basiskarte', data: 'Kachel-Koordinaten (Kartenausschnitt)', region: '[REGION OSM Foundation, EU/UK]' },
{ name: 'Esri ArcGIS World Imagery', purpose: 'Satellitenkarte', data: 'Kachel-Koordinaten', region: '[REGION]' },
{ name: 'swisstopo (geo.admin.ch)', purpose: 'Schweizer Landeskarte', data: 'Kachel-Koordinaten', region: 'Schweiz' },
{ name: 'Nominatim (OpenStreetMap)', purpose: 'Adress-/Reverse-Geocoding', data: 'Adress-/Ortssuche, GPS-Koordinaten', region: '[REGION OSM Foundation]' },
{ name: 'Open-Meteo', purpose: 'Wind- und Höhendaten', data: 'Koordinaten', region: '[REGION EU]' },
{ name: 'Open-Elevation', purpose: 'Höhendaten (Fallback)', data: 'Koordinaten', region: '[REGION]' },
{ name: 'Stripe', purpose: 'Zahlungsabwicklung freiwilliger Unterstützungsbeiträge', data: 'Betrag, freiwilliger Name/Nachricht, Zahlungsdaten (direkt bei Stripe)', region: '[REGION u.a. USA/EU; Auslandbearbeitung möglich]', optional: true },
{ name: 'SMTP-/E-Mail-Anbieter', purpose: 'System-E-Mails (Verifizierung, Reset, Kontakt, Einladungen)', data: 'E-Mail-Adresse, Nachrichteninhalt', region: '[REGION abhängig vom konfigurierten Anbieter]', optional: true },
{ name: 'MinIO (Objektspeicher)', purpose: 'Datei-/Bild-Uploads (Pläne, Logos, Symbole)', data: 'hochgeladene Dateien', region: '[SERVERSTANDORT]' },
{ name: 'PostgreSQL (Datenbank)', purpose: 'Primäre Datenhaltung', data: 'Konto-, Organisations-, Plan- und Journaldaten', region: '[SERVERSTANDORT]' },
] as ExternalProcessor[],
// ─── Rechtsdokumente: bewusst gesetzte Versionen ──────────
// WICHTIG: Version manuell erhöhen, wenn sich der Inhalt WESENTLICH ändert (löst Re-Consent aus).
// Nicht automatisch aus minimalen Dateiänderungen ableiten.
documents: {
terms: { version: '1.0.0-draft', publishedAt: '2026-07-23' },
privacy: { version: '1.0.0-draft', publishedAt: '2026-07-23' },
organizationDeclaration: { version: '1.0.0-draft', publishedAt: '2026-07-23' },
responsibleUse: { version: '1.0.0-draft', publishedAt: '2026-07-23' },
dataProcessingAgreement: { version: '1.0.0-draft', publishedAt: '2026-07-23' },
} as Record<string, LegalDocumentMeta>,
/** Hinweis, der auf allen sorgfältig erstellten, aber juristisch ungeprüften Texten steht. */
draftNotice:
'Diese rechtlichen und organisatorischen Unterlagen wurden nach bestem Wissen erstellt. ' +
'Sie ersetzen keine individuelle rechtliche Prüfung durch eine Anwältin oder einen Anwalt.',
} as const
/** Pflichtfelder, die vor Produktivbetrieb ausgefüllt sein müssen. */
const REQUIRED_FIELDS: { key: string; value: string }[] = [
{ key: 'operatorName', value: legal.operatorName },
{ key: 'operatorLegalForm', value: legal.operatorLegalForm },
{ key: 'postalAddress', value: legal.postalAddress },
{ key: 'privacyContactEmail', value: legal.privacyContactEmail },
{ key: 'hostingProvider', value: legal.hostingProvider },
{ key: 'serverLocation', value: legal.serverLocation },
{ key: 'backupRetention', value: legal.backupRetention },
]
/** Liste der noch offenen Pflichtangaben (Platzhalter). */
export function missingLegalFields(): string[] {
return REQUIRED_FIELDS.filter(f => isPlaceholder(f.value)).map(f => f.key)
}
/**
* Warnt beim Build/Start, wenn Pflichtangaben noch Platzhalter sind.
* Bricht den Build NICHT ab (damit Entwicklung möglich bleibt), macht das Risiko aber sichtbar.
*/
export function assertLegalConfig(): void {
const missing = missingLegalFields()
if (missing.length > 0) {
const msg = `⚠️ [legal.ts] Offene rechtliche Pflichtangaben (noch Platzhalter): ${missing.join(', ')}. ` +
`Bitte in src/config/legal.ts bzw. via Umgebungsvariablen (LEGAL_*) setzen, bevor produktiv gegangen wird.`
// In Produktion als Fehler-Log, in Dev als Warnung — nie stillschweigend.
if (process.env.NODE_ENV === 'production') console.error(msg)
else console.warn(msg)
}
}
// Beim Laden der Konfiguration einmalig prüfen (serverseitig).
if (typeof window === 'undefined') {
assertLegalConfig()
}
export type LegalConfig = typeof legal