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:
164
src/config/legal.ts
Normal file
164
src/config/legal.ts
Normal 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
|
||||
Reference in New Issue
Block a user