Initial commit: breakpilot-compliance - Compliance SDK Platform

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>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,464 @@
/**
* <breakpilot-dsr-portal> Web Component
*
* Data Subject Request Portal for GDPR rights
*
* Usage:
* <breakpilot-dsr-portal
* api-key="pk_live_xxx"
* language="de">
* </breakpilot-dsr-portal>
*/
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
import { BreakPilotElement, COMMON_STYLES } from './base'
const TRANSLATIONS = {
de: {
title: 'Betroffenenrechte-Portal',
subtitle:
'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.',
requestType: 'Art der Anfrage',
name: 'Ihr Name',
namePlaceholder: 'Max Mustermann',
email: 'E-Mail-Adresse',
emailPlaceholder: 'max@example.com',
additionalInfo: 'Zusätzliche Informationen (optional)',
additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...',
submit: 'Anfrage einreichen',
submitting: 'Wird gesendet...',
successTitle: 'Anfrage eingereicht',
successMessage:
'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an',
disclaimer:
'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.',
types: {
ACCESS: {
name: 'Auskunft (Art. 15)',
description: 'Welche Daten haben Sie über mich gespeichert?',
},
RECTIFICATION: {
name: 'Berichtigung (Art. 16)',
description: 'Korrigieren Sie falsche Daten über mich.',
},
ERASURE: {
name: 'Löschung (Art. 17)',
description: 'Löschen Sie alle meine personenbezogenen Daten.',
},
PORTABILITY: {
name: 'Datenübertragbarkeit (Art. 20)',
description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.',
},
RESTRICTION: {
name: 'Einschränkung (Art. 18)',
description: 'Schränken Sie die Verarbeitung meiner Daten ein.',
},
OBJECTION: {
name: 'Widerspruch (Art. 21)',
description: 'Ich widerspreche der Verarbeitung meiner Daten.',
},
},
},
en: {
title: 'Data Subject Rights Portal',
subtitle:
'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.',
requestType: 'Request Type',
name: 'Your Name',
namePlaceholder: 'John Doe',
email: 'Email Address',
emailPlaceholder: 'john@example.com',
additionalInfo: 'Additional Information (optional)',
additionalInfoPlaceholder: 'Any additional details about your request...',
submit: 'Submit Request',
submitting: 'Submitting...',
successTitle: 'Request Submitted',
successMessage:
'We will process your request within 30 days. You will receive a confirmation email at',
disclaimer:
'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.',
types: {
ACCESS: {
name: 'Access (Art. 15)',
description: 'What data do you have about me?',
},
RECTIFICATION: {
name: 'Rectification (Art. 16)',
description: 'Correct inaccurate data about me.',
},
ERASURE: {
name: 'Erasure (Art. 17)',
description: 'Delete all my personal data.',
},
PORTABILITY: {
name: 'Data Portability (Art. 20)',
description: 'Export my data in a machine-readable format.',
},
RESTRICTION: {
name: 'Restriction (Art. 18)',
description: 'Restrict the processing of my data.',
},
OBJECTION: {
name: 'Objection (Art. 21)',
description: 'I object to the processing of my data.',
},
},
},
}
export class DSRPortalElement extends BreakPilotElement {
static get observedAttributes(): string[] {
return ['api-key', 'api-endpoint', 'tenant-id', 'language']
}
private selectedType: DSRRequestType | null = null
private name = ''
private email = ''
private additionalInfo = ''
private isSubmitting = false
private isSubmitted = false
private error: string | null = null
private get language(): 'de' | 'en' {
return (this.getAttribute('language') as 'de' | 'en') || 'de'
}
private get t() {
return TRANSLATIONS[this.language]
}
private handleTypeSelect = (type: DSRRequestType): void => {
this.selectedType = type
this.render()
}
private handleSubmit = async (e: Event): Promise<void> => {
e.preventDefault()
if (!this.selectedType || !this.email || !this.name) {
return
}
this.isSubmitting = true
this.error = null
this.render()
try {
// In a real implementation, this would use the client
// For now, we simulate a successful submission
await new Promise(resolve => setTimeout(resolve, 1000))
this.emit('dsr-submitted', {
type: this.selectedType,
email: this.email,
name: this.name,
additionalInfo: this.additionalInfo,
})
this.isSubmitted = true
} catch (err) {
this.error = err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten'
} finally {
this.isSubmitting = false
this.render()
}
}
protected render(): void {
const styles = `
${COMMON_STYLES}
:host {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.portal {
background: #fff;
}
.title {
margin: 0 0 10px;
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
}
.subtitle {
margin: 0 0 20px;
color: #666;
}
.form-group {
margin-bottom: 20px;
}
.label {
display: block;
font-weight: 500;
margin-bottom: 10px;
}
.type-options {
display: grid;
gap: 10px;
}
.type-option {
display: flex;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
background: #fff;
transition: all 0.2s;
}
.type-option:hover {
border-color: #999;
}
.type-option.selected {
border-color: #1a1a1a;
background: #f5f5f5;
}
.type-option input {
margin-right: 15px;
}
.type-name {
font-weight: 500;
}
.type-description {
font-size: 13px;
color: #666;
}
.input {
width: 100%;
padding: 12px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.input:focus {
outline: none;
border-color: #1a1a1a;
}
.textarea {
resize: vertical;
min-height: 100px;
}
.error {
padding: 12px;
background: #fef2f2;
color: #dc2626;
border-radius: 4px;
margin-bottom: 15px;
}
.btn-submit {
width: 100%;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
color: #fff;
background: #1a1a1a;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.disclaimer {
margin-top: 20px;
font-size: 12px;
color: #666;
}
.success {
text-align: center;
padding: 40px 20px;
background: #f0fdf4;
border-radius: 8px;
}
.success-icon {
font-size: 48px;
margin-bottom: 20px;
}
.success-title {
margin: 0 0 10px;
color: #166534;
}
.success-message {
margin: 0;
color: #166534;
}
`
if (this.isSubmitted) {
this.renderSuccess(styles)
} else {
this.renderForm(styles)
}
}
private renderForm(styles: string): void {
const t = this.t
const types: DSRRequestType[] = [
'ACCESS',
'RECTIFICATION',
'ERASURE',
'PORTABILITY',
'RESTRICTION',
'OBJECTION',
]
const typesHtml = types
.map(
type => `
<label class="type-option ${this.selectedType === type ? 'selected' : ''}">
<input
type="radio"
name="dsrType"
value="${type}"
${this.selectedType === type ? 'checked' : ''}
/>
<div>
<div class="type-name">${t.types[type].name}</div>
<div class="type-description">${t.types[type].description}</div>
</div>
</label>
`
)
.join('')
const isValid = this.selectedType && this.email && this.name
const isDisabled = !isValid || this.isSubmitting
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="portal">
<h2 class="title">${t.title}</h2>
<p class="subtitle">${t.subtitle}</p>
<form id="dsr-form">
<div class="form-group">
<label class="label">${t.requestType} *</label>
<div class="type-options">
${typesHtml}
</div>
</div>
<div class="form-group">
<label class="label">${t.name} *</label>
<input
type="text"
class="input"
id="name-input"
value="${this.name}"
placeholder="${t.namePlaceholder}"
required
/>
</div>
<div class="form-group">
<label class="label">${t.email} *</label>
<input
type="email"
class="input"
id="email-input"
value="${this.email}"
placeholder="${t.emailPlaceholder}"
required
/>
</div>
<div class="form-group">
<label class="label">${t.additionalInfo}</label>
<textarea
class="input textarea"
id="info-input"
placeholder="${t.additionalInfoPlaceholder}"
rows="4"
>${this.additionalInfo}</textarea>
</div>
${this.error ? `<div class="error">${this.error}</div>` : ''}
<button
type="submit"
class="btn-submit"
${isDisabled ? 'disabled' : ''}
>
${this.isSubmitting ? t.submitting : t.submit}
</button>
</form>
<p class="disclaimer">${t.disclaimer}</p>
</div>
`
// Bind events
const form = this.shadow.getElementById('dsr-form') as HTMLFormElement
form.onsubmit = this.handleSubmit
const nameInput = this.shadow.getElementById('name-input') as HTMLInputElement
nameInput.oninput = e => {
this.name = (e.target as HTMLInputElement).value
}
const emailInput = this.shadow.getElementById('email-input') as HTMLInputElement
emailInput.oninput = e => {
this.email = (e.target as HTMLInputElement).value
}
const infoInput = this.shadow.getElementById('info-input') as HTMLTextAreaElement
infoInput.oninput = e => {
this.additionalInfo = (e.target as HTMLTextAreaElement).value
}
// Bind radio buttons
this.shadow.querySelectorAll<HTMLInputElement>('input[name="dsrType"]').forEach(radio => {
radio.onchange = () => {
this.handleTypeSelect(radio.value as DSRRequestType)
}
})
}
private renderSuccess(styles: string): void {
const t = this.t
this.shadow.innerHTML = `
<style>${styles}</style>
<div class="portal">
<div class="success">
<div class="success-icon">✓</div>
<h2 class="success-title">${t.successTitle}</h2>
<p class="success-message">
${t.successMessage} ${this.email}.
</p>
</div>
</div>
`
}
}
// Register the custom element
if (typeof customElements !== 'undefined') {
customElements.define('breakpilot-dsr-portal', DSRPortalElement)
}