fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
965
admin-v2/lib/sdk/einwilligungen/generator/privacy-policy.ts
Normal file
965
admin-v2/lib/sdk/einwilligungen/generator/privacy-policy.ts
Normal file
@@ -0,0 +1,965 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
Reference in New Issue
Block a user