Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
966 lines
33 KiB
TypeScript
966 lines
33 KiB
TypeScript
/**
|
|
* Privacy Policy Generator
|
|
*
|
|
* Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog.
|
|
* Die DSI wird aus 9 Abschnitten generiert:
|
|
*
|
|
* 1. Verantwortlicher (companyInfo)
|
|
* 2. Erhobene Daten (dataPoints nach Kategorie)
|
|
* 3. Verarbeitungszwecke (dataPoints.purpose)
|
|
* 4. Rechtsgrundlagen (dataPoints.legalBasis)
|
|
* 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients)
|
|
* 6. Speicherdauer (retentionMatrix)
|
|
* 7. Betroffenenrechte (statischer Text + Links)
|
|
* 8. Cookies (cookieCategory-basiert)
|
|
* 9. Aenderungen (statischer Text + Versionierung)
|
|
*/
|
|
|
|
import {
|
|
DataPoint,
|
|
DataPointCategory,
|
|
CompanyInfo,
|
|
PrivacyPolicySection,
|
|
GeneratedPrivacyPolicy,
|
|
SupportedLanguage,
|
|
ExportFormat,
|
|
LocalizedText,
|
|
RetentionMatrixEntry,
|
|
LegalBasis,
|
|
CATEGORY_METADATA,
|
|
LEGAL_BASIS_INFO,
|
|
RETENTION_PERIOD_INFO,
|
|
ARTICLE_9_WARNING,
|
|
} from '../types'
|
|
import { RETENTION_MATRIX } from '../catalog/loader'
|
|
|
|
// =============================================================================
|
|
// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge
|
|
// =============================================================================
|
|
|
|
const ALL_CATEGORIES: DataPointCategory[] = [
|
|
'MASTER_DATA', // A
|
|
'CONTACT_DATA', // B
|
|
'AUTHENTICATION', // C
|
|
'CONSENT', // D
|
|
'COMMUNICATION', // E
|
|
'PAYMENT', // F
|
|
'USAGE_DATA', // G
|
|
'LOCATION', // H
|
|
'DEVICE_DATA', // I
|
|
'MARKETING', // J
|
|
'ANALYTICS', // K
|
|
'SOCIAL_MEDIA', // L
|
|
'HEALTH_DATA', // M - Art. 9 DSGVO
|
|
'EMPLOYEE_DATA', // N - BDSG § 26
|
|
'CONTRACT_DATA', // O
|
|
'LOG_DATA', // P
|
|
'AI_DATA', // Q - AI Act
|
|
'SECURITY', // R
|
|
]
|
|
|
|
// Alle Rechtsgrundlagen in der richtigen Reihenfolge
|
|
const ALL_LEGAL_BASES: LegalBasis[] = [
|
|
'CONTRACT',
|
|
'CONSENT',
|
|
'EXPLICIT_CONSENT',
|
|
'LEGITIMATE_INTEREST',
|
|
'LEGAL_OBLIGATION',
|
|
'VITAL_INTERESTS',
|
|
'PUBLIC_INTEREST',
|
|
]
|
|
|
|
// =============================================================================
|
|
// HELPER FUNCTIONS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Holt den lokalisierten Text
|
|
*/
|
|
function t(text: LocalizedText, language: SupportedLanguage): string {
|
|
return text[language]
|
|
}
|
|
|
|
/**
|
|
* Gruppiert Datenpunkte nach Kategorie
|
|
*/
|
|
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
|
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
|
for (const dp of dataPoints) {
|
|
const existing = grouped.get(dp.category) || []
|
|
grouped.set(dp.category, [...existing, dp])
|
|
}
|
|
return grouped
|
|
}
|
|
|
|
/**
|
|
* Gruppiert Datenpunkte nach Rechtsgrundlage
|
|
*/
|
|
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
|
|
const grouped = new Map<LegalBasis, DataPoint[]>()
|
|
for (const dp of dataPoints) {
|
|
const existing = grouped.get(dp.legalBasis) || []
|
|
grouped.set(dp.legalBasis, [...existing, dp])
|
|
}
|
|
return grouped
|
|
}
|
|
|
|
/**
|
|
* Extrahiert alle einzigartigen Drittanbieter
|
|
*/
|
|
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
|
const thirdParties = new Set<string>()
|
|
for (const dp of dataPoints) {
|
|
for (const recipient of dp.thirdPartyRecipients) {
|
|
thirdParties.add(recipient)
|
|
}
|
|
}
|
|
return Array.from(thirdParties).sort()
|
|
}
|
|
|
|
/**
|
|
* Formatiert ein Datum fuer die Anzeige
|
|
*/
|
|
function formatDate(date: Date, language: SupportedLanguage): string {
|
|
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECTION GENERATORS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Abschnitt 1: Verantwortlicher
|
|
*/
|
|
function generateControllerSection(
|
|
companyInfo: CompanyInfo,
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '1. Verantwortlicher',
|
|
en: '1. Data Controller',
|
|
}
|
|
|
|
const dpoSection = companyInfo.dpoName
|
|
? language === 'de'
|
|
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
|
|
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
|
|
: ''
|
|
|
|
const content: LocalizedText = {
|
|
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
|
|
|
|
**${companyInfo.name}**
|
|
${companyInfo.address}
|
|
${companyInfo.postalCode} ${companyInfo.city}
|
|
${companyInfo.country}
|
|
|
|
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
|
en: `The controller responsible for data processing on this website is:
|
|
|
|
**${companyInfo.name}**
|
|
${companyInfo.address}
|
|
${companyInfo.postalCode} ${companyInfo.city}
|
|
${companyInfo.country}
|
|
|
|
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
|
}
|
|
|
|
return {
|
|
id: 'controller',
|
|
order: 1,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: true,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 2: Erhobene Daten (18 Kategorien)
|
|
*/
|
|
function generateDataCollectionSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '2. Erhobene personenbezogene Daten',
|
|
en: '2. Personal Data We Collect',
|
|
}
|
|
|
|
const grouped = groupByCategory(dataPoints)
|
|
const sections: string[] = []
|
|
|
|
// Prüfe ob Art. 9 Daten enthalten sind
|
|
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
|
|
|
for (const category of ALL_CATEGORIES) {
|
|
const categoryData = grouped.get(category)
|
|
if (!categoryData || categoryData.length === 0) continue
|
|
|
|
const categoryMeta = CATEGORY_METADATA[category]
|
|
if (!categoryMeta) continue
|
|
|
|
const categoryTitle = t(categoryMeta.name, language)
|
|
|
|
// Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten)
|
|
let categoryNote = ''
|
|
if (category === 'HEALTH_DATA') {
|
|
categoryNote = language === 'de'
|
|
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
|
|
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
|
|
} else if (category === 'EMPLOYEE_DATA') {
|
|
categoryNote = language === 'de'
|
|
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
|
|
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
|
|
} else if (category === 'AI_DATA') {
|
|
categoryNote = language === 'de'
|
|
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
|
|
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
|
|
}
|
|
|
|
const dataList = categoryData
|
|
.map((dp) => {
|
|
const specialTag = dp.isSpecialCategory
|
|
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
|
: ''
|
|
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
|
|
})
|
|
.join('\n')
|
|
|
|
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
|
|
}
|
|
|
|
const intro: LocalizedText = {
|
|
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
|
|
en: 'We collect and process the following personal data:',
|
|
}
|
|
|
|
// Zusätzlicher Hinweis für Art. 9 Daten
|
|
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
|
|
? {
|
|
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
|
|
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
|
|
}
|
|
: { de: '', en: '' }
|
|
|
|
const content: LocalizedText = {
|
|
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
|
|
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
|
|
}
|
|
|
|
return {
|
|
id: 'data-collection',
|
|
order: 2,
|
|
title,
|
|
content,
|
|
dataPointIds: dataPoints.map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 3: Verarbeitungszwecke
|
|
*/
|
|
function generatePurposesSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '3. Zwecke der Datenverarbeitung',
|
|
en: '3. Purposes of Data Processing',
|
|
}
|
|
|
|
// Gruppiere nach Zweck (unique purposes)
|
|
const purposes = new Map<string, DataPoint[]>()
|
|
for (const dp of dataPoints) {
|
|
const purpose = t(dp.purpose, language)
|
|
const existing = purposes.get(purpose) || []
|
|
purposes.set(purpose, [...existing, dp])
|
|
}
|
|
|
|
const purposeList = Array.from(purposes.entries())
|
|
.map(([purpose, dps]) => {
|
|
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
|
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
|
|
})
|
|
.join('\n\n')
|
|
|
|
const content: LocalizedText = {
|
|
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
|
|
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
|
|
}
|
|
|
|
return {
|
|
id: 'purposes',
|
|
order: 3,
|
|
title,
|
|
content,
|
|
dataPointIds: dataPoints.map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen)
|
|
*/
|
|
function generateLegalBasisSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '4. Rechtsgrundlagen der Verarbeitung',
|
|
en: '4. Legal Basis for Processing',
|
|
}
|
|
|
|
const grouped = groupByLegalBasis(dataPoints)
|
|
const sections: string[] = []
|
|
|
|
// Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge
|
|
for (const basis of ALL_LEGAL_BASES) {
|
|
const basisData = grouped.get(basis)
|
|
if (!basisData || basisData.length === 0) continue
|
|
|
|
const basisInfo = LEGAL_BASIS_INFO[basis]
|
|
if (!basisInfo) continue
|
|
|
|
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
|
|
const basisDesc = t(basisInfo.description, language)
|
|
|
|
// Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen
|
|
let additionalWarning = ''
|
|
if (basis === 'EXPLICIT_CONSENT') {
|
|
additionalWarning = language === 'de'
|
|
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
|
|
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
|
|
}
|
|
|
|
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
|
|
const dataList = basisData
|
|
.map((dp) => {
|
|
const specialTag = dp.isSpecialCategory
|
|
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
|
: ''
|
|
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
|
|
})
|
|
.join('\n')
|
|
|
|
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
|
|
}
|
|
|
|
const content: LocalizedText = {
|
|
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
|
|
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
|
|
}
|
|
|
|
return {
|
|
id: 'legal-basis',
|
|
order: 4,
|
|
title,
|
|
content,
|
|
dataPointIds: dataPoints.map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 5: Empfaenger / Dritte
|
|
*/
|
|
function generateRecipientsSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '5. Empfaenger und Datenweitergabe',
|
|
en: '5. Recipients and Data Sharing',
|
|
}
|
|
|
|
const thirdParties = extractThirdParties(dataPoints)
|
|
|
|
if (thirdParties.length === 0) {
|
|
const content: LocalizedText = {
|
|
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
|
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
|
}
|
|
return {
|
|
id: 'recipients',
|
|
order: 5,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: true,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
// Gruppiere nach Drittanbieter
|
|
const recipientDetails = new Map<string, DataPoint[]>()
|
|
for (const dp of dataPoints) {
|
|
for (const recipient of dp.thirdPartyRecipients) {
|
|
const existing = recipientDetails.get(recipient) || []
|
|
recipientDetails.set(recipient, [...existing, dp])
|
|
}
|
|
}
|
|
|
|
const recipientList = Array.from(recipientDetails.entries())
|
|
.map(([recipient, dps]) => {
|
|
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
|
return `- **${recipient}**: ${dataNames}`
|
|
})
|
|
.join('\n')
|
|
|
|
const content: LocalizedText = {
|
|
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
|
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
|
}
|
|
|
|
return {
|
|
id: 'recipients',
|
|
order: 5,
|
|
title,
|
|
content,
|
|
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 6: Speicherdauer
|
|
*/
|
|
function generateRetentionSection(
|
|
dataPoints: DataPoint[],
|
|
retentionMatrix: RetentionMatrixEntry[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '6. Speicherdauer',
|
|
en: '6. Data Retention',
|
|
}
|
|
|
|
const grouped = groupByCategory(dataPoints)
|
|
const sections: string[] = []
|
|
|
|
for (const entry of retentionMatrix) {
|
|
const categoryData = grouped.get(entry.category)
|
|
if (!categoryData || categoryData.length === 0) continue
|
|
|
|
const categoryName = t(entry.categoryName, language)
|
|
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
|
|
|
const dataRetention = categoryData
|
|
.map((dp) => {
|
|
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
|
return `- ${t(dp.name, language)}: ${period}`
|
|
})
|
|
.join('\n')
|
|
|
|
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
|
}
|
|
|
|
const content: LocalizedText = {
|
|
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
|
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
|
}
|
|
|
|
return {
|
|
id: 'retention',
|
|
order: 6,
|
|
title,
|
|
content,
|
|
dataPointIds: dataPoints.map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO)
|
|
* Wird nur generiert, wenn Art. 9 Daten vorhanden sind
|
|
*/
|
|
function generateSpecialCategoriesSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection | null {
|
|
// Filtere Art. 9 Datenpunkte
|
|
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
|
|
|
if (specialCategoryDataPoints.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const title: LocalizedText = {
|
|
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
|
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
|
}
|
|
|
|
const dataList = specialCategoryDataPoints
|
|
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
|
.join('\n')
|
|
|
|
const content: LocalizedText = {
|
|
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
|
|
|
${dataList}
|
|
|
|
### Ihre ausdrueckliche Einwilligung
|
|
|
|
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
|
|
|
### Ihre Rechte bei Art. 9 Daten
|
|
|
|
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
|
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
|
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
|
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
|
|
|
### Besondere Schutzmassnahmen
|
|
|
|
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
|
- Ende-zu-Ende-Verschluesselung
|
|
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
|
- Audit-Logging aller Zugriffe
|
|
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
|
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
|
|
|
${dataList}
|
|
|
|
### Your Explicit Consent
|
|
|
|
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
|
|
|
### Your Rights Regarding Art. 9 Data
|
|
|
|
- You can **withdraw your consent at any time**
|
|
- Withdrawal does not affect the lawfulness of previous processing
|
|
- Upon withdrawal, your data will be deleted immediately
|
|
- You have the right to **access, rectification, and erasure**
|
|
|
|
### Special Protection Measures
|
|
|
|
For this sensitive data, we have implemented special technical and organizational measures:
|
|
- End-to-end encryption
|
|
- Strict access control (need-to-know principle)
|
|
- Audit logging of all access
|
|
- Regular data protection impact assessments`,
|
|
}
|
|
|
|
return {
|
|
id: 'special-categories',
|
|
order: 6.5, // Zwischen Speicherdauer (6) und Rechte (7)
|
|
title,
|
|
content,
|
|
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
|
isRequired: false,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 7: Betroffenenrechte
|
|
*/
|
|
function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '7. Ihre Rechte als betroffene Person',
|
|
en: '7. Your Rights as a Data Subject',
|
|
}
|
|
|
|
const content: LocalizedText = {
|
|
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
|
|
|
### Auskunftsrecht (Art. 15 DSGVO)
|
|
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
|
|
|
### Recht auf Berichtigung (Art. 16 DSGVO)
|
|
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
|
|
|
### Recht auf Loeschung (Art. 17 DSGVO)
|
|
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
|
|
|
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
|
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
|
|
|
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
|
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
|
|
|
### Widerspruchsrecht (Art. 21 DSGVO)
|
|
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
|
|
|
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
|
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
|
|
|
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
|
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
|
|
|
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
|
en: `You have the following rights regarding your personal data:
|
|
|
|
### Right of Access (Art. 15 GDPR)
|
|
You have the right to request information about the personal data we process about you.
|
|
|
|
### Right to Rectification (Art. 16 GDPR)
|
|
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
|
|
|
### Right to Erasure (Art. 17 GDPR)
|
|
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
|
|
|
### Right to Restriction of Processing (Art. 18 GDPR)
|
|
You have the right to request the restriction of processing of your data.
|
|
|
|
### Right to Data Portability (Art. 20 GDPR)
|
|
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
|
|
|
### Right to Object (Art. 21 GDPR)
|
|
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
|
|
|
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
|
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
|
|
|
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
|
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
|
|
|
**To exercise your rights, please contact us using the contact details provided above.**`,
|
|
}
|
|
|
|
return {
|
|
id: 'rights',
|
|
order: 7,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: true,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 8: Cookies
|
|
*/
|
|
function generateCookiesSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '8. Cookies und aehnliche Technologien',
|
|
en: '8. Cookies and Similar Technologies',
|
|
}
|
|
|
|
// Filtere Datenpunkte mit Cookie-Kategorie
|
|
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
|
|
|
if (cookieDataPoints.length === 0) {
|
|
const content: LocalizedText = {
|
|
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
|
en: 'We do not use cookies on this website.',
|
|
}
|
|
return {
|
|
id: 'cookies',
|
|
order: 8,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: false,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
// Gruppiere nach Cookie-Kategorie
|
|
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
|
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
|
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
|
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
|
|
|
const sections: string[] = []
|
|
|
|
if (essential.length > 0) {
|
|
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
|
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
if (performance.length > 0) {
|
|
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
|
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
if (personalization.length > 0) {
|
|
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
|
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
if (externalMedia.length > 0) {
|
|
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
|
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
const intro: LocalizedText = {
|
|
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
|
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
|
}
|
|
|
|
const content: LocalizedText = {
|
|
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
|
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
|
}
|
|
|
|
return {
|
|
id: 'cookies',
|
|
order: 8,
|
|
title,
|
|
content,
|
|
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abschnitt 9: Aenderungen
|
|
*/
|
|
function generateChangesSection(
|
|
version: string,
|
|
date: Date,
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
|
en: '9. Changes to this Privacy Policy',
|
|
}
|
|
|
|
const formattedDate = formatDate(date, language)
|
|
|
|
const content: LocalizedText = {
|
|
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
|
|
|
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
|
|
|
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
|
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
|
|
|
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
|
|
|
The new privacy policy will then apply for your next visit.`,
|
|
}
|
|
|
|
return {
|
|
id: 'changes',
|
|
order: 9,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: true,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN GENERATOR FUNCTIONS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9)
|
|
*/
|
|
export function generatePrivacyPolicySections(
|
|
dataPoints: DataPoint[],
|
|
companyInfo: CompanyInfo,
|
|
language: SupportedLanguage,
|
|
version: string = '1.0.0'
|
|
): PrivacyPolicySection[] {
|
|
const now = new Date()
|
|
|
|
const sections: PrivacyPolicySection[] = [
|
|
generateControllerSection(companyInfo, language),
|
|
generateDataCollectionSection(dataPoints, language),
|
|
generatePurposesSection(dataPoints, language),
|
|
generateLegalBasisSection(dataPoints, language),
|
|
generateRecipientsSection(dataPoints, language),
|
|
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
|
]
|
|
|
|
// Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden
|
|
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
|
if (specialCategoriesSection) {
|
|
sections.push(specialCategoriesSection)
|
|
}
|
|
|
|
sections.push(
|
|
generateRightsSection(language),
|
|
generateCookiesSection(dataPoints, language),
|
|
generateChangesSection(version, now, language)
|
|
)
|
|
|
|
// Abschnittsnummern neu vergeben
|
|
sections.forEach((section, index) => {
|
|
section.order = index + 1
|
|
// Titel-Nummer aktualisieren
|
|
const titleDe = section.title.de
|
|
const titleEn = section.title.en
|
|
if (titleDe.match(/^\d+[a-z]?\./)) {
|
|
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
|
}
|
|
if (titleEn.match(/^\d+[a-z]?\./)) {
|
|
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
|
}
|
|
})
|
|
|
|
return sections
|
|
}
|
|
|
|
/**
|
|
* Generiert die vollstaendige Privacy Policy
|
|
*/
|
|
export function generatePrivacyPolicy(
|
|
tenantId: string,
|
|
dataPoints: DataPoint[],
|
|
companyInfo: CompanyInfo,
|
|
language: SupportedLanguage,
|
|
format: ExportFormat = 'HTML'
|
|
): GeneratedPrivacyPolicy {
|
|
const version = '1.0.0'
|
|
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
|
|
|
// Generiere den Inhalt
|
|
const content = renderPrivacyPolicy(sections, language, format)
|
|
|
|
return {
|
|
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
|
tenantId,
|
|
language,
|
|
sections,
|
|
companyInfo,
|
|
generatedAt: new Date(),
|
|
version,
|
|
format,
|
|
content,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rendert die Privacy Policy im gewuenschten Format
|
|
*/
|
|
function renderPrivacyPolicy(
|
|
sections: PrivacyPolicySection[],
|
|
language: SupportedLanguage,
|
|
format: ExportFormat
|
|
): string {
|
|
switch (format) {
|
|
case 'HTML':
|
|
return renderAsHTML(sections, language)
|
|
case 'MARKDOWN':
|
|
return renderAsMarkdown(sections, language)
|
|
default:
|
|
return renderAsMarkdown(sections, language)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rendert als HTML
|
|
*/
|
|
function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
|
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
|
|
|
const sectionsHTML = sections
|
|
.map((section) => {
|
|
const content = t(section.content, language)
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/### (.+)/g, '<h3>$1</h3>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
|
|
|
return `
|
|
<section id="${section.id}">
|
|
<h2>${t(section.title, language)}</h2>
|
|
<p>${content}</p>
|
|
</section>
|
|
`
|
|
})
|
|
.join('\n')
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="${language}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
color: #333;
|
|
}
|
|
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
|
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
|
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
|
p { margin: 1rem 0; }
|
|
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
|
li { margin: 0.5rem 0; }
|
|
strong { font-weight: 600; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${title}</h1>
|
|
${sectionsHTML}
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
/**
|
|
* Rendert als Markdown
|
|
*/
|
|
function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
|
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
|
|
|
const sectionsMarkdown = sections
|
|
.map((section) => {
|
|
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
|
})
|
|
.join('\n\n---\n\n')
|
|
|
|
return `# ${title}\n\n${sectionsMarkdown}`
|
|
}
|
|
|
|
// =============================================================================
|
|
// EXPORTS
|
|
// =============================================================================
|
|
|
|
export {
|
|
generateControllerSection,
|
|
generateDataCollectionSection,
|
|
generatePurposesSection,
|
|
generateLegalBasisSection,
|
|
generateRecipientsSection,
|
|
generateRetentionSection,
|
|
generateSpecialCategoriesSection,
|
|
generateRightsSection,
|
|
generateCookiesSection,
|
|
generateChangesSection,
|
|
renderAsHTML,
|
|
renderAsMarkdown,
|
|
}
|