feat: Werkzeugkatalog nennt tatsächliche Verarbeitungsländer statt EU/USA/gemischt-Eimer, plus DPF-Feld
Die USA sind DSGVO-rechtlich bereits ein Drittland wie jedes andere - der alte verarbeitungsort-Wertebereich (EU/USA/gemischt/on-prem) verschleierte das. Ersetzt durch verarbeitungslaender TEXT[] mit echter Weltländerliste im Formular (Mehrfachauswahl, kein JS nötig). Der harte Filter eu_verarbeitung verlangt jetzt, dass ALLE genannten Länder EU/EWR sind. Neues Feld dpf_zertifiziert macht die EU-US Data Privacy Framework-Zertifizierung strukturiert statt nur Fließtext. Öffnet nebenbei den Weg für Katalogeinträge außerhalb EU/USA (z. B. DeepSeek), die vorher am alten Wertebereich scheiterten. Zusätzlich Typografie-Nachbesserung nach Nutzer-Feedback: Formular- Abschnittsüberschriften (<legend>) saßen zu dicht am Kartenrand, weil <legend> das Padding des umschließenden <fieldset> ignoriert - jetzt mit eigenem Padding und größerer Schrift, h2/h3 einheitlich gesetzt.
This commit is contained in:
60
CLAUDE.md
60
CLAUDE.md
@@ -251,7 +251,7 @@ Sperrungen zentraler Einträge (`werkzeug_sperre`) führen, ohne den
|
|||||||
zentralen Katalog zu verändern.
|
zentralen Katalog zu verändern.
|
||||||
|
|
||||||
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
|
**Pflicht:** `letzte_pruefung` und `quelle` sind NOT NULL — jede
|
||||||
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsort, Training-Opt-
|
Zusicherung im Katalog (AVV verfügbar, Verarbeitungsländer, Training-Opt-
|
||||||
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
|
out, Zertifizierungen) muss belegbar sein, sonst haftet Deklarix für
|
||||||
eine Aussage, die nicht nachweisbar ist. Beide Katalogansichten zeigen
|
eine Aussage, die nicht nachweisbar ist. Beide Katalogansichten zeigen
|
||||||
eine dreistufige Ampel zum Prüfalter (`pruefStatus()` in
|
eine dreistufige Ampel zum Prüfalter (`pruefStatus()` in
|
||||||
@@ -295,7 +295,7 @@ scheinbar EU-verarbeitetes Werkzeug die USA doch nicht ausschließt.
|
|||||||
|
|
||||||
Sowohl der zentrale Katalog (`GET /betreiber/werkzeuge`) als auch die
|
Sowohl der zentrale Katalog (`GET /betreiber/werkzeuge`) als auch die
|
||||||
mandantenseitige Sicht (`GET /verwaltung/werkzeuge`) zeigen den Katalog
|
mandantenseitige Sicht (`GET /verwaltung/werkzeuge`) zeigen den Katalog
|
||||||
als Tabelle (Name, Anbieter, Verarbeitungsort, AVV, Training aus,
|
als Tabelle (Name, Anbieter, Verarbeitungsländer, DPF, AVV, Training aus,
|
||||||
Aufbewahrung, Zertifizierungen, Geeignete Zwecke, Zuletzt geprüft) statt
|
Aufbewahrung, Zertifizierungen, Geeignete Zwecke, Zuletzt geprüft) statt
|
||||||
als Liste — vorher waren nur Name/Anbieter/Ort auf einen Blick sichtbar,
|
als Liste — vorher waren nur Name/Anbieter/Ort auf einen Blick sichtbar,
|
||||||
der Rest war erst nach Klick auf den Detaileintrag zu sehen.
|
der Rest war erst nach Klick auf den Detaileintrag zu sehen.
|
||||||
@@ -324,16 +324,33 @@ CLAUDE.md-Leitplanken für personenbezogene/vertrauliche Daten nicht
|
|||||||
geeignet, absichtlich als Negativbeispiel im Katalog, nicht
|
geeignet, absichtlich als Negativbeispiel im Katalog, nicht
|
||||||
weggelassen)**.
|
weggelassen)**.
|
||||||
|
|
||||||
**Bekannte Schema-Lücke (2026-08-31, noch nicht behoben):** Anbieter
|
**Schema-Lücke behoben (2026-08-31, Migration 0016):** der grobe
|
||||||
mit Verarbeitung außerhalb EU/USA (z. B. DeepSeek, China) lassen sich
|
`verarbeitungsort`-Eimer (`EU`/`USA`/`gemischt`/`on-prem`) wurde durch
|
||||||
mit dem aktuellen `verarbeitungsort`-Wertebereich (`EU`/`USA`/
|
`verarbeitungslaender TEXT[]` ersetzt — echte Länder statt Buckets.
|
||||||
`gemischt`/`on-prem`, DB-CHECK-Constraint) nicht korrekt abbilden —
|
Grund: "USA" war schon immer ein Drittland im Sinne der DSGVO (Art. 44
|
||||||
"gemischt" würde fälschlich EU-Berührung suggerieren, "USA" wäre
|
ff.) wie jedes andere auch, das Wort "Drittland" für einen fünften
|
||||||
schlicht falsch. DeepSeek wurde deshalb bewusst NICHT angelegt statt
|
Bucket-Wert wäre also verwirrend gewesen, nicht erhellend. Das Formular
|
||||||
mit einem falschen Wert erzwungen. Vor dem nächsten Katalogeintrag
|
bietet dafür eine echte Weltländerliste (`weltLaenderliste` in
|
||||||
außerhalb EU/USA/on-prem-Hosting muss der Wertebereich erweitert werden
|
`laenderliste.go`, `<select multiple>`, Mehrfachauswahl per Strg/Cmd-
|
||||||
(z. B. um `drittland`) — eigene Migration nötig, Entscheidung noch
|
Klick — kein JavaScript nötig) plus zwei Pseudo-Einträge ("EU-Region
|
||||||
nicht getroffen.
|
(kein einzelnes Land benannt)" für Anbieter, die nur eine Region statt
|
||||||
|
eines Staates zusichern, z. B. Microsofts EU Data Boundary; "On-Premise
|
||||||
|
(selbst gehostet)"). Der harte Filter `eu_verarbeitung`
|
||||||
|
(`internal/rules/evaluate.go`, `alleLaenderInEUEWR`) verlangt jetzt,
|
||||||
|
dass ALLE genannten Länder EU/EWR-Mitgliedstaaten sind — eine leere
|
||||||
|
Liste gilt als nicht erfüllt, nicht als Freifahrtschein. Zusätzlich neues
|
||||||
|
Feld `dpf_zertifiziert` (Checkbox "EU-US Data Privacy Framework
|
||||||
|
zertifiziert"): DPF ist eine schmalere, gerichtlich schon zweimal
|
||||||
|
gekippte Rechtsgrundlage (Safe Harbor, Privacy Shield) für USA-Transfers,
|
||||||
|
bisher nur als Fließtext in "Einschränkungen" erwähnt, jetzt strukturiert
|
||||||
|
und damit künftig filterbar. Alle 19 Katalogeinträge wurden mit den
|
||||||
|
tatsächlichen, bereits recherchierten Ländern nachgepflegt (z. B.
|
||||||
|
Mistral → Frankreich, DeepL → Deutschland, Synthesia → Irland, Claude
|
||||||
|
for Work → USA) statt der pauschalen alten Buckets.
|
||||||
|
|
||||||
|
Das öffnet jetzt auch DeepSeek (Verarbeitung in China) für einen
|
||||||
|
künftigen Katalogeintrag — die Sperre lag ausschließlich am alten
|
||||||
|
Wertebereich, nicht an einer fachlichen Entscheidung.
|
||||||
|
|
||||||
`CurrentKatalogVersion` liefert eine reproduzierbare Kennung des
|
`CurrentKatalogVersion` liefert eine reproduzierbare Kennung des
|
||||||
aktuellen Katalogzustands (Anzahl Einträge + letzte Änderung) — wird in
|
aktuellen Katalogzustands (Anzahl Einträge + letzte Änderung) — wird in
|
||||||
@@ -409,7 +426,7 @@ wenn mindestens einer zutrifft: (1) `gueltig_bis` ist erreicht oder
|
|||||||
liegt innerhalb von 30 Tagen, (2) das zugesagte Werkzeug wurde aus dem
|
liegt innerhalb von 30 Tagen, (2) das zugesagte Werkzeug wurde aus dem
|
||||||
Katalog entfernt, (3) `werkzeugDiff` erkennt eine Abweichung zwischen
|
Katalog entfernt, (3) `werkzeugDiff` erkennt eine Abweichung zwischen
|
||||||
dem eingefrorenen `werkzeug_snapshot` und dem aktuellen Katalogeintrag
|
dem eingefrorenen `werkzeug_snapshot` und dem aktuellen Katalogeintrag
|
||||||
bei AVV-Verfügbarkeit, Training-Standard oder Verarbeitungsort. Keine
|
bei AVV-Verfügbarkeit, Training-Standard oder Verarbeitungsländern. Keine
|
||||||
gefundene Abweichung → die Genehmigung erscheint nicht (kein stiller
|
gefundene Abweichung → die Genehmigung erscheint nicht (kein stiller
|
||||||
Blanko-Eintrag für jede Genehmigung). `store.ListAktiveGenehmigungenForAccount`
|
Blanko-Eintrag für jede Genehmigung). `store.ListAktiveGenehmigungenForAccount`
|
||||||
liefert dafür alle `genehmigt`/`genehmigt_mit_auflagen`-Entscheidungen
|
liefert dafür alle `genehmigt`/`genehmigt_mit_auflagen`-Entscheidungen
|
||||||
@@ -517,7 +534,7 @@ Karte mit Rahmen/Schatten, Ant-Design-Tokens aus `design/enterprise.css`
|
|||||||
(thematische Abschnitte mit Titel, per `border-top` getrennt), darin
|
(thematische Abschnitte mit Titel, per `border-top` getrennt), darin
|
||||||
`.form-grid` (2 Spalten ab 640px für kurze Felder wie Name/Anbieter
|
`.form-grid` (2 Spalten ab 640px für kurze Felder wie Name/Anbieter
|
||||||
nebeneinander, `.form-full` erzwingt volle Breite für lange Felder).
|
nebeneinander, `.form-full` erzwingt volle Breite für lange Felder).
|
||||||
Checkbox-/Radio-Gruppen mit vielen Optionen (Verarbeitungsort, die
|
Checkbox-Gruppen mit überschaubar vielen Optionen (Zusicherungen, die
|
||||||
zwölf Zwecke) sind `.chip-group` — abgerundete Pillen statt einer
|
zwölf Zwecke) sind `.chip-group` — abgerundete Pillen statt einer
|
||||||
langen Untereinander-Liste, ausgewählte Chips werden über
|
langen Untereinander-Liste, ausgewählte Chips werden über
|
||||||
`:has(input:checked)` blau hervorgehoben (kein JavaScript). Ein
|
`:has(input:checked)` blau hervorgehoben (kein JavaScript). Ein
|
||||||
@@ -534,6 +551,21 @@ genutzt wird — ein Mandant hätte beim Löschen eines eigenen Werkzeugs
|
|||||||
403 bekommen. Jetzt `{{.ActionBase}}/{{.ID}}/loeschen`, wie die
|
403 bekommen. Jetzt `{{.ActionBase}}/{{.ID}}/loeschen`, wie die
|
||||||
Formular-`action` selbst.
|
Formular-`action` selbst.
|
||||||
|
|
||||||
|
**Typografie-Nachbesserung (2026-08-31):** Nutzer-Feedback nach dem
|
||||||
|
ersten Formular-Redesign: Überschriften wirkten zu klein und zu dicht
|
||||||
|
am Rand. Ursache für Letzteres: `<legend>` in `<fieldset
|
||||||
|
class="form-section">` positioniert sich per Spec an der Border-Box
|
||||||
|
des Fieldsets, nicht an dessen Padding-Box — ohne eigenes Padding sitzt
|
||||||
|
die Abschnittsüberschrift optisch enger am Kartenrand als der übrige,
|
||||||
|
per `.form-section { padding: 20px }` eingerückte Inhalt. `.form-section
|
||||||
|
> legend` hat jetzt selbst `padding: 0 0 16px 0` und eine größere
|
||||||
|
Schrift (1.0625rem/700 statt 0.9375rem/600). Gleichzeitig `h2`
|
||||||
|
(1.25rem) und `h3` (1.0625rem) erstmals explizit gesetzt (vorher reiner
|
||||||
|
Browser-Default) und `.fragebogen legend` auf dieselbe Gewichtsstufe
|
||||||
|
wie die Formular-Abschnitte angehoben (600/1rem statt 500/0.9375rem) —
|
||||||
|
für eine einheitliche Überschriften-Hierarchie statt Zufallsgrößen je
|
||||||
|
nach UA-Stylesheet.
|
||||||
|
|
||||||
**Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
|
**Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
|
||||||
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein
|
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein
|
||||||
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
|
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
|
||||||
|
|||||||
@@ -186,11 +186,47 @@ func containsString(list []string, v string) bool {
|
|||||||
// statt store.Werkzeug direkt zu verwenden: internal/rules bleibt so
|
// statt store.Werkzeug direkt zu verwenden: internal/rules bleibt so
|
||||||
// unabhängig von internal/store und für sich allein testbar.
|
// unabhängig von internal/store und für sich allein testbar.
|
||||||
type WerkzeugEigenschaften struct {
|
type WerkzeugEigenschaften struct {
|
||||||
AVVVerfuegbar bool
|
AVVVerfuegbar bool
|
||||||
Verarbeitungsort string
|
// Verarbeitungslaender nennt die tatsächlichen Länder statt eines
|
||||||
TrainingOptOut bool
|
// groben "EU"/"USA"-Eimers (siehe CLAUDE.md, Werkzeugkatalog) — die
|
||||||
TrainingStandard bool
|
// eu_verarbeitung-Anforderung gilt nur als erfüllt, wenn ALLE
|
||||||
AufbewahrungTage int
|
// genannten Länder EU/EWR-Mitgliedstaaten sind.
|
||||||
|
Verarbeitungslaender []string
|
||||||
|
TrainingOptOut bool
|
||||||
|
TrainingStandard bool
|
||||||
|
AufbewahrungTage int
|
||||||
|
}
|
||||||
|
|
||||||
|
// euEwrLaender sind die Staaten, für die eine Verarbeitung nicht als
|
||||||
|
// Drittlandtransfer gilt (Art. 44 ff. DSGVO) — die 27 EU-Mitgliedstaaten
|
||||||
|
// plus die drei über den EWR-Vertrag gleichgestellten Staaten. Die USA
|
||||||
|
// gehören bewusst NICHT dazu — sie sind ein Drittland wie jedes andere,
|
||||||
|
// unabhängig vom EU-US Data Privacy Framework (das mildert die
|
||||||
|
// Transfer-Grundlage, macht die USA aber nicht zum EU/EWR-Gebiet).
|
||||||
|
var euEwrLaender = map[string]bool{
|
||||||
|
"Belgien": true, "Bulgarien": true, "Dänemark": true, "Deutschland": true,
|
||||||
|
"Estland": true, "Finnland": true, "Frankreich": true, "Griechenland": true,
|
||||||
|
"Irland": true, "Italien": true, "Kroatien": true, "Lettland": true,
|
||||||
|
"Litauen": true, "Luxemburg": true, "Malta": true, "Niederlande": true,
|
||||||
|
"Österreich": true, "Polen": true, "Portugal": true, "Rumänien": true,
|
||||||
|
"Schweden": true, "Slowakei": true, "Slowenien": true, "Spanien": true,
|
||||||
|
"Tschechien": true, "Ungarn": true, "Zypern": true,
|
||||||
|
"Island": true, "Liechtenstein": true, "Norwegen": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// alleLaenderInEUEWR prüft, ob jedes genannte Land EU/EWR ist. Eine
|
||||||
|
// leere Liste (kein Land benannt) gilt als NICHT erfüllt — "wissen wir
|
||||||
|
// nicht" darf nie stillschweigend als "ist okay" durchgehen.
|
||||||
|
func alleLaenderInEUEWR(laender []string) bool {
|
||||||
|
if len(laender) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, l := range laender {
|
||||||
|
if !euEwrLaender[l] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErfuelltAnforderung prüft, ob ein Werkzeug eine einzelne Anforderung
|
// ErfuelltAnforderung prüft, ob ein Werkzeug eine einzelne Anforderung
|
||||||
@@ -206,7 +242,7 @@ func ErfuelltAnforderung(anforderungID string, w WerkzeugEigenschaften) bool {
|
|||||||
case "avv_erforderlich":
|
case "avv_erforderlich":
|
||||||
return w.AVVVerfuegbar
|
return w.AVVVerfuegbar
|
||||||
case "eu_verarbeitung":
|
case "eu_verarbeitung":
|
||||||
return w.Verarbeitungsort == "EU"
|
return alleLaenderInEUEWR(w.Verarbeitungslaender)
|
||||||
case "kein_training_auf_eingabe":
|
case "kein_training_auf_eingabe":
|
||||||
return w.TrainingStandard
|
return w.TrainingStandard
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -158,6 +158,21 @@ func TestErfuelltAnforderungAVV(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestErfuelltAnforderungEUVerarbeitung(t *testing.T) {
|
||||||
|
if !rules.ErfuelltAnforderung("eu_verarbeitung", rules.WerkzeugEigenschaften{Verarbeitungslaender: []string{"Irland"}}) {
|
||||||
|
t.Error("erwartet: erfuellt bei einem einzelnen EU/EWR-Land")
|
||||||
|
}
|
||||||
|
if rules.ErfuelltAnforderung("eu_verarbeitung", rules.WerkzeugEigenschaften{Verarbeitungslaender: []string{"USA"}}) {
|
||||||
|
t.Error("erwartet: nicht erfuellt bei USA - die USA sind ein Drittland wie jedes andere")
|
||||||
|
}
|
||||||
|
if rules.ErfuelltAnforderung("eu_verarbeitung", rules.WerkzeugEigenschaften{Verarbeitungslaender: []string{"Irland", "USA"}}) {
|
||||||
|
t.Error("erwartet: nicht erfuellt, sobald auch nur ein Land nicht EU/EWR ist")
|
||||||
|
}
|
||||||
|
if rules.ErfuelltAnforderung("eu_verarbeitung", rules.WerkzeugEigenschaften{}) {
|
||||||
|
t.Error("erwartet: nicht erfuellt ohne benanntes Land - 'wissen wir nicht' darf nicht als 'ist okay' durchgehen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestErfuelltAnforderungUnbekannteAnforderungIstUnkritisch(t *testing.T) {
|
func TestErfuelltAnforderungUnbekannteAnforderungIstUnkritisch(t *testing.T) {
|
||||||
if !rules.ErfuelltAnforderung("menschliche_aufsicht", rules.WerkzeugEigenschaften{}) {
|
if !rules.ErfuelltAnforderung("menschliche_aufsicht", rules.WerkzeugEigenschaften{}) {
|
||||||
t.Error("Prozess-Anforderungen duerfen ein Werkzeug nicht hart aussortieren")
|
t.Error("Prozess-Anforderungen duerfen ein Werkzeug nicht hart aussortieren")
|
||||||
@@ -166,8 +181,8 @@ func TestErfuelltAnforderungUnbekannteAnforderungIstUnkritisch(t *testing.T) {
|
|||||||
|
|
||||||
func TestFilterWerkzeugeSortsOutMissingAVV(t *testing.T) {
|
func TestFilterWerkzeugeSortsOutMissingAVV(t *testing.T) {
|
||||||
kandidaten := []rules.WerkzeugKandidat{
|
kandidaten := []rules.WerkzeugKandidat{
|
||||||
{ID: "mit-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: true, Verarbeitungsort: "EU", TrainingStandard: true}},
|
{ID: "mit-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: true, Verarbeitungslaender: []string{"Deutschland"}, TrainingStandard: true}},
|
||||||
{ID: "ohne-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: false, Verarbeitungsort: "EU", TrainingStandard: true}},
|
{ID: "ohne-avv", Eigenschaften: rules.WerkzeugEigenschaften{AVVVerfuegbar: false, Verarbeitungslaender: []string{"Deutschland"}, TrainingStandard: true}},
|
||||||
}
|
}
|
||||||
zulaessig, ausgeschlossen := rules.FilterWerkzeuge(kandidaten, []string{"avv_erforderlich"})
|
zulaessig, ausgeschlossen := rules.FilterWerkzeuge(kandidaten, []string{"avv_erforderlich"})
|
||||||
if len(zulaessig) != 1 || zulaessig[0] != "mit-avv" {
|
if len(zulaessig) != 1 || zulaessig[0] != "mit-avv" {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE werkzeug ADD COLUMN verarbeitungsort TEXT;
|
||||||
|
|
||||||
|
-- Ein Rückbau von Freitext-Ländern auf den alten Vier-Werte-Eimer ist
|
||||||
|
-- verlustbehaftet (welches Land genau "gemischt" war, geht verloren) —
|
||||||
|
-- bestmögliche Näherung statt Datenverlust durch NULL.
|
||||||
|
UPDATE werkzeug SET verarbeitungsort =
|
||||||
|
CASE
|
||||||
|
WHEN verarbeitungslaender = ARRAY['On-Premise (selbst gehostet)'] THEN 'on-prem'
|
||||||
|
WHEN verarbeitungslaender = ARRAY['USA'] THEN 'USA'
|
||||||
|
WHEN array_length(verarbeitungslaender, 1) IS NULL OR array_length(verarbeitungslaender, 1) = 0 THEN 'gemischt'
|
||||||
|
WHEN 'USA' = ANY(verarbeitungslaender) THEN 'gemischt'
|
||||||
|
ELSE 'EU'
|
||||||
|
END;
|
||||||
|
|
||||||
|
ALTER TABLE werkzeug ALTER COLUMN verarbeitungsort SET NOT NULL;
|
||||||
|
ALTER TABLE werkzeug ADD CONSTRAINT werkzeug_verarbeitungsort_check
|
||||||
|
CHECK (verarbeitungsort IN ('EU', 'USA', 'gemischt', 'on-prem'));
|
||||||
|
|
||||||
|
ALTER TABLE werkzeug DROP COLUMN verarbeitungslaender;
|
||||||
|
ALTER TABLE werkzeug DROP COLUMN dpf_zertifiziert;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- "EU"/"USA"/"gemischt"/"on-prem" als grobe Eimer verschleierten, dass
|
||||||
|
-- auch die USA bereits ein Drittland sind (DSGVO Art. 44 ff. kennt nur
|
||||||
|
-- EU/EWR vs. Drittland) und dass "gemischt" nichts über die tatsächlich
|
||||||
|
-- beteiligten Länder aussagt (z. B. Irland+USA bei Microsoft 365
|
||||||
|
-- Copilot vs. Frankreich bei Mistral) — beides macht einen Unterschied
|
||||||
|
-- für die Einschätzung, nicht nur für die Anzeige. Ersetzt durch eine
|
||||||
|
-- Liste tatsächlicher Länder (oder einer Region, wenn der Anbieter
|
||||||
|
-- selbst keinen einzelnen Staat benennt, z. B. "EU-Region" bei
|
||||||
|
-- OpenAI/Microsoft).
|
||||||
|
ALTER TABLE werkzeug ADD COLUMN verarbeitungslaender TEXT[] NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
ALTER TABLE werkzeug DROP CONSTRAINT werkzeug_verarbeitungsort_check;
|
||||||
|
|
||||||
|
UPDATE werkzeug SET verarbeitungslaender =
|
||||||
|
CASE verarbeitungsort
|
||||||
|
WHEN 'EU' THEN ARRAY['EU-Region (Land nicht migriert, bitte prüfen)']
|
||||||
|
WHEN 'USA' THEN ARRAY['USA']
|
||||||
|
WHEN 'on-prem' THEN ARRAY['On-Premise (selbst gehostet)']
|
||||||
|
ELSE ARRAY['nicht spezifiziert (bitte prüfen)']
|
||||||
|
END;
|
||||||
|
|
||||||
|
ALTER TABLE werkzeug DROP COLUMN verarbeitungsort;
|
||||||
|
|
||||||
|
-- Ob ein US-Anbieter unter dem EU-US Data Privacy Framework zertifiziert
|
||||||
|
-- ist (schmalere, für Schrems-Nachfolgeverfahren anfällige Rechtsgrundlage)
|
||||||
|
-- oder sich rein auf Standardvertragsklauseln stützt, stand bisher nur als
|
||||||
|
-- Fließtext in "einschraenkungen" — jetzt strukturiert und damit künftig
|
||||||
|
-- filterbar.
|
||||||
|
ALTER TABLE werkzeug ADD COLUMN dpf_zertifiziert BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -16,15 +16,27 @@ import (
|
|||||||
// LetztePruefung und Quelle sind Pflicht — jede Zusicherung im Katalog
|
// LetztePruefung und Quelle sind Pflicht — jede Zusicherung im Katalog
|
||||||
// muss belegbar sein.
|
// muss belegbar sein.
|
||||||
type Werkzeug struct {
|
type Werkzeug struct {
|
||||||
ID string
|
ID string
|
||||||
AccountID *string
|
AccountID *string
|
||||||
Name string
|
Name string
|
||||||
Anbieter string
|
Anbieter string
|
||||||
Verarbeitungsort string
|
// Verarbeitungslaender nennt die tatsächlichen Länder (oder, wenn
|
||||||
AVVVerfuegbar bool
|
// der Anbieter selbst keinen einzelnen Staat zusichert, eine Region
|
||||||
AVVURL string
|
// wie "EU-Region") statt eines groben EU/USA/gemischt-Eimers — die
|
||||||
TrainingOptOut bool
|
// USA sind DSGVO-rechtlich bereits ein Drittland wie jedes andere,
|
||||||
TrainingStandard bool
|
// "gemischt" verschleierte, welche Länder konkret beteiligt sind.
|
||||||
|
Verarbeitungslaender []string
|
||||||
|
AVVVerfuegbar bool
|
||||||
|
AVVURL string
|
||||||
|
TrainingOptOut bool
|
||||||
|
TrainingStandard bool
|
||||||
|
// DPFZertifiziert: EU-US Data Privacy Framework — schmalere,
|
||||||
|
// gerichtlich schon zweimal gekippte Rechtsgrundlage (Safe Harbor,
|
||||||
|
// Privacy Shield) für Transfers in die USA, gilt nur für zertifizierte
|
||||||
|
// Unternehmen. false bedeutet nicht "nicht zertifiziert" im Sinne von
|
||||||
|
// widerlegt, sondern "nicht als Zusicherung belegt" — wie bei den
|
||||||
|
// übrigen Booleans hier.
|
||||||
|
DPFZertifiziert bool
|
||||||
AufbewahrungTage *int // nil = vom Anbieter nicht beziffert, NICHT gleichbedeutend mit 0 Tagen
|
AufbewahrungTage *int // nil = vom Anbieter nicht beziffert, NICHT gleichbedeutend mit 0 Tagen
|
||||||
Zertifizierungen []string
|
Zertifizierungen []string
|
||||||
Subprozessoren []string
|
Subprozessoren []string
|
||||||
@@ -40,25 +52,26 @@ type Werkzeug struct {
|
|||||||
// Create/Update — bei 15 Feldern lesbarer als eine positionale
|
// Create/Update — bei 15 Feldern lesbarer als eine positionale
|
||||||
// Parameterliste.
|
// Parameterliste.
|
||||||
type WerkzeugInput struct {
|
type WerkzeugInput struct {
|
||||||
AccountID *string
|
AccountID *string
|
||||||
Name string
|
Name string
|
||||||
Anbieter string
|
Anbieter string
|
||||||
Verarbeitungsort string
|
Verarbeitungslaender []string
|
||||||
AVVVerfuegbar bool
|
AVVVerfuegbar bool
|
||||||
AVVURL string
|
AVVURL string
|
||||||
TrainingOptOut bool
|
TrainingOptOut bool
|
||||||
TrainingStandard bool
|
TrainingStandard bool
|
||||||
AufbewahrungTage *int
|
DPFZertifiziert bool
|
||||||
Zertifizierungen []string
|
AufbewahrungTage *int
|
||||||
Subprozessoren []string
|
Zertifizierungen []string
|
||||||
GeeigneteZwecke []string
|
Subprozessoren []string
|
||||||
Einschraenkungen []string
|
GeeigneteZwecke []string
|
||||||
LetztePruefung time.Time
|
Einschraenkungen []string
|
||||||
Quelle string
|
LetztePruefung time.Time
|
||||||
|
Quelle string
|
||||||
}
|
}
|
||||||
|
|
||||||
const werkzeugColumns = `id, account_id, name, anbieter, verarbeitungsort, avv_verfuegbar, avv_url,
|
const werkzeugColumns = `id, account_id, name, anbieter, verarbeitungslaender, avv_verfuegbar, avv_url,
|
||||||
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, subprozessoren,
|
training_opt_out, training_standard, dpf_zertifiziert, aufbewahrung_tage, zertifizierungen, subprozessoren,
|
||||||
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle, created_at, updated_at`
|
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle, created_at, updated_at`
|
||||||
|
|
||||||
func scanWerkzeug(row interface {
|
func scanWerkzeug(row interface {
|
||||||
@@ -66,8 +79,8 @@ func scanWerkzeug(row interface {
|
|||||||
}) (Werkzeug, error) {
|
}) (Werkzeug, error) {
|
||||||
var w Werkzeug
|
var w Werkzeug
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&w.ID, &w.AccountID, &w.Name, &w.Anbieter, &w.Verarbeitungsort, &w.AVVVerfuegbar, &w.AVVURL,
|
&w.ID, &w.AccountID, &w.Name, &w.Anbieter, &w.Verarbeitungslaender, &w.AVVVerfuegbar, &w.AVVURL,
|
||||||
&w.TrainingOptOut, &w.TrainingStandard, &w.AufbewahrungTage, &w.Zertifizierungen, &w.Subprozessoren,
|
&w.TrainingOptOut, &w.TrainingStandard, &w.DPFZertifiziert, &w.AufbewahrungTage, &w.Zertifizierungen, &w.Subprozessoren,
|
||||||
&w.GeeigneteZwecke, &w.Einschraenkungen, &w.LetztePruefung, &w.Quelle, &w.CreatedAt, &w.UpdatedAt,
|
&w.GeeigneteZwecke, &w.Einschraenkungen, &w.LetztePruefung, &w.Quelle, &w.CreatedAt, &w.UpdatedAt,
|
||||||
)
|
)
|
||||||
return w, err
|
return w, err
|
||||||
@@ -79,6 +92,9 @@ func scanWerkzeug(row interface {
|
|||||||
// einer wenig hilfreichen Constraint-Fehlermeldung abgelehnt (derselbe
|
// einer wenig hilfreichen Constraint-Fehlermeldung abgelehnt (derselbe
|
||||||
// Fall wie früher bei finding.sources).
|
// Fall wie früher bei finding.sources).
|
||||||
func normalizeWerkzeugSlices(in *WerkzeugInput) {
|
func normalizeWerkzeugSlices(in *WerkzeugInput) {
|
||||||
|
if in.Verarbeitungslaender == nil {
|
||||||
|
in.Verarbeitungslaender = []string{}
|
||||||
|
}
|
||||||
if in.Zertifizierungen == nil {
|
if in.Zertifizierungen == nil {
|
||||||
in.Zertifizierungen = []string{}
|
in.Zertifizierungen = []string{}
|
||||||
}
|
}
|
||||||
@@ -98,13 +114,13 @@ func (s *Store) CreateWerkzeug(ctx context.Context, in WerkzeugInput) (Werkzeug,
|
|||||||
normalizeWerkzeugSlices(&in)
|
normalizeWerkzeugSlices(&in)
|
||||||
row := s.Pool.QueryRow(ctx, `
|
row := s.Pool.QueryRow(ctx, `
|
||||||
INSERT INTO werkzeug (
|
INSERT INTO werkzeug (
|
||||||
account_id, name, anbieter, verarbeitungsort, avv_verfuegbar, avv_url,
|
account_id, name, anbieter, verarbeitungslaender, avv_verfuegbar, avv_url,
|
||||||
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, subprozessoren,
|
training_opt_out, training_standard, dpf_zertifiziert, aufbewahrung_tage, zertifizierungen, subprozessoren,
|
||||||
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle
|
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||||
RETURNING `+werkzeugColumns,
|
RETURNING `+werkzeugColumns,
|
||||||
in.AccountID, in.Name, in.Anbieter, in.Verarbeitungsort, in.AVVVerfuegbar, in.AVVURL,
|
in.AccountID, in.Name, in.Anbieter, in.Verarbeitungslaender, in.AVVVerfuegbar, in.AVVURL,
|
||||||
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
|
in.TrainingOptOut, in.TrainingStandard, in.DPFZertifiziert, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
|
||||||
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
|
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
|
||||||
)
|
)
|
||||||
w, err := scanWerkzeug(row)
|
w, err := scanWerkzeug(row)
|
||||||
@@ -123,14 +139,14 @@ func (s *Store) UpdateWerkzeug(ctx context.Context, id string, in WerkzeugInput)
|
|||||||
normalizeWerkzeugSlices(&in)
|
normalizeWerkzeugSlices(&in)
|
||||||
row := s.Pool.QueryRow(ctx, `
|
row := s.Pool.QueryRow(ctx, `
|
||||||
UPDATE werkzeug SET
|
UPDATE werkzeug SET
|
||||||
name = $2, anbieter = $3, verarbeitungsort = $4, avv_verfuegbar = $5, avv_url = $6,
|
name = $2, anbieter = $3, verarbeitungslaender = $4, avv_verfuegbar = $5, avv_url = $6,
|
||||||
training_opt_out = $7, training_standard = $8, aufbewahrung_tage = $9,
|
training_opt_out = $7, training_standard = $8, dpf_zertifiziert = $9, aufbewahrung_tage = $10,
|
||||||
zertifizierungen = $10, subprozessoren = $11, geeignete_zwecke = $12, einschraenkungen = $13,
|
zertifizierungen = $11, subprozessoren = $12, geeignete_zwecke = $13, einschraenkungen = $14,
|
||||||
letzte_pruefung = $14, quelle = $15, updated_at = now()
|
letzte_pruefung = $15, quelle = $16, updated_at = now()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING `+werkzeugColumns,
|
RETURNING `+werkzeugColumns,
|
||||||
id, in.Name, in.Anbieter, in.Verarbeitungsort, in.AVVVerfuegbar, in.AVVURL,
|
id, in.Name, in.Anbieter, in.Verarbeitungslaender, in.AVVVerfuegbar, in.AVVURL,
|
||||||
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
|
in.TrainingOptOut, in.TrainingStandard, in.DPFZertifiziert, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
|
||||||
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
|
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
|
||||||
)
|
)
|
||||||
w, err := scanWerkzeug(row)
|
w, err := scanWerkzeug(row)
|
||||||
|
|||||||
@@ -13,21 +13,21 @@ func intPtr(v int) *int { return &v }
|
|||||||
|
|
||||||
func testWerkzeugInput(accountID *string, name string) store.WerkzeugInput {
|
func testWerkzeugInput(accountID *string, name string) store.WerkzeugInput {
|
||||||
return store.WerkzeugInput{
|
return store.WerkzeugInput{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Anbieter: "Beispiel-Anbieter GmbH",
|
Anbieter: "Beispiel-Anbieter GmbH",
|
||||||
Verarbeitungsort: "EU",
|
Verarbeitungslaender: []string{"Deutschland"},
|
||||||
AVVVerfuegbar: true,
|
AVVVerfuegbar: true,
|
||||||
AVVURL: "https://beispiel.example/avv",
|
AVVURL: "https://beispiel.example/avv",
|
||||||
TrainingOptOut: true,
|
TrainingOptOut: true,
|
||||||
TrainingStandard: true,
|
TrainingStandard: true,
|
||||||
AufbewahrungTage: intPtr(30),
|
AufbewahrungTage: intPtr(30),
|
||||||
Zertifizierungen: []string{"ISO 27001"},
|
Zertifizierungen: []string{"ISO 27001"},
|
||||||
Subprozessoren: []string{"Beispiel-Subprozessor Inc."},
|
Subprozessoren: []string{"Beispiel-Subprozessor Inc."},
|
||||||
GeeigneteZwecke: []string{"Textgenerierung"},
|
GeeigneteZwecke: []string{"Textgenerierung"},
|
||||||
Einschraenkungen: nil,
|
Einschraenkungen: nil,
|
||||||
LetztePruefung: time.Now().Add(-24 * time.Hour).Truncate(time.Millisecond),
|
LetztePruefung: time.Now().Add(-24 * time.Hour).Truncate(time.Millisecond),
|
||||||
Quelle: "https://beispiel.example/beleg",
|
Quelle: "https://beispiel.example/beleg",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ func (s *Server) bewerteAntrag(ctx context.Context, antragID, accountID string,
|
|||||||
aufbewahrungTage = *w.AufbewahrungTage
|
aufbewahrungTage = *w.AufbewahrungTage
|
||||||
}
|
}
|
||||||
kandidaten[i] = rules.WerkzeugKandidat{ID: w.ID, Eigenschaften: rules.WerkzeugEigenschaften{
|
kandidaten[i] = rules.WerkzeugKandidat{ID: w.ID, Eigenschaften: rules.WerkzeugEigenschaften{
|
||||||
AVVVerfuegbar: w.AVVVerfuegbar, Verarbeitungsort: w.Verarbeitungsort,
|
AVVVerfuegbar: w.AVVVerfuegbar, Verarbeitungslaender: w.Verarbeitungslaender,
|
||||||
TrainingOptOut: w.TrainingOptOut, TrainingStandard: w.TrainingStandard, AufbewahrungTage: aufbewahrungTage,
|
TrainingOptOut: w.TrainingOptOut, TrainingStandard: w.TrainingStandard, AufbewahrungTage: aufbewahrungTage,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import (
|
|||||||
"github.com/netcell-it/deklarix/internal/store"
|
"github.com/netcell-it/deklarix/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
var gueltigeVerarbeitungsorte = []string{"EU", "USA", "gemischt", "on-prem"}
|
// istGueltigesLand validiert gegen weltLaenderliste (laenderliste.go)
|
||||||
|
// — dieselbe kontrollierte-Vokabular-Logik wie bei istGueltigerZweck,
|
||||||
func istGueltigerVerarbeitungsort(v string) bool {
|
// nur für Verarbeitungsländer statt Zwecke.
|
||||||
for _, o := range gueltigeVerarbeitungsorte {
|
func istGueltigesLand(v string) bool {
|
||||||
if o == v {
|
for _, l := range weltLaenderliste {
|
||||||
|
if l == v {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,18 +63,19 @@ func splitListe(s string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type werkzeugListItem struct {
|
type werkzeugListItem struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Anbieter string
|
Anbieter string
|
||||||
Verarbeitungsort string
|
Verarbeitungslaender string
|
||||||
AVVVerfuegbar bool
|
DPFZertifiziert bool
|
||||||
TrainingStandard bool
|
AVVVerfuegbar bool
|
||||||
Aufbewahrung string
|
TrainingStandard bool
|
||||||
Zertifizierungen string
|
Aufbewahrung string
|
||||||
GeeigneteZwecke string
|
Zertifizierungen string
|
||||||
LetztePruefung string
|
GeeigneteZwecke string
|
||||||
PruefStatusLabel string
|
LetztePruefung string
|
||||||
PruefStatusCSS string
|
PruefStatusLabel string
|
||||||
|
PruefStatusCSS string
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat
|
// formatAufbewahrung stellt sicher, dass "unbekannt" (Anbieter hat
|
||||||
@@ -134,8 +136,9 @@ func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Req
|
|||||||
for _, wz := range werkzeuge {
|
for _, wz := range werkzeuge {
|
||||||
label, css := pruefStatus(wz.LetztePruefung)
|
label, css := pruefStatus(wz.LetztePruefung)
|
||||||
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
data.Werkzeuge = append(data.Werkzeuge, werkzeugListItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungslaender: strings.Join(wz.Verarbeitungslaender, ", "),
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
DPFZertifiziert: wz.DPFZertifiziert,
|
||||||
|
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
||||||
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
||||||
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
||||||
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
||||||
@@ -152,32 +155,33 @@ func (s *Server) handleBetreiberWerkzeugListe(w http.ResponseWriter, r *http.Req
|
|||||||
// Formular für eigene Katalogeinträge (ActionBase "/verwaltung/werkzeuge")
|
// Formular für eigene Katalogeinträge (ActionBase "/verwaltung/werkzeuge")
|
||||||
// — dieselbe Template-Datei, siehe betreiber_werkzeug_form.html.
|
// — dieselbe Template-Datei, siehe betreiber_werkzeug_form.html.
|
||||||
type werkzeugFormData struct {
|
type werkzeugFormData struct {
|
||||||
Title string
|
Title string
|
||||||
Nav navData
|
Nav navData
|
||||||
ActionBase string
|
ActionBase string
|
||||||
ID string // leer bei "neu"
|
ID string // leer bei "neu"
|
||||||
Name string
|
Name string
|
||||||
Anbieter string
|
Anbieter string
|
||||||
Verarbeitungsorte []string
|
LaenderOptionen []string
|
||||||
Verarbeitungsort string
|
LaenderAusgewaehlt map[string]bool
|
||||||
AVVVerfuegbar bool
|
DPFZertifiziert bool
|
||||||
AVVURL string
|
AVVVerfuegbar bool
|
||||||
TrainingOptOut bool
|
AVVURL string
|
||||||
TrainingStandard bool
|
TrainingOptOut bool
|
||||||
AufbewahrungTage string // leer = vom Anbieter nicht beziffert (unbekannt), NICHT "0"
|
TrainingStandard bool
|
||||||
Zertifizierungen string
|
AufbewahrungTage string // leer = vom Anbieter nicht beziffert (unbekannt), NICHT "0"
|
||||||
Subprozessoren string
|
Zertifizierungen string
|
||||||
ZweckeOptionen []string
|
Subprozessoren string
|
||||||
ZweckeAusgewaehlt map[string]bool
|
ZweckeOptionen []string
|
||||||
Einschraenkungen string
|
ZweckeAusgewaehlt map[string]bool
|
||||||
LetztePruefung string
|
Einschraenkungen string
|
||||||
Quelle string
|
LetztePruefung string
|
||||||
Error string
|
Quelle string
|
||||||
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
// zweckeAusgewaehltVon baut die Checkbox-Vorbelegung aus einer bereits
|
// auswahlSet baut die Checkbox-Vorbelegung aus einer bereits
|
||||||
// gespeicherten oder erneut eingereichten Liste von Zwecken.
|
// gespeicherten oder erneut eingereichten Liste von Zwecken.
|
||||||
func zweckeAusgewaehltVon(ausgewaehlt []string) map[string]bool {
|
func auswahlSet(ausgewaehlt []string) map[string]bool {
|
||||||
m := make(map[string]bool, len(ausgewaehlt))
|
m := make(map[string]bool, len(ausgewaehlt))
|
||||||
for _, z := range ausgewaehlt {
|
for _, z := range ausgewaehlt {
|
||||||
m[z] = true
|
m[z] = true
|
||||||
@@ -194,12 +198,13 @@ func werkzeugFormDataFromWerkzeug(r *http.Request, wz store.Werkzeug, title, act
|
|||||||
aufbewahrung = strconv.Itoa(*wz.AufbewahrungTage)
|
aufbewahrung = strconv.Itoa(*wz.AufbewahrungTage)
|
||||||
}
|
}
|
||||||
return werkzeugFormData{
|
return werkzeugFormData{
|
||||||
Title: title, Nav: navFor(r), ActionBase: actionBase, ID: wz.ID, Verarbeitungsorte: gueltigeVerarbeitungsorte,
|
Title: title, Nav: navFor(r), ActionBase: actionBase, ID: wz.ID,
|
||||||
Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
LaenderOptionen: weltLaenderliste, LaenderAusgewaehlt: auswahlSet(wz.Verarbeitungslaender), DPFZertifiziert: wz.DPFZertifiziert,
|
||||||
|
Name: wz.Name, Anbieter: wz.Anbieter,
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, AVVURL: wz.AVVURL,
|
AVVVerfuegbar: wz.AVVVerfuegbar, AVVURL: wz.AVVURL,
|
||||||
TrainingOptOut: wz.TrainingOptOut, TrainingStandard: wz.TrainingStandard, AufbewahrungTage: aufbewahrung,
|
TrainingOptOut: wz.TrainingOptOut, TrainingStandard: wz.TrainingStandard, AufbewahrungTage: aufbewahrung,
|
||||||
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), Subprozessoren: strings.Join(wz.Subprozessoren, ", "),
|
Zertifizierungen: strings.Join(wz.Zertifizierungen, ", "), Subprozessoren: strings.Join(wz.Subprozessoren, ", "),
|
||||||
ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: zweckeAusgewaehltVon(wz.GeeigneteZwecke),
|
ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: auswahlSet(wz.GeeigneteZwecke),
|
||||||
Einschraenkungen: strings.Join(wz.Einschraenkungen, ", "), LetztePruefung: wz.LetztePruefung.Format("2006-01-02"),
|
Einschraenkungen: strings.Join(wz.Einschraenkungen, ", "), LetztePruefung: wz.LetztePruefung.Format("2006-01-02"),
|
||||||
Quelle: wz.Quelle,
|
Quelle: wz.Quelle,
|
||||||
}
|
}
|
||||||
@@ -208,7 +213,8 @@ func werkzeugFormDataFromWerkzeug(r *http.Request, wz store.Werkzeug, title, act
|
|||||||
func (s *Server) handleBetreiberWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleBetreiberWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) {
|
||||||
data := werkzeugFormData{
|
data := werkzeugFormData{
|
||||||
Title: "Werkzeug anlegen", Nav: navFor(r), ActionBase: "/betreiber/werkzeuge",
|
Title: "Werkzeug anlegen", Nav: navFor(r), ActionBase: "/betreiber/werkzeuge",
|
||||||
Verarbeitungsorte: gueltigeVerarbeitungsorte, ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: map[string]bool{},
|
LaenderOptionen: weltLaenderliste, LaenderAusgewaehlt: map[string]bool{},
|
||||||
|
ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: map[string]bool{},
|
||||||
}
|
}
|
||||||
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
||||||
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
||||||
@@ -223,9 +229,14 @@ func werkzeugInputFromForm(r *http.Request, accountID *string) (store.WerkzeugIn
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return store.WerkzeugInput{}, "Datum der letzten Prüfung ist ungültig oder fehlt"
|
return store.WerkzeugInput{}, "Datum der letzten Prüfung ist ungültig oder fehlt"
|
||||||
}
|
}
|
||||||
verarbeitungsort := r.FormValue("verarbeitungsort")
|
var verarbeitungslaender []string
|
||||||
if !istGueltigerVerarbeitungsort(verarbeitungsort) {
|
for _, l := range r.Form["verarbeitungslaender"] {
|
||||||
return store.WerkzeugInput{}, "ungültiger Verarbeitungsort"
|
if istGueltigesLand(l) {
|
||||||
|
verarbeitungslaender = append(verarbeitungslaender, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(verarbeitungslaender) == 0 {
|
||||||
|
return store.WerkzeugInput{}, "mindestens ein Verarbeitungsland ist Pflicht"
|
||||||
}
|
}
|
||||||
name := r.FormValue("name")
|
name := r.FormValue("name")
|
||||||
anbieter := r.FormValue("anbieter")
|
anbieter := r.FormValue("anbieter")
|
||||||
@@ -250,21 +261,22 @@ func werkzeugInputFromForm(r *http.Request, accountID *string) (store.WerkzeugIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
return store.WerkzeugInput{
|
return store.WerkzeugInput{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Anbieter: anbieter,
|
Anbieter: anbieter,
|
||||||
Verarbeitungsort: verarbeitungsort,
|
Verarbeitungslaender: verarbeitungslaender,
|
||||||
AVVVerfuegbar: r.FormValue("avv_verfuegbar") == "on",
|
AVVVerfuegbar: r.FormValue("avv_verfuegbar") == "on",
|
||||||
AVVURL: r.FormValue("avv_url"),
|
AVVURL: r.FormValue("avv_url"),
|
||||||
TrainingOptOut: r.FormValue("training_opt_out") == "on",
|
TrainingOptOut: r.FormValue("training_opt_out") == "on",
|
||||||
TrainingStandard: r.FormValue("training_standard") == "on",
|
TrainingStandard: r.FormValue("training_standard") == "on",
|
||||||
AufbewahrungTage: aufbewahrungTage,
|
DPFZertifiziert: r.FormValue("dpf_zertifiziert") == "on",
|
||||||
Zertifizierungen: splitListe(r.FormValue("zertifizierungen")),
|
AufbewahrungTage: aufbewahrungTage,
|
||||||
Subprozessoren: splitListe(r.FormValue("subprozessoren")),
|
Zertifizierungen: splitListe(r.FormValue("zertifizierungen")),
|
||||||
GeeigneteZwecke: geeigneteZwecke,
|
Subprozessoren: splitListe(r.FormValue("subprozessoren")),
|
||||||
Einschraenkungen: splitListe(r.FormValue("einschraenkungen")),
|
GeeigneteZwecke: geeigneteZwecke,
|
||||||
LetztePruefung: letztePruefung,
|
Einschraenkungen: splitListe(r.FormValue("einschraenkungen")),
|
||||||
Quelle: quelle,
|
LetztePruefung: letztePruefung,
|
||||||
|
Quelle: quelle,
|
||||||
}, ""
|
}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,13 +285,15 @@ func werkzeugInputFromForm(r *http.Request, accountID *string) (store.WerkzeugIn
|
|||||||
// statt das Formular zu leeren.
|
// statt das Formular zu leeren.
|
||||||
func werkzeugFormDataFromRequest(r *http.Request, title, actionBase, id, errMsg string) werkzeugFormData {
|
func werkzeugFormDataFromRequest(r *http.Request, title, actionBase, id, errMsg string) werkzeugFormData {
|
||||||
return werkzeugFormData{
|
return werkzeugFormData{
|
||||||
Title: title, Nav: navFor(r), ActionBase: actionBase, ID: id, Verarbeitungsorte: gueltigeVerarbeitungsorte,
|
Title: title, Nav: navFor(r), ActionBase: actionBase, ID: id,
|
||||||
Name: r.FormValue("name"), Anbieter: r.FormValue("anbieter"), Verarbeitungsort: r.FormValue("verarbeitungsort"),
|
LaenderOptionen: weltLaenderliste, LaenderAusgewaehlt: auswahlSet(r.Form["verarbeitungslaender"]),
|
||||||
|
DPFZertifiziert: r.FormValue("dpf_zertifiziert") == "on",
|
||||||
|
Name: r.FormValue("name"), Anbieter: r.FormValue("anbieter"),
|
||||||
AVVVerfuegbar: r.FormValue("avv_verfuegbar") == "on", AVVURL: r.FormValue("avv_url"),
|
AVVVerfuegbar: r.FormValue("avv_verfuegbar") == "on", AVVURL: r.FormValue("avv_url"),
|
||||||
TrainingOptOut: r.FormValue("training_opt_out") == "on", TrainingStandard: r.FormValue("training_standard") == "on",
|
TrainingOptOut: r.FormValue("training_opt_out") == "on", TrainingStandard: r.FormValue("training_standard") == "on",
|
||||||
AufbewahrungTage: strings.TrimSpace(r.FormValue("aufbewahrung_tage")),
|
AufbewahrungTage: strings.TrimSpace(r.FormValue("aufbewahrung_tage")),
|
||||||
Zertifizierungen: r.FormValue("zertifizierungen"), Subprozessoren: r.FormValue("subprozessoren"),
|
Zertifizierungen: r.FormValue("zertifizierungen"), Subprozessoren: r.FormValue("subprozessoren"),
|
||||||
ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: zweckeAusgewaehltVon(r.Form["geeignete_zwecke"]),
|
ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: auswahlSet(r.Form["geeignete_zwecke"]),
|
||||||
Einschraenkungen: r.FormValue("einschraenkungen"), LetztePruefung: r.FormValue("letzte_pruefung"),
|
Einschraenkungen: r.FormValue("einschraenkungen"), LetztePruefung: r.FormValue("letzte_pruefung"),
|
||||||
Quelle: r.FormValue("quelle"), Error: errMsg,
|
Quelle: r.FormValue("quelle"), Error: errMsg,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
func werkzeugForm() url.Values {
|
func werkzeugForm() url.Values {
|
||||||
return url.Values{
|
return url.Values{
|
||||||
"name": {"ChatGPT Enterprise"}, "anbieter": {"OpenAI"}, "verarbeitungsort": {"EU"},
|
"name": {"ChatGPT Enterprise"}, "anbieter": {"OpenAI"}, "verarbeitungslaender": {"Irland"},
|
||||||
"avv_verfuegbar": {"on"}, "avv_url": {"https://example.com/avv"},
|
"avv_verfuegbar": {"on"}, "avv_url": {"https://example.com/avv"},
|
||||||
"training_opt_out": {"on"}, "training_standard": {"on"}, "aufbewahrung_tage": {"30"},
|
"training_opt_out": {"on"}, "training_standard": {"on"}, "aufbewahrung_tage": {"30"},
|
||||||
"zertifizierungen": {"ISO 27001, SOC 2"}, "geeignete_zwecke": {"Textgenerierung"},
|
"zertifizierungen": {"ISO 27001, SOC 2"}, "geeignete_zwecke": {"Textgenerierung"},
|
||||||
|
|||||||
53
internal/web/laenderliste.go
Normal file
53
internal/web/laenderliste.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
// weltLaenderliste ist die kontrollierte Auswahl für "Verarbeitungsländer"
|
||||||
|
// im Werkzeugkatalog — eine echte Länderliste statt grober Eimer wie
|
||||||
|
// "EU"/"USA"/"gemischt" (die USA sind DSGVO-rechtlich bereits ein
|
||||||
|
// Drittland wie jedes andere, siehe CLAUDE.md). Enthält zusätzlich zwei
|
||||||
|
// Pseudo-Einträge für Fälle, in denen der Anbieter selbst keinen
|
||||||
|
// einzelnen Staat zusichert bzw. keine Cloud-Verarbeitung stattfindet.
|
||||||
|
var weltLaenderliste = []string{
|
||||||
|
"EU-Region (kein einzelnes Land benannt)",
|
||||||
|
"On-Premise (selbst gehostet)",
|
||||||
|
|
||||||
|
"Afghanistan", "Ägypten", "Albanien", "Algerien", "Andorra", "Angola",
|
||||||
|
"Antigua und Barbuda", "Äquatorialguinea", "Argentinien", "Armenien",
|
||||||
|
"Aserbaidschan", "Äthiopien", "Australien", "Bahamas", "Bahrain",
|
||||||
|
"Bangladesch", "Barbados", "Belgien", "Belize", "Benin", "Bhutan",
|
||||||
|
"Bolivien", "Bosnien und Herzegowina", "Botsuana", "Brasilien",
|
||||||
|
"Brunei", "Bulgarien", "Burkina Faso", "Burundi", "Chile", "China",
|
||||||
|
"Costa Rica", "Dänemark", "Deutschland", "Dominica",
|
||||||
|
"Dominikanische Republik", "Dschibuti", "Ecuador", "El Salvador",
|
||||||
|
"Elfenbeinküste", "Eritrea", "Estland", "Eswatini", "Fidschi",
|
||||||
|
"Finnland", "Frankreich", "Gabun", "Gambia", "Georgien", "Ghana",
|
||||||
|
"Grenada", "Griechenland", "Guatemala", "Guinea", "Guinea-Bissau",
|
||||||
|
"Guyana", "Haiti", "Honduras", "Indien", "Indonesien", "Irak",
|
||||||
|
"Iran", "Irland", "Island", "Israel", "Italien", "Jamaika", "Japan",
|
||||||
|
"Jemen", "Jordanien", "Kambodscha", "Kamerun", "Kanada", "Kap Verde",
|
||||||
|
"Kasachstan", "Katar", "Kenia", "Kirgisistan", "Kiribati",
|
||||||
|
"Kolumbien", "Komoren", "Kongo (Republik)",
|
||||||
|
"Kongo (Demokratische Republik)", "Kosovo", "Kroatien", "Kuba",
|
||||||
|
"Kuwait", "Laos", "Lesotho", "Lettland", "Libanon", "Liberia",
|
||||||
|
"Libyen", "Liechtenstein", "Litauen", "Luxemburg", "Madagaskar",
|
||||||
|
"Malawi", "Malaysia", "Malediven", "Mali", "Malta", "Marokko",
|
||||||
|
"Marshallinseln", "Mauretanien", "Mauritius", "Mexiko",
|
||||||
|
"Mikronesien", "Moldau", "Monaco", "Mongolei", "Montenegro",
|
||||||
|
"Mosambik", "Myanmar", "Namibia", "Nauru", "Nepal", "Neuseeland",
|
||||||
|
"Nicaragua", "Niederlande", "Niger", "Nigeria", "Nordkorea",
|
||||||
|
"Nordmazedonien", "Norwegen", "Oman", "Österreich", "Osttimor",
|
||||||
|
"Pakistan", "Palau", "Palästina", "Panama", "Papua-Neuguinea",
|
||||||
|
"Paraguay", "Peru", "Philippinen", "Polen", "Portugal", "Ruanda",
|
||||||
|
"Rumänien", "Russland", "Salomonen", "Sambia", "Samoa",
|
||||||
|
"San Marino", "São Tomé und Príncipe", "Saudi-Arabien", "Schweden",
|
||||||
|
"Schweiz", "Senegal", "Serbien", "Seychellen", "Sierra Leone",
|
||||||
|
"Simbabwe", "Singapur", "Slowakei", "Slowenien", "Somalia",
|
||||||
|
"Spanien", "Sri Lanka", "St. Kitts und Nevis", "St. Lucia",
|
||||||
|
"St. Vincent und die Grenadinen", "Südafrika", "Sudan", "Südkorea",
|
||||||
|
"Südsudan", "Suriname", "Syrien", "Tadschikistan", "Taiwan",
|
||||||
|
"Tansania", "Thailand", "Togo", "Tonga", "Trinidad und Tobago",
|
||||||
|
"Tschad", "Tschechien", "Tunesien", "Türkei", "Turkmenistan",
|
||||||
|
"Tuvalu", "Uganda", "Ukraine", "Ungarn", "Uruguay", "USA",
|
||||||
|
"Usbekistan", "Vanuatu", "Vatikanstadt", "Venezuela",
|
||||||
|
"Vereinigte Arabische Emirate", "Vereinigtes Königreich", "Vietnam",
|
||||||
|
"Zentralafrikanische Republik", "Zypern",
|
||||||
|
}
|
||||||
@@ -14,26 +14,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type mandantWerkzeugItem struct {
|
type mandantWerkzeugItem struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Anbieter string
|
Anbieter string
|
||||||
Verarbeitungsort string
|
Verarbeitungslaender string
|
||||||
AVVVerfuegbar bool
|
AVVVerfuegbar bool
|
||||||
TrainingStandard bool
|
TrainingStandard bool
|
||||||
Aufbewahrung string
|
Aufbewahrung string
|
||||||
GeeigneteZwecke string
|
GeeigneteZwecke string
|
||||||
LetztePruefung string
|
LetztePruefung string
|
||||||
PruefStatusLabel string
|
PruefStatusLabel string
|
||||||
PruefStatusCSS string
|
PruefStatusCSS string
|
||||||
Gesperrt bool
|
Gesperrt bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type eigenesWerkzeugItem struct {
|
type eigenesWerkzeugItem struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Anbieter string
|
Anbieter string
|
||||||
Verarbeitungsort string
|
Verarbeitungslaender string
|
||||||
LetztePruefung string
|
LetztePruefung string
|
||||||
}
|
}
|
||||||
|
|
||||||
type mandantWerkzeugListeData struct {
|
type mandantWerkzeugListeData struct {
|
||||||
@@ -73,7 +73,7 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque
|
|||||||
for _, wz := range zentral {
|
for _, wz := range zentral {
|
||||||
label, css := pruefStatus(wz.LetztePruefung)
|
label, css := pruefStatus(wz.LetztePruefung)
|
||||||
data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{
|
data.Werkzeuge = append(data.Werkzeuge, mandantWerkzeugItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungslaender: strings.Join(wz.Verarbeitungslaender, ", "),
|
||||||
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
AVVVerfuegbar: wz.AVVVerfuegbar, TrainingStandard: wz.TrainingStandard,
|
||||||
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
Aufbewahrung: formatAufbewahrung(wz.AufbewahrungTage),
|
||||||
GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
GeeigneteZwecke: strings.Join(wz.GeeigneteZwecke, ", "),
|
||||||
@@ -87,7 +87,7 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque
|
|||||||
continue // zentral, oben schon gelistet
|
continue // zentral, oben schon gelistet
|
||||||
}
|
}
|
||||||
data.EigeneWerkzeuge = append(data.EigeneWerkzeuge, eigenesWerkzeugItem{
|
data.EigeneWerkzeuge = append(data.EigeneWerkzeuge, eigenesWerkzeugItem{
|
||||||
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungsort: wz.Verarbeitungsort,
|
ID: wz.ID, Name: wz.Name, Anbieter: wz.Anbieter, Verarbeitungslaender: strings.Join(wz.Verarbeitungslaender, ", "),
|
||||||
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
LetztePruefung: wz.LetztePruefung.Format("02.01.2006"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,8 @@ func (s *Server) handleMandantWerkzeugListe(w http.ResponseWriter, r *http.Reque
|
|||||||
func (s *Server) handleMandantWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleMandantWerkzeugNeuForm(w http.ResponseWriter, r *http.Request) {
|
||||||
data := werkzeugFormData{
|
data := werkzeugFormData{
|
||||||
Title: "Eigenes Werkzeug anlegen", Nav: navFor(r), ActionBase: "/verwaltung/werkzeuge",
|
Title: "Eigenes Werkzeug anlegen", Nav: navFor(r), ActionBase: "/verwaltung/werkzeuge",
|
||||||
Verarbeitungsorte: gueltigeVerarbeitungsorte, ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: map[string]bool{},
|
LaenderOptionen: weltLaenderliste, LaenderAusgewaehlt: map[string]bool{},
|
||||||
|
ZweckeOptionen: gueltigeZwecke, ZweckeAusgewaehlt: map[string]bool{},
|
||||||
}
|
}
|
||||||
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
if err := s.templates.ExecuteTemplate(w, "betreiber-werkzeug-form", data); err != nil {
|
||||||
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -408,9 +408,9 @@ func (f *fakeStore) CreateWerkzeug(ctx context.Context, in store.WerkzeugInput)
|
|||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
w := store.Werkzeug{
|
w := store.Werkzeug{
|
||||||
ID: f.newID(), AccountID: in.AccountID, Name: in.Name, Anbieter: in.Anbieter,
|
ID: f.newID(), AccountID: in.AccountID, Name: in.Name, Anbieter: in.Anbieter,
|
||||||
Verarbeitungsort: in.Verarbeitungsort, AVVVerfuegbar: in.AVVVerfuegbar, AVVURL: in.AVVURL,
|
Verarbeitungslaender: in.Verarbeitungslaender, AVVVerfuegbar: in.AVVVerfuegbar, AVVURL: in.AVVURL,
|
||||||
TrainingOptOut: in.TrainingOptOut, TrainingStandard: in.TrainingStandard, AufbewahrungTage: in.AufbewahrungTage,
|
TrainingOptOut: in.TrainingOptOut, TrainingStandard: in.TrainingStandard, DPFZertifiziert: in.DPFZertifiziert, AufbewahrungTage: in.AufbewahrungTage,
|
||||||
Zertifizierungen: in.Zertifizierungen, GeeigneteZwecke: in.GeeigneteZwecke, Einschraenkungen: in.Einschraenkungen,
|
Zertifizierungen: in.Zertifizierungen, Subprozessoren: in.Subprozessoren, GeeigneteZwecke: in.GeeigneteZwecke, Einschraenkungen: in.Einschraenkungen,
|
||||||
LetztePruefung: in.LetztePruefung, Quelle: in.Quelle, CreatedAt: time.Now(), UpdatedAt: time.Now(),
|
LetztePruefung: in.LetztePruefung, Quelle: in.Quelle, CreatedAt: time.Now(), UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
f.werkzeuge[w.ID] = w
|
f.werkzeuge[w.ID] = w
|
||||||
@@ -424,10 +424,10 @@ func (f *fakeStore) UpdateWerkzeug(ctx context.Context, id string, in store.Werk
|
|||||||
if !ok {
|
if !ok {
|
||||||
return store.Werkzeug{}, store.ErrNotFound
|
return store.Werkzeug{}, store.ErrNotFound
|
||||||
}
|
}
|
||||||
w.Name, w.Anbieter, w.Verarbeitungsort = in.Name, in.Anbieter, in.Verarbeitungsort
|
w.Name, w.Anbieter, w.Verarbeitungslaender = in.Name, in.Anbieter, in.Verarbeitungslaender
|
||||||
w.AVVVerfuegbar, w.AVVURL = in.AVVVerfuegbar, in.AVVURL
|
w.AVVVerfuegbar, w.AVVURL = in.AVVVerfuegbar, in.AVVURL
|
||||||
w.TrainingOptOut, w.TrainingStandard, w.AufbewahrungTage = in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage
|
w.TrainingOptOut, w.TrainingStandard, w.DPFZertifiziert, w.AufbewahrungTage = in.TrainingOptOut, in.TrainingStandard, in.DPFZertifiziert, in.AufbewahrungTage
|
||||||
w.Zertifizierungen, w.GeeigneteZwecke, w.Einschraenkungen = in.Zertifizierungen, in.GeeigneteZwecke, in.Einschraenkungen
|
w.Zertifizierungen, w.Subprozessoren, w.GeeigneteZwecke, w.Einschraenkungen = in.Zertifizierungen, in.Subprozessoren, in.GeeigneteZwecke, in.Einschraenkungen
|
||||||
w.LetztePruefung, w.Quelle, w.UpdatedAt = in.LetztePruefung, in.Quelle, time.Now()
|
w.LetztePruefung, w.Quelle, w.UpdatedAt = in.LetztePruefung, in.Quelle, time.Now()
|
||||||
f.werkzeuge[id] = w
|
f.werkzeuge[id] = w
|
||||||
return w, nil
|
return w, nil
|
||||||
|
|||||||
@@ -59,7 +59,15 @@ h1, h2, h3 {
|
|||||||
color: var(--color-heading);
|
color: var(--color-heading);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
margin: 0 0 0.5em;
|
margin: 0 0 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.0625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@@ -542,8 +550,8 @@ button.entfernen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fragebogen legend {
|
.fragebogen legend {
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
font-size: 0.9375rem;
|
font-size: 1rem;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,10 +592,14 @@ button.entfernen {
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* <legend> ignoriert das padding des umschließenden <fieldset> (es
|
||||||
|
positioniert sich an dessen Border-Box, nicht an der Padding-Box) —
|
||||||
|
ohne eigenes padding-left säße die Abschnittsüberschrift bündig am
|
||||||
|
Kartenrand, während der restliche Inhalt eingerückt ist. */
|
||||||
.form-section > legend {
|
.form-section > legend {
|
||||||
padding: 0 0 12px;
|
padding: 0 0 16px 0;
|
||||||
font-size: 0.9375rem;
|
font-size: 1.0625rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--color-heading);
|
color: var(--color-heading);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<p><a href="{{.ActionBase}}">← Werkzeugkatalog</a></p>
|
<p><a href="{{.ActionBase}}">← Werkzeugkatalog</a></p>
|
||||||
<h1>{{.Title}}</h1>
|
<h1>{{.Title}}</h1>
|
||||||
<p class="hinweis">
|
<p class="hinweis">
|
||||||
Jede Zusicherung hier (AVV, Verarbeitungsort, Training-Opt-out,
|
Jede Zusicherung hier (AVV, Verarbeitungsländer, Training-Opt-out,
|
||||||
Zertifizierungen) muss belegbar sein — Quelle und letzte Prüfung sind
|
Zertifizierungen) muss belegbar sein — Quelle und letzte Prüfung sind
|
||||||
Pflicht.
|
Pflicht.
|
||||||
</p>
|
</p>
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
<input type="text" id="anbieter" name="anbieter" value="{{.Anbieter}}" required>
|
<input type="text" id="anbieter" name="anbieter" value="{{.Anbieter}}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-full">
|
<div class="form-full">
|
||||||
<label>Verarbeitungsort</label>
|
<label for="verarbeitungslaender">Verarbeitungsländer (Mehrfachauswahl: Strg/Cmd-Klick)</label>
|
||||||
{{$gewaehlt := .Verarbeitungsort}}
|
<select id="verarbeitungslaender" name="verarbeitungslaender" multiple size="8" required>
|
||||||
<div class="chip-group">
|
{{range .LaenderOptionen}}
|
||||||
{{range .Verarbeitungsorte}}
|
<option value="{{.}}" {{if index $.LaenderAusgewaehlt .}}selected{{end}}>{{.}}</option>
|
||||||
<label><input type="radio" name="verarbeitungsort" value="{{.}}" {{if eq . $gewaehlt}}checked{{end}} required> {{.}}</label>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</select>
|
||||||
|
<p class="hinweis">Tatsächliche Länder benennen, nicht "EU"/"Drittland" — die USA sind DSGVO-rechtlich bereits ein Drittland wie jedes andere.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
<label><input type="checkbox" name="avv_verfuegbar" {{if .AVVVerfuegbar}}checked{{end}}> AVV verfügbar</label>
|
<label><input type="checkbox" name="avv_verfuegbar" {{if .AVVVerfuegbar}}checked{{end}}> AVV verfügbar</label>
|
||||||
<label><input type="checkbox" name="training_opt_out" {{if .TrainingOptOut}}checked{{end}}> Training-Opt-out möglich</label>
|
<label><input type="checkbox" name="training_opt_out" {{if .TrainingOptOut}}checked{{end}}> Training-Opt-out möglich</label>
|
||||||
<label><input type="checkbox" name="training_standard" {{if .TrainingStandard}}checked{{end}}> Training standardmäßig aus</label>
|
<label><input type="checkbox" name="training_standard" {{if .TrainingStandard}}checked{{end}}> Training standardmäßig aus</label>
|
||||||
|
<label><input type="checkbox" name="dpf_zertifiziert" {{if .DPFZertifiziert}}checked{{end}}> EU-US Data Privacy Framework (DPF) zertifiziert</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-full">
|
<div class="form-full">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th><th>Anbieter</th><th>Verarbeitungsort</th><th>AVV</th>
|
<th>Name</th><th>Anbieter</th><th>Verarbeitungsländer</th><th>DPF</th><th>AVV</th>
|
||||||
<th>Training aus</th><th>Aufbewahrung</th><th>Zertifizierungen</th><th>Geeignete Zwecke</th>
|
<th>Training aus</th><th>Aufbewahrung</th><th>Zertifizierungen</th><th>Geeignete Zwecke</th>
|
||||||
<th>Zuletzt geprüft</th>
|
<th>Zuletzt geprüft</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="/betreiber/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
<td><a href="/betreiber/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||||
<td>{{.Anbieter}}</td>
|
<td>{{.Anbieter}}</td>
|
||||||
<td>{{.Verarbeitungsort}}</td>
|
<td>{{.Verarbeitungslaender}}</td>
|
||||||
|
<td>{{if .DPFZertifiziert}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{.Aufbewahrung}}</td>
|
<td>{{.Aufbewahrung}}</td>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th><th>Anbieter</th><th>Verarbeitungsort</th><th>AVV</th>
|
<th>Name</th><th>Anbieter</th><th>Verarbeitungsländer</th><th>AVV</th>
|
||||||
<th>Training aus</th><th>Aufbewahrung</th><th>Geeignete Zwecke</th><th>Zuletzt geprüft</th><th>Status</th>
|
<th>Training aus</th><th>Aufbewahrung</th><th>Geeignete Zwecke</th><th>Zuletzt geprüft</th><th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{.Name}}</td>
|
<td>{{.Name}}</td>
|
||||||
<td>{{.Anbieter}}</td>
|
<td>{{.Anbieter}}</td>
|
||||||
<td>{{.Verarbeitungsort}}</td>
|
<td>{{.Verarbeitungslaender}}</td>
|
||||||
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
<td>{{if .AVVVerfuegbar}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
<td>{{if .TrainingStandard}}ja{{else}}nein{{end}}</td>
|
||||||
<td>{{.Aufbewahrung}}</td>
|
<td>{{.Aufbewahrung}}</td>
|
||||||
@@ -65,14 +65,14 @@
|
|||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Anbieter</th><th>Verarbeitungsort</th><th>Zuletzt geprüft</th></tr>
|
<tr><th>Name</th><th>Anbieter</th><th>Verarbeitungsländer</th><th>Zuletzt geprüft</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .EigeneWerkzeuge}}
|
{{range .EigeneWerkzeuge}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
<td><a href="/verwaltung/werkzeuge/{{.ID}}">{{.Name}}</a></td>
|
||||||
<td>{{.Anbieter}}</td>
|
<td>{{.Anbieter}}</td>
|
||||||
<td>{{.Verarbeitungsort}}</td>
|
<td>{{.Verarbeitungslaender}}</td>
|
||||||
<td>{{.LetztePruefung}}</td>
|
<td>{{.LetztePruefung}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ package web
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netcell-it/deklarix/internal/store"
|
"github.com/netcell-it/deklarix/internal/store"
|
||||||
@@ -30,8 +32,9 @@ func werkzeugDiff(snapshot, current store.Werkzeug) []string {
|
|||||||
if snapshot.TrainingStandard != current.TrainingStandard {
|
if snapshot.TrainingStandard != current.TrainingStandard {
|
||||||
diffs = append(diffs, fmt.Sprintf("Training standardmäßig aus: %v → %v", snapshot.TrainingStandard, current.TrainingStandard))
|
diffs = append(diffs, fmt.Sprintf("Training standardmäßig aus: %v → %v", snapshot.TrainingStandard, current.TrainingStandard))
|
||||||
}
|
}
|
||||||
if snapshot.Verarbeitungsort != current.Verarbeitungsort {
|
if !slices.Equal(snapshot.Verarbeitungslaender, current.Verarbeitungslaender) {
|
||||||
diffs = append(diffs, fmt.Sprintf("Verarbeitungsort: %s → %s", snapshot.Verarbeitungsort, current.Verarbeitungsort))
|
diffs = append(diffs, fmt.Sprintf("Verarbeitungsländer: %s → %s",
|
||||||
|
strings.Join(snapshot.Verarbeitungslaender, ", "), strings.Join(current.Verarbeitungslaender, ", ")))
|
||||||
}
|
}
|
||||||
return diffs
|
return diffs
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user