All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m23s
Die öffentliche API gab bei ?admin=true alle Datensätze heraus, auch die nicht veröffentlichten: von 117 Erklärungen waren 112 Entwürfe, die jeder Besucher abrufen konnte. Der Parameter setzt jetzt eine Admin-Sitzung voraus, sonst kommt die öffentliche Liste. Der Einzelabruf behandelt Entwürfe wie nicht vorhanden, damit sich über die fortlaufende ID nicht abklopfen lässt, welche Datensätze existieren. Markdown wurde mit sanitize: false gerendert und per dangerouslySetInnerHTML eingesetzt. Das Standard-Schema lässt jetzt Überschriften, Listen, Tabellen und http(s)/mailto-Links durch und entfernt Skripte, Event-Attribute, eingebettete Rahmen und javascript:-Ziele. Der Titel in der Druckfassung war an drei Stellen unmaskiert. Standardtexte - Zehn Bausteine, die für alle Erklärungen gelten, werden einmal zentral gepflegt. In der Erklärung steht nur eine Abweichung; ein leeres Feld bedeutet "folgt dem Standard". Ein Wechsel in der Amtsleitung wirkt damit sofort auf alle Erklärungen, die nicht bewusst abweichen. - Die Migration übernimmt den häufigsten Bestandswert als Standard und setzt übereinstimmende Felder auf NULL. Von 117 Erklärungen folgen danach 116 dem Standard, eine weicht ab (Online-Verfahren mit ZIT-SH als zusätzlichem Verantwortlichen). Pflichtangaben nach Art. 13/14 DSGVO - Neue Felder für Drittlandübermittlung (Art. 13 Abs. 1 lit. f), automatisierte Entscheidungsfindung (Art. 13 Abs. 2 lit. f), Datenkategorien (Art. 14 Abs. 1 lit. d) und berechtigte Interessen (Art. 13 Abs. 1 lit. d). Auf die ersten beiden muss auch dann hingewiesen werden, wenn es sie nicht gibt, deshalb sind sie Standardtexte mit ausdrücklichem "findet nicht statt". - Der Hinweis auf das Widerspruchsrecht steht nach Art. 21 Abs. 4 in einem eigenen, abgesetzten Kasten vor allen Abschnitten, nicht als Zeile im Fließtext der Betroffenenrechte. - Zweck, Rechtsgrundlage und Speicherdauer sind Pflicht. Geprüft wird beim Veröffentlichen, nicht beim Speichern: Ein Entwurf darf unvollständig sein, eine öffentlich sichtbare Erklärung nicht. So bleiben Bestandsdaten bearbeitbar, ohne dass die Anwendung Angaben erfindet, die nur die Fachabteilung kennt. Die Admin-Übersicht markiert unvollständige Erklärungen und nennt die fehlenden Angaben. Öffentliche Seiten - Jede Erklärung hat eine dauerhafte Adresse /erklaerung/<bezeichner>, serverseitig gerendert, zum Verlinken aus Antragsformularen. Der Bezeichner wird aus dem Titel abgeleitet und bleibt beim Bearbeiten erhalten, solange der Titel gleich bleibt. Entwürfe liefern 404. - Impressum, Datenschutzerklärung des Portals und Erklärung zur Barrierefreiheit werden im Admin als Markdown gepflegt und in der Fußzeile verlinkt. Die Fußzeile rendert serverseitig, weil das Impressum ohne JavaScript erreichbar sein muss. Ein leerer Text nimmt die Seite vom Netz, statt eine leere Seite auszuliefern. - Die Übersicht nutzt echte Links statt Klick-Handler und wird serverseitig vorbefüllt. APP_COMPANY_NAME ersetzt den Namen der Anwendung in Kopf- und Fußzeile, Seitentiteln, Anmeldeseite, Druckfassung und Startmeldung des Containers. Der Wert wird zur Laufzeit gelesen, ein Neubau des Images ist zum Umbenennen nicht nötig. Getestet gegen die 117 Bestandsdatensätze: Migrationen angewendet, der angezeigte Text blieb bei allen 117 x 7 Baustein-Feldern identisch. Alle 117 Bezeichner sind eindeutig und stimmen mit lib/slug.ts überein (SQLites LOWER() kennt nur ASCII, Großumlaute werden deshalb vorher ersetzt). ?admin=true liefert ohne Anmeldung 5 statt 117 Datensätze, Entwürfe 404. Über die API mit Anmeldung geprüft: unvollständig als Entwurf wird angelegt, dieselben Angaben mit isPublic abgelehnt, ebenso fehlende Datenkategorien bei angegebener Herkunft. Ohne APP_COMPANY_NAME erscheint "Syncova Policies", mit gesetztem Wert kein einziges Vorkommen mehr. Die Änderungen liegen in einem Commit, weil Schema, API-Routen und Formulare von allen Teilen berührt werden und eine thematische Aufteilung nicht lauffähige Zwischenstände ergäbe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
194 lines
8.0 KiB
Markdown
194 lines
8.0 KiB
Markdown
# Syncova Policies
|
||
|
||
Verwaltung und Veröffentlichung von Datenschutzerklärungen nach DSGVO (Art. 12ff.).
|
||
Öffentliche Übersicht für Betroffene, geschützter Admin-Bereich zur Pflege.
|
||
|
||
Next.js 15 (App Router) · React 19 · Tailwind CSS 4 · shadcn/ui · Prisma + SQLite · Auth.js
|
||
|
||
---
|
||
|
||
## Schnellstart (lokal)
|
||
|
||
```bash
|
||
npm install
|
||
cp .env.example .env # NEXTAUTH_SECRET eintragen (siehe unten)
|
||
npx prisma migrate deploy
|
||
npm run seed-admin # legt den ersten Admin an und zeigt das Passwort einmalig
|
||
npm run dev
|
||
```
|
||
|
||
Die Anwendung läuft anschließend auf http://localhost:3000, der Admin-Bereich
|
||
unter `/admin`.
|
||
|
||
### Umgebungsvariablen
|
||
|
||
| Variable | Pflicht | Bedeutung |
|
||
|---|---|---|
|
||
| `APP_COMPANY_NAME` | nein | Name, unter dem die Anwendung auftritt – Kopfzeile, Fußzeile, Seitentitel und Druckfassung. Leer lassen für „Syncova Policies“. Wird zur Laufzeit gelesen, ein Neubau des Images ist nicht nötig. |
|
||
| `NEXTAUTH_SECRET` | ja | Signiert die Session-Token. Erzeugen mit `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"` |
|
||
| `NEXTAUTH_URL` | nein | Nur nötig, wenn ein Reverse-Proxy keine `X-Forwarded-*`-Header setzt. Sonst erkennt die Anwendung Host und Port selbst – ein fester Wert erzwingt Weiterleitungen auf genau diesen Port. |
|
||
| `DATABASE_URL` | ja | SQLite-Pfad. Lokal `file:./dev.db`, im Container `file:/app/data/syncova.db` |
|
||
| `ADMIN_EMAIL` | nein | Vorgabe für `seed-admin` / `reset-admin` |
|
||
| `ADMIN_NAME` | nein | Anzeigename des Admin-Kontos |
|
||
| `ADMIN_PASSWORD` | nein | Leer lassen für ein generiertes Zufallspasswort |
|
||
|
||
> `.env` ist absichtlich nicht versioniert – sie enthält das Session-Secret.
|
||
> Ebenso wenig die SQLite-Datei, die Benutzerkonten und Passwort-Hashes enthält.
|
||
|
||
---
|
||
|
||
## Betrieb per Docker
|
||
|
||
```bash
|
||
docker run -d --name syncova-policies \
|
||
-p 3000:3000 \
|
||
-e NEXTAUTH_SECRET="$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))")" \
|
||
-e NEXTAUTH_URL="https://datenschutz.example.de" \
|
||
-v syncova-data:/app/data \
|
||
git.jfritzsche.de/jf/syncova-policies:latest
|
||
```
|
||
|
||
Oder mit Compose:
|
||
|
||
```bash
|
||
export NEXTAUTH_SECRET="…"
|
||
export NEXTAUTH_URL="https://datenschutz.example.de"
|
||
docker compose up -d
|
||
```
|
||
|
||
Beim Start wendet der Container ausstehende Migrationen selbst an
|
||
(`prisma migrate deploy`). Die Datenbank liegt im Volume unter `/app/data` –
|
||
**ohne dieses Volume gehen alle Daten beim Neustart verloren.**
|
||
|
||
Ersten Admin-Zugang anlegen:
|
||
|
||
```bash
|
||
docker exec -it syncova-policies node scripts/seed-admin.js
|
||
```
|
||
|
||
Passwort zurücksetzen:
|
||
|
||
```bash
|
||
docker exec -it syncova-policies node scripts/reset-admin.js --email admin@example.de
|
||
```
|
||
|
||
---
|
||
|
||
## Datenpflege
|
||
|
||
### Standardtexte
|
||
|
||
Sieben Bausteine sind in aller Regel für jede Erklärung gleich und werden
|
||
deshalb einmal zentral gepflegt – im Admin-Bereich unter **Standardtexte**:
|
||
|
||
| Baustein | Artikel | Pflicht |
|
||
|---|---|---|
|
||
| Verantwortlicher im Sinne der DSGVO | Art. 13 Abs. 1 lit. a | ja |
|
||
| Kontaktdaten der/des Datenschutzbeauftragten | Art. 13 Abs. 1 lit. b | ja |
|
||
| Betroffenen-Rechte | Art. 13 Abs. 2 lit. b | ja |
|
||
| Beschwerderecht bei der Aufsichtsbehörde | Art. 13 Abs. 2 lit. d | ja |
|
||
| Widerrufsrecht bei Einwilligung | Art. 13 Abs. 2 lit. c | ja |
|
||
| Widerspruchsrecht | Art. 21 Abs. 4 | ja |
|
||
| Übermittlung in Drittländer | Art. 13 Abs. 1 lit. f | ja |
|
||
| Automatisierte Entscheidungsfindung | Art. 13 Abs. 2 lit. f | ja |
|
||
| Ihre Pflicht zur Bereitstellung der Daten | Art. 13 Abs. 2 lit. e | nein |
|
||
| Folgen, wenn Sie die Daten nicht angeben | Art. 13 Abs. 2 lit. e | nein |
|
||
|
||
Auf Drittlandübermittlung und automatisierte Entscheidungen muss auch dann
|
||
hingewiesen werden, wenn es beides nicht gibt – ein ausdrückliches „findet nicht
|
||
statt“ ist die richtige Antwort, kein leeres Feld. Der Hinweis auf das
|
||
Widerspruchsrecht steht nach Art. 21 Abs. 4 in einem eigenen, abgesetzten
|
||
Kasten, getrennt von den übrigen Informationen.
|
||
|
||
Im Formular einer einzelnen Erklärung erscheinen diese Felder schreibgeschützt
|
||
mit der Markierung **Standard**. Erst „Für diese Erklärung abweichen“ macht
|
||
einen Baustein für genau diesen Datensatz bearbeitbar; „Standard übernehmen“
|
||
verwirft die Abweichung wieder.
|
||
|
||
Technisch steht in der Erklärung nur die Abweichung: Ein leeres Feld bedeutet
|
||
„folgt dem Standard“. Eine Änderung an den Standardtexten – etwa ein Wechsel
|
||
in der Amtsleitung – wirkt damit sofort auf alle Erklärungen, die nicht bewusst
|
||
abweichen. Welche Felder abweichen, liefert die API im Feld `overrides`.
|
||
|
||
### Rechtliche Seiten
|
||
|
||
Impressum, Datenschutzerklärung des Portals und Erklärung zur Barrierefreiheit
|
||
werden im Admin unter **Rechtliche Seiten** als Markdown gepflegt und unter
|
||
`/impressum`, `/datenschutz` und `/barrierefreiheit` ausgeliefert. Sie erscheinen
|
||
in der Fußzeile aller öffentlichen Seiten. Ein leer gelassener Text nimmt die
|
||
betreffende Seite vom Netz – verlinkt wird nur, was auch gepflegt ist. Die
|
||
Eingabefelder enthalten Vorlagen mit den jeweils erforderlichen Angaben.
|
||
|
||
### Erklärungen
|
||
|
||
Jede veröffentlichte Erklärung hat eine eigene, dauerhafte Adresse unter
|
||
`/erklaerung/<bezeichner>` – etwa zum Verlinken aus einem Antragsformular. Der
|
||
Bezeichner wird aus dem Titel abgeleitet und bleibt beim Bearbeiten erhalten,
|
||
solange der Titel unverändert ist. Nicht veröffentlichte Erklärungen sind unter
|
||
ihrer Adresse nicht erreichbar.
|
||
|
||
#### Vollständigkeit
|
||
|
||
Pflichtangaben aus Art. 13/14 DSGVO werden **beim Veröffentlichen** geprüft,
|
||
nicht beim Speichern: Ein Entwurf darf unvollständig sein, eine öffentlich
|
||
sichtbare Erklärung nicht. Bestehende Datensätze lassen sich damit weiter
|
||
bearbeiten, ohne dass die Anwendung Angaben erfindet, die nur die Fachabteilung
|
||
kennt.
|
||
|
||
Zusätzlich zu den Bausteinen oben verlangt die Veröffentlichung:
|
||
|
||
| Angabe | Artikel |
|
||
|---|---|
|
||
| Zweck der Datenerhebung | Art. 13 Abs. 1 lit. c |
|
||
| Rechtsgrundlage | Art. 13 Abs. 1 lit. c |
|
||
| Geplante Empfänger | Art. 13 Abs. 1 lit. e |
|
||
| Speicherdauer oder Kriterien | Art. 13 Abs. 2 lit. a |
|
||
| Kategorien der verarbeiteten Daten | Art. 14 Abs. 1 lit. d – nur, wenn eine Herkunft der Daten angegeben ist, die Daten also nicht bei der betroffenen Person selbst erhoben wurden |
|
||
|
||
Die Übersicht im Admin markiert jede Erklärung mit fehlenden Pflichtangaben und
|
||
nennt sie im Tooltip; die Kopfzeile zählt sie.
|
||
|
||
Datenschutzerklärungen werden normalerweise im Admin-Bereich gepflegt. Für
|
||
Massenimporte gibt es zwei Wege:
|
||
|
||
| Befehl | Zweck |
|
||
|---|---|
|
||
| `npm run import-policy -- data/policies/<datei>.json` | Einzelne Erklärung aus JSON importieren (`--update` überschreibt) |
|
||
| `npm run analyze-pdfs` | PDFs in `data/import/` analysieren, ohne zu schreiben |
|
||
| `npm run import-pdfs` | Alle PDFs aus `data/import/` importieren (`--dry-run`, `--public`) |
|
||
|
||
Der PDF-Import ist auf die Merkblatt-Vorlage des Amtes Leezen zugeschnitten
|
||
(`scripts/lib/pdf-policy.js`). Er vergleicht den Textbaustein-Teil jedes
|
||
Dokuments gegen ein Referenzdokument und meldet jede Abweichung, statt sie
|
||
stillschweigend zu übernehmen. Importierte Datensätze werden am Titel erkannt
|
||
und aktualisiert, nicht dupliziert.
|
||
|
||
Beide Importwege gleichen die sieben Bausteine gegen die Standardtexte ab:
|
||
Was dem Standard entspricht, wird nicht je Erklärung dupliziert, sondern als
|
||
„folgt dem Standard“ gespeichert. Nur echte Abweichungen landen im Datensatz.
|
||
|
||
---
|
||
|
||
## Veröffentlichung des Images
|
||
|
||
`.gitea/workflows/publish-image.yml` baut bei jedem Push auf `main` sowie bei
|
||
`v*`-Tags ein Image und lädt es in die Gitea-Registry.
|
||
|
||
Voraussetzung: im Repository unter **Settings → Actions → Secrets** ein Secret
|
||
`REGISTRY_TOKEN` mit einem Token hinterlegen, das `write:package` darf.
|
||
|
||
---
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
app/ Routen (öffentlich, /erklaerung, /admin, /auth, /api)
|
||
components/ UI-Komponenten; components/ui = shadcn/ui
|
||
hooks/ SWR-Datenzugriff, Theme
|
||
lib/ Prisma/Auth-Helfer, Markdown, Formatierung
|
||
prisma/ Schema und Migrationen
|
||
scripts/ Admin- und Importskripte
|
||
data/import/ Quell-PDFs für den Massenimport
|
||
data/policies/ Aufbereitete Datensätze als JSON
|
||
```
|