Files
anouma/globals/CookieSettings.ts
T
maro 65b6eeac51 Add privacy-friendly analytics and cookie consent, integrated into the CMS
Adds a first-party analytics system with no third-party trackers and no
persistent per-visitor identifiers: events are grouped into sessions via a
daily-rotating pseudonymous hash (IP+UA+host, HMAC-salted, never stored raw).

- New "Analytics" section in the Payload admin, gated by a canViewAnalytics
  permission on Users, following the existing custom-admin-view pattern
  used by the calendar feature
- Sessions, page views, unique visitors, top/entry/exit pages, events,
  device/browser breakdown, date range filter with period comparison
- Raw events and daily aggregate rollups as their own collections, with
  configurable retention and a cron endpoint that rolls up before purging
- Cookie consent banner (necessary/preferences/statistics) with categories
  and copy editable from a new CMS global; a track() helper that is a
  complete no-op without statistics consent, so nothing is ever sent
  before a visitor opts in
- track() calls wired into the booking flow, contact form, and navigation
2026-09-06 21:59:59 +02:00

103 lines
3.5 KiB
TypeScript

import type { GlobalConfig } from "payload";
import { isAdmin } from "@/access";
/**
* Consent-category configuration shown in the public cookie banner
* (components/Analytics.tsx). Read access is intentionally public — the
* unauthenticated frontend needs the labels/descriptions and the
* enabled/disabled flags before a visitor has logged in anywhere.
*
* "necessary" deliberately has no `enabled` field at all, anywhere in this
* schema — there is no toggle to accidentally switch off, so it can never
* be disabled through the admin UI (see requirement: technically required
* functionality must not be disable-able by an admin).
*/
export const CookieSettings: GlobalConfig = {
slug: "cookie-settings",
label: "Cookie-Einstellungen",
admin: {
description: "Kategorien und Texte für den Cookie-Consent-Banner der Website.",
group: "Datenschutz",
},
access: {
read: () => true,
update: isAdmin,
},
fields: [
{
name: "consentVersion",
type: "text",
label: "Consent-Version",
required: true,
defaultValue: "1.0",
admin: {
description:
"Bei Änderung müssen alle Besucher:innen erneut zustimmen — bereits gespeicherte Einwilligungen mit einer älteren Version werden ignoriert und der Banner erscheint erneut.",
},
},
{
type: "group",
name: "necessary",
label: "Notwendig",
admin: { description: "Immer aktiv — technisch erforderlich, kann nicht deaktiviert werden." },
fields: [
{ name: "label", type: "text", label: "Bezeichnung", required: true, defaultValue: "Notwendig" },
{
name: "description",
type: "textarea",
label: "Beschreibung",
required: true,
defaultValue: "Für den Betrieb der Website technisch erforderlich (z. B. Login, Sitzung). Kann nicht deaktiviert werden.",
},
],
},
{
type: "group",
name: "preferences",
label: "Präferenzen",
fields: [
{ name: "label", type: "text", label: "Bezeichnung", required: true, defaultValue: "Präferenzen" },
{
name: "description",
type: "textarea",
label: "Beschreibung",
required: true,
defaultValue: "Speichert Einstellungen wie z. B. bevorzugte Anzeigeoptionen.",
},
{
name: "enabled",
type: "checkbox",
label: "Kategorie aktiv",
defaultValue: true,
admin: { description: "Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten." },
},
],
},
{
type: "group",
name: "statistics",
label: "Statistik",
fields: [
{ name: "label", type: "text", label: "Bezeichnung", required: true, defaultValue: "Statistik" },
{
name: "description",
type: "textarea",
label: "Beschreibung",
required: true,
defaultValue: "Anonymisierte, datensparsame Reichweitenmessung (Seitenaufrufe, Ereignisse) ohne personenbezogene Profile.",
},
{
name: "enabled",
type: "checkbox",
label: "Kategorie aktiv",
defaultValue: true,
admin: {
description:
"Globaler Schalter für das gesamte Analytics-System. Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten und Analytics-Daten werden serverseitig abgelehnt — auch bei einer noch gespeicherten alten Zustimmung.",
},
},
],
},
],
};