feat(cmp): vendor-agnostic consent data model — 13 new fields
Build + Deploy / build-admin-compliance (push) Successful in 2m28s
Build + Deploy / build-backend-compliance (push) Successful in 3m48s
Build + Deploy / build-ai-sdk (push) Failing after 45s
Build + Deploy / build-developer-portal (push) Successful in 1m28s
Build + Deploy / build-tts (push) Successful in 1m48s
Build + Deploy / build-document-crawler (push) Successful in 48s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-dsms-node (push) Successful in 20s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 24s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m1s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 49s
CI / test-python-backend (push) Successful in 45s
CI / test-python-document-crawler (push) Successful in 31s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 18s
Build + Deploy / build-admin-compliance (push) Successful in 2m28s
Build + Deploy / build-backend-compliance (push) Successful in 3m48s
Build + Deploy / build-ai-sdk (push) Failing after 45s
Build + Deploy / build-developer-portal (push) Successful in 1m28s
Build + Deploy / build-tts (push) Successful in 1m48s
Build + Deploy / build-document-crawler (push) Successful in 48s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-dsms-node (push) Successful in 20s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 24s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m1s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 49s
CI / test-python-backend (push) Successful in 45s
CI / test-python-document-crawler (push) Successful in 31s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 18s
Extend banner consent records with consent_method, banner_version, banner_config_hash, geo, page_url, referrer, device info, session_id and consent_scope for full Art. 7 DSGVO proof with any tracking vendor. Migration 107, backward-compatible (all fields nullable). Admin detail modal shows tracking context, device info and technical data. Fix pre-existing str|None → Optional[str] for Python 3.9 compat. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,18 @@ const categoryColors: Record<string, string> = {
|
|||||||
marketing: 'bg-pink-100 text-pink-700',
|
marketing: 'bg-pink-100 text-pink-700',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const methodLabels: Record<string, string> = {
|
||||||
|
accept_all: 'Alle akzeptiert',
|
||||||
|
reject_all: 'Nur notwendige',
|
||||||
|
custom_selection: 'Individuelle Auswahl',
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodColors: Record<string, string> = {
|
||||||
|
accept_all: 'bg-green-100 text-green-700',
|
||||||
|
reject_all: 'bg-red-100 text-red-700',
|
||||||
|
custom_selection: 'bg-yellow-100 text-yellow-700',
|
||||||
|
}
|
||||||
|
|
||||||
export default function BannerConsentsTab() {
|
export default function BannerConsentsTab() {
|
||||||
const {
|
const {
|
||||||
records, sites, selectedSite, changeSite,
|
records, sites, selectedSite, changeSite,
|
||||||
@@ -76,7 +88,7 @@ export default function BannerConsentsTab() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="text-left px-4 py-3 font-medium text-gray-500">Device</th>
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Device</th>
|
||||||
<th className="text-left px-4 py-3 font-medium text-gray-500">Kategorien</th>
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Kategorien</th>
|
||||||
<th className="text-left px-4 py-3 font-medium text-gray-500">Verknüpft mit</th>
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Methode</th>
|
||||||
<th className="text-left px-4 py-3 font-medium text-gray-500">Erteilt am</th>
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Erteilt am</th>
|
||||||
<th className="text-left px-4 py-3 font-medium text-gray-500">Ablauf</th>
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Ablauf</th>
|
||||||
<th className="text-left px-4 py-3 font-medium text-gray-500">Browser</th>
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Browser</th>
|
||||||
@@ -102,10 +114,11 @@ export default function BannerConsentsTab() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-xs">
|
<td className="px-4 py-3 text-xs">
|
||||||
{record.linked_email
|
{record.consent_method ? (
|
||||||
? <span className="text-purple-600">{record.linked_email}</span>
|
<span className={`px-2 py-0.5 rounded-full ${methodColors[record.consent_method] || 'bg-gray-100 text-gray-600'}`}>
|
||||||
: <span className="text-gray-400">— (anonym)</span>
|
{methodLabels[record.consent_method] || record.consent_method}
|
||||||
}
|
</span>
|
||||||
|
) : <span className="text-gray-400">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-xs text-gray-600">{formatDate(record.created_at)}</td>
|
<td className="px-4 py-3 text-xs text-gray-600">{formatDate(record.created_at)}</td>
|
||||||
<td className="px-4 py-3 text-xs text-gray-600">{formatDate(record.expires_at)}</td>
|
<td className="px-4 py-3 text-xs text-gray-600">{formatDate(record.expires_at)}</td>
|
||||||
@@ -171,6 +184,14 @@ export default function BannerConsentsTab() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">Methode</span>
|
||||||
|
<span>{detail.consent_method ? (
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full ${methodColors[detail.consent_method] || 'bg-gray-100'}`}>
|
||||||
|
{methodLabels[detail.consent_method] || detail.consent_method}
|
||||||
|
</span>
|
||||||
|
) : '—'}</span>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-500">Verknüpft mit</span>
|
<span className="text-gray-500">Verknüpft mit</span>
|
||||||
<span>{detail.linked_email || '— (anonym)'}</span>
|
<span>{detail.linked_email || '— (anonym)'}</span>
|
||||||
@@ -178,16 +199,40 @@ export default function BannerConsentsTab() {
|
|||||||
<div className="flex justify-between"><span className="text-gray-500">Erteilt</span><span>{formatDate(detail.created_at)}</span></div>
|
<div className="flex justify-between"><span className="text-gray-500">Erteilt</span><span>{formatDate(detail.created_at)}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-gray-500">Ablauf</span><span>{formatDate(detail.expires_at)}</span></div>
|
<div className="flex justify-between"><span className="text-gray-500">Ablauf</span><span>{formatDate(detail.expires_at)}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-gray-500">Aktualisiert</span><span>{formatDate(detail.updated_at)}</span></div>
|
<div className="flex justify-between"><span className="text-gray-500">Aktualisiert</span><span>{formatDate(detail.updated_at)}</span></div>
|
||||||
<div className="border-t border-gray-100 pt-3">
|
<div className="flex justify-between"><span className="text-gray-500">Geltungsbereich</span><span>{detail.consent_scope || '—'}</span></div>
|
||||||
<span className="text-gray-500 text-xs">User-Agent</span>
|
{detail.banner_version && (
|
||||||
<p className="text-xs text-gray-600 mt-1 font-mono break-all">{detail.user_agent || '—'}</p>
|
<div className="flex justify-between"><span className="text-gray-500">Banner-Version</span><span>{detail.banner_version}</span></div>
|
||||||
</div>
|
|
||||||
{detail.ip_hash && (
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-500 text-xs">IP-Hash</span>
|
|
||||||
<p className="text-xs text-gray-600 mt-1 font-mono">{detail.ip_hash}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Tracking-Kontext */}
|
||||||
|
<div className="border-t border-gray-100 pt-3">
|
||||||
|
<p className="text-xs font-semibold text-gray-700 mb-2">Tracking-Kontext</p>
|
||||||
|
{detail.page_url && <div className="flex justify-between"><span className="text-gray-500 text-xs">Seite</span><span className="text-xs text-gray-600 truncate max-w-[250px]">{detail.page_url}</span></div>}
|
||||||
|
{detail.referrer && <div className="flex justify-between mt-1"><span className="text-gray-500 text-xs">Referrer</span><span className="text-xs text-gray-600 truncate max-w-[250px]">{detail.referrer}</span></div>}
|
||||||
|
{detail.geo_country && <div className="flex justify-between mt-1"><span className="text-gray-500 text-xs">Land</span><span className="text-xs text-gray-600">{detail.geo_country}{detail.geo_region ? ` / ${detail.geo_region}` : ''}</span></div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Device-Informationen */}
|
||||||
|
<div className="border-t border-gray-100 pt-3">
|
||||||
|
<p className="text-xs font-semibold text-gray-700 mb-2">Device</p>
|
||||||
|
<div className="grid grid-cols-2 gap-1 text-xs">
|
||||||
|
<span className="text-gray-500">Typ</span><span className="text-gray-600">{detail.device_type || '—'}</span>
|
||||||
|
<span className="text-gray-500">Browser</span><span className="text-gray-600">{detail.browser || shortenUA(detail.user_agent)}</span>
|
||||||
|
<span className="text-gray-500">OS</span><span className="text-gray-600">{detail.os || '—'}</span>
|
||||||
|
<span className="text-gray-500">Auflösung</span><span className="text-gray-600">{detail.screen_resolution || '—'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technische Details */}
|
||||||
|
<div className="border-t border-gray-100 pt-3">
|
||||||
|
<p className="text-xs font-semibold text-gray-700 mb-2">Technisch</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div><span className="text-gray-500 text-xs">User-Agent</span><p className="text-xs text-gray-600 font-mono break-all">{detail.user_agent || '—'}</p></div>
|
||||||
|
{detail.ip_hash && <div><span className="text-gray-500 text-xs">IP-Hash</span><p className="text-xs text-gray-600 font-mono">{detail.ip_hash}</p></div>}
|
||||||
|
{detail.session_id && <div><span className="text-gray-500 text-xs">Session</span><p className="text-xs text-gray-600 font-mono">{detail.session_id}</p></div>}
|
||||||
|
{detail.banner_config_hash && <div><span className="text-gray-500 text-xs">Config-Hash</span><p className="text-xs text-gray-600 font-mono">{detail.banner_config_hash}</p></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,6 +112,20 @@ export interface BannerConsentRecord {
|
|||||||
user_agent: string | null
|
user_agent: string | null
|
||||||
linked_email: string | null
|
linked_email: string | null
|
||||||
consent_string: string | null
|
consent_string: string | null
|
||||||
|
// Vendor-agnostische Felder (Migration 107)
|
||||||
|
consent_method: string | null
|
||||||
|
banner_version: number | null
|
||||||
|
banner_config_hash: string | null
|
||||||
|
geo_country: string | null
|
||||||
|
geo_region: string | null
|
||||||
|
consent_scope: string | null
|
||||||
|
page_url: string | null
|
||||||
|
referrer: string | null
|
||||||
|
device_type: string | null
|
||||||
|
browser: string | null
|
||||||
|
os: string | null
|
||||||
|
screen_resolution: string | null
|
||||||
|
session_id: string | null
|
||||||
expires_at: string | null
|
expires_at: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ async def record_consent(
|
|||||||
ip_address=body.ip_address,
|
ip_address=body.ip_address,
|
||||||
user_agent=body.user_agent,
|
user_agent=body.user_agent,
|
||||||
consent_string=body.consent_string,
|
consent_string=body.consent_string,
|
||||||
|
consent_method=body.consent_method,
|
||||||
|
page_url=body.page_url,
|
||||||
|
referrer=body.referrer,
|
||||||
|
device_type=body.device_type,
|
||||||
|
browser=body.browser,
|
||||||
|
os=body.os,
|
||||||
|
screen_resolution=body.screen_resolution,
|
||||||
|
session_id=body.session_id,
|
||||||
|
consent_scope=body.consent_scope,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -206,7 +215,7 @@ async def get_site_stats(
|
|||||||
|
|
||||||
@router.get("/admin/consents")
|
@router.get("/admin/consents")
|
||||||
async def list_banner_consents(
|
async def list_banner_consents(
|
||||||
site_id: str | None = None,
|
site_id: Optional[str] = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
tenant_id: str = Depends(_get_tenant),
|
tenant_id: str = Depends(_get_tenant),
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ class BannerConsentDB(Base):
|
|||||||
user_agent = Column(Text)
|
user_agent = Column(Text)
|
||||||
consent_string = Column(Text)
|
consent_string = Column(Text)
|
||||||
linked_email = Column(Text)
|
linked_email = Column(Text)
|
||||||
|
# Vendor-agnostische Felder (Migration 107)
|
||||||
|
consent_method = Column(Text) # accept_all / reject_all / custom_selection
|
||||||
|
banner_version = Column(Integer)
|
||||||
|
banner_config_hash = Column(Text)
|
||||||
|
geo_country = Column(Text)
|
||||||
|
geo_region = Column(Text)
|
||||||
|
consent_scope = Column(Text, default='domain')
|
||||||
|
page_url = Column(Text)
|
||||||
|
referrer = Column(Text)
|
||||||
|
device_type = Column(Text) # mobile / desktop / tablet
|
||||||
|
browser = Column(Text)
|
||||||
|
os = Column(Text)
|
||||||
|
screen_resolution = Column(Text)
|
||||||
|
session_id = Column(Text)
|
||||||
expires_at = Column(DateTime)
|
expires_at = Column(DateTime)
|
||||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
@@ -63,6 +77,8 @@ class BannerConsentAuditLogDB(Base):
|
|||||||
ip_hash = Column(Text)
|
ip_hash = Column(Text)
|
||||||
banner_config_hash = Column(Text)
|
banner_config_hash = Column(Text)
|
||||||
consent_version = Column(Integer)
|
consent_version = Column(Integer)
|
||||||
|
consent_method = Column(Text)
|
||||||
|
page_url = Column(Text)
|
||||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ class ConsentCreate(BaseModel):
|
|||||||
ip_address: Optional[str] = None
|
ip_address: Optional[str] = None
|
||||||
user_agent: Optional[str] = None
|
user_agent: Optional[str] = None
|
||||||
consent_string: Optional[str] = None
|
consent_string: Optional[str] = None
|
||||||
|
# Vendor-agnostische Felder (Migration 107)
|
||||||
|
consent_method: Optional[str] = None
|
||||||
|
page_url: Optional[str] = None
|
||||||
|
referrer: Optional[str] = None
|
||||||
|
device_type: Optional[str] = None
|
||||||
|
browser: Optional[str] = None
|
||||||
|
os: Optional[str] = None
|
||||||
|
screen_resolution: Optional[str] = None
|
||||||
|
session_id: Optional[str] = None
|
||||||
|
consent_scope: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class SiteConfigCreate(BaseModel):
|
class SiteConfigCreate(BaseModel):
|
||||||
|
|||||||
@@ -24,8 +24,22 @@ def consent_to_dict(c: BannerConsentDB) -> dict[str, Any]:
|
|||||||
"categories": c.categories or [],
|
"categories": c.categories or [],
|
||||||
"vendors": c.vendors or [],
|
"vendors": c.vendors or [],
|
||||||
"ip_hash": c.ip_hash,
|
"ip_hash": c.ip_hash,
|
||||||
|
"user_agent": c.user_agent,
|
||||||
"consent_string": c.consent_string,
|
"consent_string": c.consent_string,
|
||||||
"linked_email": c.linked_email,
|
"linked_email": c.linked_email,
|
||||||
|
"consent_method": c.consent_method,
|
||||||
|
"banner_version": c.banner_version,
|
||||||
|
"banner_config_hash": c.banner_config_hash,
|
||||||
|
"geo_country": c.geo_country,
|
||||||
|
"geo_region": c.geo_region,
|
||||||
|
"consent_scope": c.consent_scope,
|
||||||
|
"page_url": c.page_url,
|
||||||
|
"referrer": c.referrer,
|
||||||
|
"device_type": c.device_type,
|
||||||
|
"browser": c.browser,
|
||||||
|
"os": c.os,
|
||||||
|
"screen_resolution": c.screen_resolution,
|
||||||
|
"session_id": c.session_id,
|
||||||
"expires_at": c.expires_at.isoformat() if c.expires_at else None,
|
"expires_at": c.expires_at.isoformat() if c.expires_at else None,
|
||||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||||
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ class BannerConsentService:
|
|||||||
ip_hash: Optional[str] = None,
|
ip_hash: Optional[str] = None,
|
||||||
banner_config_hash: Optional[str] = None,
|
banner_config_hash: Optional[str] = None,
|
||||||
consent_version: Optional[int] = None,
|
consent_version: Optional[int] = None,
|
||||||
|
*,
|
||||||
|
consent_method: Optional[str] = None,
|
||||||
|
page_url: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
entry = BannerConsentAuditLogDB(
|
entry = BannerConsentAuditLogDB(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
@@ -84,6 +87,8 @@ class BannerConsentService:
|
|||||||
ip_hash=ip_hash,
|
ip_hash=ip_hash,
|
||||||
banner_config_hash=banner_config_hash,
|
banner_config_hash=banner_config_hash,
|
||||||
consent_version=consent_version,
|
consent_version=consent_version,
|
||||||
|
consent_method=consent_method,
|
||||||
|
page_url=page_url,
|
||||||
)
|
)
|
||||||
self.db.add(entry)
|
self.db.add(entry)
|
||||||
|
|
||||||
@@ -143,6 +148,16 @@ class BannerConsentService:
|
|||||||
ip_address: Optional[str],
|
ip_address: Optional[str],
|
||||||
user_agent: Optional[str],
|
user_agent: Optional[str],
|
||||||
consent_string: Optional[str],
|
consent_string: Optional[str],
|
||||||
|
*,
|
||||||
|
consent_method: Optional[str] = None,
|
||||||
|
page_url: Optional[str] = None,
|
||||||
|
referrer: Optional[str] = None,
|
||||||
|
device_type: Optional[str] = None,
|
||||||
|
browser: Optional[str] = None,
|
||||||
|
os: Optional[str] = None,
|
||||||
|
screen_resolution: Optional[str] = None,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
consent_scope: Optional[str] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Upsert a device consent row for (tenant, site, device_fingerprint).
|
"""Upsert a device consent row for (tenant, site, device_fingerprint).
|
||||||
|
|
||||||
@@ -158,6 +173,21 @@ class BannerConsentService:
|
|||||||
expires_at = now + timedelta(days=retention)
|
expires_at = now + timedelta(days=retention)
|
||||||
config_hash, config_ver = self._compute_config_hash(tid, site_id)
|
config_hash, config_ver = self._compute_config_hash(tid, site_id)
|
||||||
|
|
||||||
|
# Vendor-agnostische Zusatzfelder
|
||||||
|
extra = {
|
||||||
|
"consent_method": consent_method,
|
||||||
|
"banner_version": config_ver,
|
||||||
|
"banner_config_hash": config_hash,
|
||||||
|
"page_url": page_url,
|
||||||
|
"referrer": referrer,
|
||||||
|
"device_type": device_type,
|
||||||
|
"browser": browser,
|
||||||
|
"os": os,
|
||||||
|
"screen_resolution": screen_resolution,
|
||||||
|
"session_id": session_id,
|
||||||
|
"consent_scope": consent_scope or "domain",
|
||||||
|
}
|
||||||
|
|
||||||
existing = (
|
existing = (
|
||||||
self.db.query(BannerConsentDB)
|
self.db.query(BannerConsentDB)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -176,10 +206,13 @@ class BannerConsentService:
|
|||||||
existing.consent_string = consent_string
|
existing.consent_string = consent_string
|
||||||
existing.expires_at = expires_at
|
existing.expires_at = expires_at
|
||||||
existing.updated_at = now
|
existing.updated_at = now
|
||||||
|
for key, val in extra.items():
|
||||||
|
setattr(existing, key, val)
|
||||||
self.db.flush()
|
self.db.flush()
|
||||||
self._log(
|
self._log(
|
||||||
tid, existing.id, "consent_updated", site_id, device_fingerprint,
|
tid, existing.id, "consent_updated", site_id, device_fingerprint,
|
||||||
categories, ip_hash, config_hash, config_ver,
|
categories, ip_hash, config_hash, config_ver,
|
||||||
|
consent_method=consent_method, page_url=page_url,
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.db.refresh(existing)
|
self.db.refresh(existing)
|
||||||
@@ -195,12 +228,14 @@ class BannerConsentService:
|
|||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
consent_string=consent_string,
|
consent_string=consent_string,
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
|
**extra,
|
||||||
)
|
)
|
||||||
self.db.add(consent)
|
self.db.add(consent)
|
||||||
self.db.flush()
|
self.db.flush()
|
||||||
self._log(
|
self._log(
|
||||||
tid, consent.id, "consent_given", site_id, device_fingerprint,
|
tid, consent.id, "consent_given", site_id, device_fingerprint,
|
||||||
categories, ip_hash, config_hash, config_ver,
|
categories, ip_hash, config_hash, config_ver,
|
||||||
|
consent_method=consent_method, page_url=page_url,
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.db.refresh(consent)
|
self.db.refresh(consent)
|
||||||
@@ -371,7 +406,7 @@ class BannerConsentService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def list_consents(
|
def list_consents(
|
||||||
self, tenant_id: str, site_id: str | None = None,
|
self, tenant_id: str, site_id: Optional[str] = None,
|
||||||
limit: int = 50, offset: int = 0,
|
limit: int = 50, offset: int = 0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""List paginated banner consents with parsed categories."""
|
"""List paginated banner consents with parsed categories."""
|
||||||
@@ -406,6 +441,19 @@ class BannerConsentService:
|
|||||||
"user_agent": c.user_agent,
|
"user_agent": c.user_agent,
|
||||||
"linked_email": c.linked_email,
|
"linked_email": c.linked_email,
|
||||||
"consent_string": c.consent_string,
|
"consent_string": c.consent_string,
|
||||||
|
"consent_method": c.consent_method,
|
||||||
|
"banner_version": c.banner_version,
|
||||||
|
"banner_config_hash": c.banner_config_hash,
|
||||||
|
"geo_country": c.geo_country,
|
||||||
|
"geo_region": c.geo_region,
|
||||||
|
"consent_scope": c.consent_scope,
|
||||||
|
"page_url": c.page_url,
|
||||||
|
"referrer": c.referrer,
|
||||||
|
"device_type": c.device_type,
|
||||||
|
"browser": c.browser,
|
||||||
|
"os": c.os,
|
||||||
|
"screen_resolution": c.screen_resolution,
|
||||||
|
"session_id": c.session_id,
|
||||||
"expires_at": c.expires_at.isoformat() if c.expires_at else None,
|
"expires_at": c.expires_at.isoformat() if c.expires_at else None,
|
||||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||||
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
-- Migration 107: Vendor-agnostische Felder fuer Banner-Consents
|
||||||
|
-- Erweitert compliance_banner_consents um Felder die fuer beliebige
|
||||||
|
-- Tracking-Anbieter (GA4, Matomo, HubSpot etc.) benoetigt werden.
|
||||||
|
-- Alle Felder nullable → backward-compatible.
|
||||||
|
|
||||||
|
-- Consent-Methode: wie hat der User entschieden
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS consent_method TEXT;
|
||||||
|
-- "accept_all" / "reject_all" / "custom_selection"
|
||||||
|
|
||||||
|
-- Banner-Version + Config-Hash zum Zeitpunkt des Consents (Art. 7(1) DSGVO)
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS banner_version INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS banner_config_hash TEXT;
|
||||||
|
|
||||||
|
-- Geo-Daten fuer EWR-Only-Logik
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS geo_country TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS geo_region TEXT;
|
||||||
|
|
||||||
|
-- Geltungsbereich: "page" / "domain" / "cross-domain"
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS consent_scope TEXT DEFAULT 'domain';
|
||||||
|
|
||||||
|
-- Tracking-Kontext
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS page_url TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS referrer TEXT;
|
||||||
|
|
||||||
|
-- Device-Informationen (parsed aus User-Agent im Frontend)
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS device_type TEXT;
|
||||||
|
-- "mobile" / "desktop" / "tablet"
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS browser TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS os TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS screen_resolution TEXT;
|
||||||
|
|
||||||
|
-- Session-basierte Zuordnung
|
||||||
|
ALTER TABLE compliance_banner_consents
|
||||||
|
ADD COLUMN IF NOT EXISTS session_id TEXT;
|
||||||
|
|
||||||
|
-- Audit-Log ebenfalls erweitern (consent_method fuer Nachweis)
|
||||||
|
ALTER TABLE compliance_banner_consent_audit_log
|
||||||
|
ADD COLUMN IF NOT EXISTS consent_method TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE compliance_banner_consent_audit_log
|
||||||
|
ADD COLUMN IF NOT EXISTS page_url TEXT;
|
||||||
|
|
||||||
|
-- Index auf consent_method fuer Reporting
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_banner_consent_method
|
||||||
|
ON compliance_banner_consents (consent_method)
|
||||||
|
WHERE consent_method IS NOT NULL;
|
||||||
|
|
||||||
|
-- Index auf session_id fuer Session-Lookup
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_banner_consent_session
|
||||||
|
ON compliance_banner_consents (session_id)
|
||||||
|
WHERE session_id IS NOT NULL;
|
||||||
Reference in New Issue
Block a user