feat(sprint-6): Phase 2 — DSGVO consent management
- 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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user