feat(sprint-6): Phase 2 — DSGVO consent management
Deploy to Production / test (push) Has been cancelled
Deploy to Production / deploy (push) Has been cancelled

- V6 migration: consents table with audit columns
- Consent entity, repository, service (grant/revoke/check)
- ConsentController: GET/POST/DELETE consent endpoints
- DSGVO export (Art. 15): full personal data JSON download
- DSGVO deletion (Art. 17): anonymization + account deactivation
- Frontend: consent banner (modal, cannot dismiss), privacy settings page
- React Query hooks for consent + DSGVO operations
- Full i18n (de/en) for consent and DSGVO namespaces
This commit is contained in:
Patrick Plate
2026-06-12 22:22:48 +02:00
parent b38902a7ee
commit 3232d2f7fd
17 changed files with 2227 additions and 0 deletions
@@ -0,0 +1,100 @@
"use client"
import { useState } from "react"
import {
useConsentCheckQuery,
useGrantConsentMutation,
} from "@/services/consent"
import { useTranslations } from "next-intl"
import { CheckCircle, Shield } from "lucide-react"
/**
* DSGVO Consent Banner — fullscreen modal overlay.
* Shows when a logged-in user has NOT yet granted DATA_PROCESSING consent.
* Cannot be dismissed without action.
*/
export function ConsentBanner() {
const t = useTranslations("consent")
const [marketingChecked, setMarketingChecked] = useState(false)
const { data: consentCheck, isLoading } = useConsentCheckQuery()
const grantMutation = useGrantConsentMutation()
// Don't show if still loading or consent already granted
if (isLoading || consentCheck?.hasDataProcessingConsent) {
return null
}
const handleAccept = async () => {
// Grant DATA_PROCESSING consent (required)
await grantMutation.mutateAsync({ type: "DATA_PROCESSING", version: 1 })
// Grant MARKETING if checked
if (marketingChecked) {
await grantMutation.mutateAsync({ type: "MARKETING", version: 1 })
}
}
const handleReject = () => {
// Redirect to deletion confirmation
window.location.href = "/settings/privacy?action=delete"
}
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm">
<div className="mx-4 max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-xl bg-card p-6 shadow-2xl">
<div className="mb-4 flex items-center gap-3">
<Shield className="h-8 w-8 text-primary" />
<h2 className="text-xl font-semibold">{t("title")}</h2>
</div>
{/* Required: Data Processing */}
<div className="mb-4 rounded-lg border border-primary/20 bg-primary/5 p-4">
<h3 className="mb-1 font-medium">{t("dataProcessing")}</h3>
<p className="text-sm text-muted-foreground">
{t("dataProcessingDesc")}
</p>
<p className="mt-2 text-xs font-medium text-primary">
{t("required")}
</p>
</div>
{/* Optional: Marketing */}
<div className="mb-6 rounded-lg border p-4">
<label className="flex cursor-pointer items-start gap-3">
<input
type="checkbox"
checked={marketingChecked}
onChange={(e) => setMarketingChecked(e.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-300"
/>
<div>
<h3 className="font-medium">{t("marketing")}</h3>
<p className="text-sm text-muted-foreground">
{t("marketingDesc")}
</p>
</div>
</label>
</div>
{/* Actions */}
<div className="flex flex-col gap-3">
<button
onClick={handleAccept}
disabled={grantMutation.isPending}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
>
<CheckCircle className="h-4 w-4" />
{grantMutation.isPending ? "..." : t("accept")}
</button>
<button
onClick={handleReject}
className="w-full text-center text-sm text-destructive underline transition-colors hover:text-destructive/80"
>
{t("reject")}
</button>
</div>
</div>
</div>
)
}