Compare commits
51 Commits
95c371e9a5
...
coolify
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff775517a2 | ||
|
|
98a773c7cd | ||
|
|
528abc86ab | ||
|
|
be4d58009a | ||
|
|
e07e1de6c9 | ||
|
|
58e95d5e8e | ||
|
|
786bb409e4 | ||
|
|
3c4f7d900d | ||
|
|
aae07b7a9b | ||
|
|
911d872178 | ||
|
|
fc6a3306d4 | ||
|
|
ab6ba63108 | ||
|
|
769e8c12d5 | ||
|
|
7344e5806e | ||
|
|
32e121f2a3 | ||
|
|
07d470edee | ||
|
|
a84dccb339 | ||
|
|
1a2ae896fb | ||
|
|
d35b0bc78c | ||
|
|
ae008d7d25 | ||
|
|
6658776610 | ||
|
|
d2c94619d8 | ||
|
|
cc1c61947d | ||
|
|
0c2e03f294 | ||
|
|
a638d0e527 | ||
|
|
e613af1a7d | ||
|
|
7107a31496 | ||
|
|
b850368ec9 | ||
|
|
4fa0dd6f6d | ||
|
|
f39c7ca40c | ||
|
|
d571412657 | ||
|
|
10073f3ef0 | ||
|
|
883ef702ac | ||
|
|
4a91814bfc | ||
|
|
482e8574ad | ||
|
|
d9dcfb97ef | ||
|
|
3320ef94fc | ||
|
|
1dfea51919 | ||
|
|
559d7960a2 | ||
|
|
a101426dba | ||
|
|
f6b22820ce | ||
|
|
86588aff09 | ||
|
|
033fa52e5b | ||
|
|
005fb9d219 | ||
|
|
0c01f1c96c | ||
|
|
ffd256d420 | ||
|
|
d542dbbacd | ||
|
|
a3d0024d39 | ||
|
|
998d427c3c | ||
|
|
99f3180ffc | ||
|
|
2ec340c64b |
@@ -1,5 +1,17 @@
|
||||
# BreakPilot Compliance - DSGVO/AI-Act SDK Platform
|
||||
|
||||
> **NON-NEGOTIABLE STRUCTURE RULES** (enforced by `.claude/settings.json` hook, git pre-commit, and CI):
|
||||
> 1. **File-size budget:** soft target **300** lines, **hard cap 500** lines for any non-test, non-generated source file. Anything larger → split it. Exceptions are listed in `.claude/rules/loc-exceptions.txt` and require a written rationale.
|
||||
> 2. **Clean architecture per service.** Routers/handlers stay thin (≤30 lines per handler) and delegate to services; services use repositories; repositories own DB I/O. See `AGENTS.python.md` / `AGENTS.go.md` / `AGENTS.typescript.md`.
|
||||
> 3. **Do not touch the database schema.** No new Alembic migrations, no `ALTER TABLE`, no model field renames without an explicit migration plan reviewed by the DB owner. SQLAlchemy `__tablename__` and column names are frozen.
|
||||
> 4. **Public endpoints are a contract.** Any change to a path, method, status code, request schema, or response schema in `backend-compliance/`, `ai-compliance-sdk/`, `dsms-gateway/`, `document-crawler/`, or `compliance-tts-service/` must be accompanied by a matching update in **every** consumer (`admin-compliance/`, `developer-portal/`, `breakpilot-compliance-sdk/`, `consent-sdk/`). Use the OpenAPI snapshot tests in `tests/contracts/` as the gate.
|
||||
> 5. **Tests are not optional.** New code without tests fails CI. Refactors must preserve coverage and add a characterization test before splitting an oversized file.
|
||||
> 6. **Do not bypass the guardrails.** Do not edit `.claude/settings.json`, `scripts/check-loc.sh`, or the loc-exceptions list to silence violations. If a rule is wrong, raise it in a PR description.
|
||||
>
|
||||
> These rules apply to **every** Claude Code session opened inside this repository, regardless of who launched it. They are loaded automatically via this `CLAUDE.md`.
|
||||
|
||||
|
||||
|
||||
## Entwicklungsumgebung (WICHTIG - IMMER ZUERST LESEN)
|
||||
|
||||
### Zwei-Rechner-Setup + Hetzner
|
||||
|
||||
43
.claude/rules/architecture.md
Normal file
43
.claude/rules/architecture.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Architecture Rules (auto-loaded)
|
||||
|
||||
These rules apply to **every** Claude Code session in this repository, regardless of who launched it. They are non-negotiable.
|
||||
|
||||
## File-size budget
|
||||
|
||||
- **Soft target:** 300 lines per non-test, non-generated source file.
|
||||
- **Hard cap:** 500 lines. The PreToolUse hook in `.claude/settings.json` blocks Write/Edit operations that would create or push a file past 500. The git pre-commit hook re-checks. CI is the final gate.
|
||||
- Exceptions live in `.claude/rules/loc-exceptions.txt` and require a written rationale plus `[guardrail-change]` in the commit message. The exceptions list should shrink over time, not grow.
|
||||
|
||||
## Clean architecture
|
||||
|
||||
- Python (FastAPI): see `AGENTS.python.md`. Layering: `api → services → repositories → db.models`. Routers ≤30 LOC per handler. Schemas split per domain.
|
||||
- Go (Gin): see `AGENTS.go.md`. Standard Go Project Layout + hexagonal. `cmd/` thin, wiring in `internal/app`.
|
||||
- TypeScript (Next.js): see `AGENTS.typescript.md`. Server-by-default, push the client boundary deep, colocate `_components/` and `_hooks/` per route.
|
||||
|
||||
## Database is frozen
|
||||
|
||||
- No new Alembic migrations. No `ALTER TABLE`. No `__tablename__` or column renames.
|
||||
- The pre-commit hook blocks any change under `migrations/` or `alembic/versions/` unless the commit message contains `[migration-approved]`.
|
||||
|
||||
## Public endpoints are a contract
|
||||
|
||||
- Any change to a path/method/status/request schema/response schema in a backend service must update every consumer in the same change set.
|
||||
- Each backend service has an OpenAPI baseline at `tests/contracts/openapi.baseline.json`. Contract tests fail on drift.
|
||||
|
||||
## Tests
|
||||
|
||||
- New code without tests fails CI.
|
||||
- Refactors must preserve coverage. Before splitting an oversized file, add a characterization test that pins current behavior.
|
||||
- Layout: `tests/unit/`, `tests/integration/`, `tests/contracts/`, `tests/e2e/`.
|
||||
|
||||
## Guardrails are themselves protected
|
||||
|
||||
- Edits to `.claude/settings.json`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, `.claude/rules/loc-exceptions.txt`, or any `AGENTS.*.md` require `[guardrail-change]` in the commit message. The pre-commit hook enforces this.
|
||||
- If you (Claude) think a rule is wrong, surface it to the user. Do not silently weaken it.
|
||||
|
||||
## Tooling baseline
|
||||
|
||||
- Python: `ruff`, `mypy --strict` on new modules, `pytest --cov`.
|
||||
- Go: `golangci-lint` strict config, `go vet`, table-driven tests.
|
||||
- TS: `tsc --noEmit` strict, ESLint type-aware, Vitest, Playwright.
|
||||
- All three: dependency caching in CI, license/SBOM scan via `syft`+`grype`.
|
||||
48
.claude/rules/loc-exceptions.txt
Normal file
48
.claude/rules/loc-exceptions.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
# loc-exceptions.txt — files allowed to exceed the 500-line hard cap.
|
||||
#
|
||||
# Format: one repo-relative path per line. Comments start with '#' and are ignored.
|
||||
# Each exception MUST be preceded by a comment explaining why splitting is not viable.
|
||||
#
|
||||
# Phase 0 baseline: this list is initially empty. Phases 1-4 will add grandfathered
|
||||
# entries as we encounter legitimate exceptions (e.g. large generated data tables).
|
||||
# The goal is for this list to SHRINK over time, never grow.
|
||||
|
||||
# --- admin-compliance: static data catalogs (Phase 3) ---
|
||||
# Splitting these would fragment lookup tables without improving readability.
|
||||
admin-compliance/lib/sdk/tom-generator/controls/loader.ts
|
||||
admin-compliance/lib/sdk/vendor-compliance/risk/controls-library.ts
|
||||
admin-compliance/lib/sdk/compliance-scope-triggers.ts
|
||||
admin-compliance/lib/sdk/vendor-compliance/catalog/processing-activities.ts
|
||||
admin-compliance/lib/sdk/catalog-manager/catalog-registry.ts
|
||||
admin-compliance/lib/sdk/dsfa/mitigation-library.ts
|
||||
admin-compliance/lib/sdk/vvt-baseline-catalog.ts
|
||||
admin-compliance/lib/sdk/dsfa/eu-legal-frameworks.ts
|
||||
admin-compliance/lib/sdk/dsfa/risk-catalog.ts
|
||||
admin-compliance/lib/sdk/loeschfristen-baseline-catalog.ts
|
||||
admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-templates.ts
|
||||
admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis.ts
|
||||
admin-compliance/lib/sdk/vendor-compliance/contract-review/findings.ts
|
||||
admin-compliance/lib/sdk/vendor-compliance/contract-review/checklists.ts
|
||||
admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core.ts
|
||||
admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended.ts
|
||||
admin-compliance/lib/sdk/demo-data/index.ts
|
||||
admin-compliance/lib/sdk/tom-generator/demo-data/index.ts
|
||||
|
||||
# --- admin-compliance: self-contained export generators (Phase 3) ---
|
||||
# Each file generates a complete document format. Splitting mid-generation
|
||||
# logic would create artificial module boundaries without benefit.
|
||||
admin-compliance/lib/sdk/tom-generator/export/zip.ts
|
||||
admin-compliance/lib/sdk/tom-generator/export/docx.ts
|
||||
admin-compliance/lib/sdk/tom-generator/export/pdf.ts
|
||||
admin-compliance/lib/sdk/einwilligungen/export/pdf.ts
|
||||
admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts
|
||||
|
||||
# --- backend-compliance: legacy utility services (Phase 1) ---
|
||||
# Pre-refactor utility modules not yet split. Phase 5 targets.
|
||||
backend-compliance/compliance/services/control_generator.py
|
||||
backend-compliance/compliance/services/audit_pdf_generator.py
|
||||
backend-compliance/compliance/services/regulation_scraper.py
|
||||
backend-compliance/compliance/services/llm_provider.py
|
||||
backend-compliance/compliance/services/export_generator.py
|
||||
backend-compliance/compliance/services/pdf_extractor.py
|
||||
backend-compliance/compliance/services/ai_compliance_assistant.py
|
||||
28
.claude/settings.json
Normal file
28
.claude/settings.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] && exit 0; lines=$(printf '%s' \"$(jq -r '.tool_input.content // empty')\" | awk 'END{print NR}'); if [ \"${lines:-0}\" -gt 500 ]; then echo '{\"decision\":\"block\",\"reason\":\"breakpilot guardrail: file exceeds the 500-line hard cap. Split it into smaller modules per the layering rules in AGENTS.<lang>.md. If this is generated/data code, add an entry to .claude/rules/loc-exceptions.txt with rationale and reference [guardrail-change].\"}'; exit 0; fi",
|
||||
"shell": "bash",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] || [ ! -f \"$f\" ] && exit 0; case \"$f\" in *.md|*.json|*.yaml|*.yml|*test*|*tests/*|*node_modules/*|*.next/*|*migrations/*) exit 0 ;; esac; new_str=$(jq -r '.tool_input.new_string // empty'); old_str=$(jq -r '.tool_input.old_string // empty'); old_lines=$(printf '%s' \"$old_str\" | awk 'END{print NR}'); new_lines=$(printf '%s' \"$new_str\" | awk 'END{print NR}'); cur=$(wc -l < \"$f\" | tr -d ' '); proj=$((cur - old_lines + new_lines)); if [ \"$proj\" -gt 500 ]; then echo \"{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"breakpilot guardrail: this edit would push $f to ~$proj lines (hard cap is 500). Split the file before continuing. See AGENTS.<lang>.md for the layering rules.\\\"}\"; fi; exit 0",
|
||||
"shell": "bash",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
61
.env.coolify.example
Normal file
61
.env.coolify.example
Normal file
@@ -0,0 +1,61 @@
|
||||
# =========================================================
|
||||
# BreakPilot Compliance — Coolify Environment Variables
|
||||
# =========================================================
|
||||
# Copy these into Coolify's environment variable UI
|
||||
# for the breakpilot-compliance Docker Compose resource.
|
||||
# =========================================================
|
||||
|
||||
# --- External PostgreSQL (Coolify-managed, same as Core) ---
|
||||
COMPLIANCE_DATABASE_URL=postgresql://breakpilot:CHANGE_ME@<coolify-postgres-hostname>:5432/breakpilot_db
|
||||
|
||||
# --- Security ---
|
||||
JWT_SECRET=CHANGE_ME_SAME_AS_CORE
|
||||
|
||||
# --- External S3 Storage (same as Core) ---
|
||||
S3_ENDPOINT=<s3-endpoint-host:port>
|
||||
S3_ACCESS_KEY=CHANGE_ME_SAME_AS_CORE
|
||||
S3_SECRET_KEY=CHANGE_ME_SAME_AS_CORE
|
||||
S3_SECURE=true
|
||||
|
||||
# --- External Qdrant ---
|
||||
QDRANT_URL=https://<qdrant-hostname>
|
||||
QDRANT_API_KEY=CHANGE_ME_QDRANT_API_KEY
|
||||
|
||||
# --- Session ---
|
||||
SESSION_TTL_HOURS=24
|
||||
|
||||
# --- SMTP (Real mail server) ---
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=compliance@breakpilot.ai
|
||||
SMTP_PASSWORD=CHANGE_ME_SMTP_PASSWORD
|
||||
SMTP_FROM_NAME=BreakPilot Compliance
|
||||
SMTP_FROM_ADDR=compliance@breakpilot.ai
|
||||
|
||||
# --- LLM Configuration ---
|
||||
COMPLIANCE_LLM_PROVIDER=anthropic
|
||||
SELF_HOSTED_LLM_URL=
|
||||
SELF_HOSTED_LLM_MODEL=
|
||||
COMPLIANCE_LLM_MAX_TOKENS=4096
|
||||
COMPLIANCE_LLM_TEMPERATURE=0.3
|
||||
COMPLIANCE_LLM_TIMEOUT=120
|
||||
ANTHROPIC_API_KEY=CHANGE_ME_ANTHROPIC_KEY
|
||||
ANTHROPIC_DEFAULT_MODEL=claude-sonnet-4-5-20250929
|
||||
|
||||
# --- Ollama (optional) ---
|
||||
OLLAMA_URL=
|
||||
OLLAMA_DEFAULT_MODEL=
|
||||
COMPLIANCE_LLM_MODEL=
|
||||
|
||||
# --- LLM Fallback ---
|
||||
LLM_FALLBACK_PROVIDER=
|
||||
|
||||
# --- PII & Audit ---
|
||||
PII_REDACTION_ENABLED=true
|
||||
PII_REDACTION_LEVEL=standard
|
||||
AUDIT_RETENTION_DAYS=365
|
||||
AUDIT_LOG_PROMPTS=true
|
||||
|
||||
# --- Frontend URLs (build args) ---
|
||||
NEXT_PUBLIC_API_URL=https://api-compliance.breakpilot.ai
|
||||
NEXT_PUBLIC_SDK_URL=https://sdk.breakpilot.ai
|
||||
@@ -7,7 +7,7 @@
|
||||
# Node.js: admin-compliance, developer-portal
|
||||
#
|
||||
# Workflow:
|
||||
# Push auf main → Tests → Build → Deploy (Hetzner)
|
||||
# Push auf main → Tests → Deploy (Coolify)
|
||||
# Pull Request → Lint + Tests (kein Deploy)
|
||||
|
||||
name: CI/CD
|
||||
@@ -19,6 +19,55 @@ on:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
# ========================================
|
||||
# Guardrails — LOC budget + architecture gates
|
||||
# Runs on every push/PR. Fails fast and cheap.
|
||||
# ========================================
|
||||
|
||||
loc-budget:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Enforce 500-line hard cap on changed files
|
||||
run: |
|
||||
chmod +x scripts/check-loc.sh
|
||||
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
|
||||
git fetch origin ${GITHUB_BASE_REF}:base
|
||||
mapfile -t changed < <(git diff --name-only --diff-filter=ACM base...HEAD)
|
||||
[ ${#changed[@]} -eq 0 ] && { echo "No changed files."; exit 0; }
|
||||
scripts/check-loc.sh "${changed[@]}"
|
||||
else
|
||||
# Push to main: only warn on whole-repo state; blocking gate is on PRs.
|
||||
scripts/check-loc.sh || true
|
||||
fi
|
||||
# Phase 0 intentionally gates only changed files so the 205-file legacy
|
||||
# baseline doesn't block every PR. Phases 1-4 drain the baseline; Phase 5
|
||||
# flips this to a whole-repo blocking gate.
|
||||
|
||||
guardrail-integrity:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 20 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git fetch origin ${GITHUB_BASE_REF}:base
|
||||
- name: Require [guardrail-change] label in PR commits touching guardrails
|
||||
run: |
|
||||
changed=$(git diff --name-only base...HEAD)
|
||||
echo "$changed" | grep -E '^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' || exit 0
|
||||
if ! git log base..HEAD --format=%B | grep -q '\[guardrail-change\]'; then
|
||||
echo "::error:: Guardrail files were modified but no commit in this PR carries [guardrail-change]."
|
||||
echo "If intentional, amend one commit message with [guardrail-change] and explain why in the body."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Lint (nur bei PRs)
|
||||
# ========================================
|
||||
@@ -47,15 +96,28 @@ jobs:
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq git > /dev/null 2>&1
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Lint Python services
|
||||
- name: Lint Python services (ruff)
|
||||
run: |
|
||||
pip install --quiet ruff
|
||||
for svc in backend-compliance document-crawler dsms-gateway; do
|
||||
fail=0
|
||||
for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do
|
||||
if [ -d "$svc" ]; then
|
||||
echo "=== Linting $svc ==="
|
||||
ruff check "$svc/" --output-format=github || true
|
||||
echo "=== ruff: $svc ==="
|
||||
ruff check "$svc/" --output-format=github || fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
- name: Type-check (mypy via backend-compliance/mypy.ini)
|
||||
# Policy is declared in backend-compliance/mypy.ini: strict mode globally,
|
||||
# with per-module overrides for legacy utility services, the SQLAlchemy
|
||||
# ORM layer, and yet-unrefactored route files. Each Phase 1 Step 4
|
||||
# refactor flips a route file from loose->strict via its own mypy.ini
|
||||
# override block.
|
||||
run: |
|
||||
pip install --quiet mypy
|
||||
if [ -f "backend-compliance/mypy.ini" ]; then
|
||||
cd backend-compliance && mypy compliance/
|
||||
fi
|
||||
|
||||
nodejs-lint:
|
||||
runs-on: docker
|
||||
@@ -66,17 +128,20 @@ jobs:
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Lint Node.js services
|
||||
- name: Lint + type-check Node.js services
|
||||
run: |
|
||||
fail=0
|
||||
for svc in admin-compliance developer-portal; do
|
||||
if [ -d "$svc" ]; then
|
||||
echo "=== Linting $svc ==="
|
||||
cd "$svc"
|
||||
npm ci --silent 2>/dev/null || npm install --silent
|
||||
npx next lint || true
|
||||
cd ..
|
||||
echo "=== $svc: install ==="
|
||||
(cd "$svc" && (npm ci --silent 2>/dev/null || npm install --silent))
|
||||
echo "=== $svc: next lint ==="
|
||||
(cd "$svc" && npx next lint) || fail=1
|
||||
echo "=== $svc: tsc --noEmit ==="
|
||||
(cd "$svc" && npx tsc --noEmit) || fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
|
||||
# ========================================
|
||||
# Unit Tests
|
||||
@@ -169,6 +234,32 @@ jobs:
|
||||
pip install --quiet --no-cache-dir pytest pytest-asyncio
|
||||
python -m pytest test_main.py -v --tb=short
|
||||
|
||||
# ========================================
|
||||
# SBOM + license scan (compliance product → we eat our own dog food)
|
||||
# ========================================
|
||||
|
||||
sbom-scan:
|
||||
runs-on: docker
|
||||
if: github.event_name == 'pull_request'
|
||||
container: alpine:3.20
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git curl bash
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Install syft + grype
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
mkdir -p sbom-out
|
||||
syft dir:. -o cyclonedx-json=sbom-out/sbom.cdx.json -q
|
||||
- name: Vulnerability scan (fail on high+)
|
||||
run: |
|
||||
grype sbom:sbom-out/sbom.cdx.json --fail-on high -q || true
|
||||
# Initially non-blocking ('|| true'). Flip to blocking after baseline is clean.
|
||||
|
||||
# ========================================
|
||||
# Validate Canonical Controls
|
||||
# ========================================
|
||||
@@ -186,104 +277,25 @@ jobs:
|
||||
python scripts/validate-controls.py
|
||||
|
||||
# ========================================
|
||||
# Build & Deploy auf Hetzner (nur main, kein PR)
|
||||
# Deploy via Coolify (nur main, kein PR)
|
||||
# ========================================
|
||||
|
||||
deploy-hetzner:
|
||||
deploy-coolify:
|
||||
name: Deploy
|
||||
runs-on: docker
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- loc-budget
|
||||
- test-go-ai-compliance
|
||||
- test-python-backend-compliance
|
||||
- test-python-document-crawler
|
||||
- test-python-dsms-gateway
|
||||
- validate-canonical-controls
|
||||
container: docker:27-cli
|
||||
container:
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Deploy
|
||||
- name: Trigger Coolify deploy
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DEPLOY_DIR="/opt/breakpilot-compliance"
|
||||
COMPOSE_FILES="-f docker-compose.yml -f docker-compose.hetzner.yml"
|
||||
COMMIT_SHA="${GITHUB_SHA:-unknown}"
|
||||
SHORT_SHA="${COMMIT_SHA:0:8}"
|
||||
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
|
||||
echo "=== BreakPilot Compliance Deploy ==="
|
||||
echo "Commit: ${SHORT_SHA}"
|
||||
echo "Deploy Dir: ${DEPLOY_DIR}"
|
||||
echo ""
|
||||
|
||||
# Der Runner laeuft in einem Container mit Docker-Socket-Zugriff,
|
||||
# hat aber KEINEN direkten Zugriff auf das Host-Dateisystem.
|
||||
# Loesung: Alpine-Helper-Container mit Host-Bind-Mount fuer Git-Ops.
|
||||
|
||||
# 1. Repo auf dem Host erstellen/aktualisieren via Helper-Container
|
||||
echo "=== Updating code on host ==="
|
||||
docker run --rm \
|
||||
-v "${DEPLOY_DIR}:${DEPLOY_DIR}" \
|
||||
--entrypoint sh \
|
||||
alpine/git:latest \
|
||||
-c "
|
||||
if [ ! -d '${DEPLOY_DIR}/.git' ]; then
|
||||
echo 'Erstmaliges Klonen nach ${DEPLOY_DIR}...'
|
||||
git clone '${REPO_URL}' '${DEPLOY_DIR}'
|
||||
else
|
||||
cd '${DEPLOY_DIR}'
|
||||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
fi
|
||||
"
|
||||
echo "Code aktualisiert auf ${SHORT_SHA}"
|
||||
|
||||
# 2. .env sicherstellen (muss einmalig manuell angelegt werden)
|
||||
docker run --rm -v "${DEPLOY_DIR}:${DEPLOY_DIR}" alpine \
|
||||
sh -c "
|
||||
if [ ! -f '${DEPLOY_DIR}/.env' ]; then
|
||||
echo 'WARNUNG: ${DEPLOY_DIR}/.env fehlt!'
|
||||
echo 'Bitte einmalig auf dem Host anlegen.'
|
||||
echo 'Deploy wird fortgesetzt (Services starten ggf. mit Defaults).'
|
||||
else
|
||||
echo '.env vorhanden'
|
||||
fi
|
||||
"
|
||||
|
||||
# 3. Build + Deploy via Helper-Container mit Docker-Socket + Deploy-Dir
|
||||
# docker compose muss die YAML-Dateien lesen koennen, daher
|
||||
# alles in einem Container mit beiden Mounts ausfuehren.
|
||||
echo ""
|
||||
echo "=== Building + Deploying ==="
|
||||
docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "${DEPLOY_DIR}:${DEPLOY_DIR}" \
|
||||
-w "${DEPLOY_DIR}" \
|
||||
docker:27-cli \
|
||||
sh -c "
|
||||
COMPOSE_FILES='-f docker-compose.yml -f docker-compose.hetzner.yml'
|
||||
|
||||
echo '=== Building Docker Images ==='
|
||||
docker compose \${COMPOSE_FILES} build --parallel \
|
||||
admin-compliance \
|
||||
backend-compliance \
|
||||
ai-compliance-sdk \
|
||||
developer-portal
|
||||
|
||||
echo ''
|
||||
echo '=== Starting containers ==='
|
||||
docker compose \${COMPOSE_FILES} up -d --remove-orphans \
|
||||
admin-compliance \
|
||||
backend-compliance \
|
||||
ai-compliance-sdk \
|
||||
developer-portal
|
||||
|
||||
echo ''
|
||||
echo '=== Health Checks ==='
|
||||
sleep 10
|
||||
for svc in bp-compliance-admin bp-compliance-backend bp-compliance-ai-sdk bp-compliance-developer-portal; do
|
||||
STATUS=\$(docker inspect --format='{{.State.Status}}' \"\${svc}\" 2>/dev/null || echo 'not found')
|
||||
echo \"\${svc}: \${STATUS}\"
|
||||
done
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "=== Deploy abgeschlossen: ${SHORT_SHA} ==="
|
||||
apk add --no-cache curl
|
||||
curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||
|
||||
126
AGENTS.go.md
Normal file
126
AGENTS.go.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# AGENTS.go.md — Go Service Conventions
|
||||
|
||||
Applies to: `ai-compliance-sdk/`.
|
||||
|
||||
## Layered architecture (Gin)
|
||||
|
||||
Follows [Standard Go Project Layout](https://github.com/golang-standards/project-layout) + hexagonal/clean-arch.
|
||||
|
||||
```
|
||||
ai-compliance-sdk/
|
||||
├── cmd/server/main.go # Thin: parse flags → app.New → app.Run. <50 LOC.
|
||||
├── internal/
|
||||
│ ├── app/ # Wiring: config + DI graph + lifecycle.
|
||||
│ ├── domain/ # Pure types, interfaces, errors. No I/O imports.
|
||||
│ │ └── <aggregate>/
|
||||
│ ├── service/ # Business logic. Depends on domain interfaces only.
|
||||
│ │ └── <aggregate>/
|
||||
│ ├── repository/postgres/ # Concrete repo implementations.
|
||||
│ │ └── <aggregate>/
|
||||
│ ├── transport/http/ # Gin handlers. Thin. One handler per file group.
|
||||
│ │ ├── handler/<aggregate>/
|
||||
│ │ ├── middleware/
|
||||
│ │ └── router.go
|
||||
│ └── platform/ # DB pool, logger, config, tracing.
|
||||
└── pkg/ # Importable by other repos. Empty unless needed.
|
||||
```
|
||||
|
||||
**Dependency direction:** `transport → service → domain ← repository`. `domain` imports nothing from siblings.
|
||||
|
||||
## Handlers
|
||||
|
||||
- One handler = one Gin function. ≤40 LOC.
|
||||
- Bind → call service → map domain error to HTTP via `httperr.Write(c, err)` → respond.
|
||||
- Return early on errors. No business logic, no SQL.
|
||||
|
||||
```go
|
||||
func (h *IACEHandler) Create(c *gin.Context) {
|
||||
var req CreateIACERequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httperr.Write(c, httperr.BadRequest(err))
|
||||
return
|
||||
}
|
||||
out, err := h.svc.Create(c.Request.Context(), req.ToInput())
|
||||
if err != nil {
|
||||
httperr.Write(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, out)
|
||||
}
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
- Struct + constructor + interface methods. No package-level state.
|
||||
- Take `context.Context` as first arg always. Propagate to repos.
|
||||
- Return `(value, error)`. Wrap with `fmt.Errorf("create iace: %w", err)`.
|
||||
- Domain errors implemented as sentinel vars or typed errors; matched with `errors.Is` / `errors.As`.
|
||||
|
||||
## Repositories
|
||||
|
||||
- Interface lives in `domain/<aggregate>/repository.go`. Implementation in `repository/postgres/<aggregate>/`.
|
||||
- One file per query group; no file >500 LOC.
|
||||
- Use `pgx`/`sqlc` over hand-rolled string SQL when feasible. No ORM globals.
|
||||
- All queries take `ctx`. No background goroutines without explicit lifecycle.
|
||||
|
||||
## Errors
|
||||
|
||||
Single `internal/platform/httperr` package maps `error` → HTTP status:
|
||||
|
||||
```go
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNotFound): return 404
|
||||
case errors.Is(err, domain.ErrConflict): return 409
|
||||
case errors.As(err, &validationErr): return 422
|
||||
default: return 500
|
||||
}
|
||||
```
|
||||
|
||||
Never `panic` in request handling. `recover` middleware logs and returns 500.
|
||||
|
||||
## Tests
|
||||
|
||||
- Co-located `*_test.go`.
|
||||
- **Table-driven** tests for service logic; use `t.Run(tt.name, ...)`.
|
||||
- Handlers tested with `httptest.NewRecorder`.
|
||||
- Repos tested with `testcontainers-go` (or the existing compose Postgres) — never mocks at the SQL boundary.
|
||||
- Coverage target: 80% on `service/`. CI fails on regression.
|
||||
|
||||
```go
|
||||
func TestIACEService_Create(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input service.CreateInput
|
||||
setup func(*mockRepo)
|
||||
wantErr error
|
||||
}{
|
||||
{"happy path", validInput(), func(r *mockRepo) { r.createReturns(nil) }, nil},
|
||||
{"conflict", validInput(), func(r *mockRepo) { r.createReturns(domain.ErrConflict) }, domain.ErrConflict},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) { /* ... */ })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tooling
|
||||
|
||||
- `golangci-lint` with: `errcheck, govet, staticcheck, revive, gosec, gocyclo (max 15), gocognit (max 20), unused, ineffassign, errorlint, nilerr, nolintlint, contextcheck`.
|
||||
- `gofumpt` formatting.
|
||||
- `go vet ./...` clean.
|
||||
- `go mod tidy` clean — no unused deps.
|
||||
|
||||
## Concurrency
|
||||
|
||||
- Goroutines must have a clear lifecycle owner (struct method that started them must stop them).
|
||||
- Pass `ctx` everywhere. Cancellation respected.
|
||||
- No global mutexes for request data. Use per-request context.
|
||||
|
||||
## What you may NOT do
|
||||
|
||||
- Touch DB schema/migrations.
|
||||
- Add a new top-level package directly under `internal/` without architectural review.
|
||||
- `import "C"`, unsafe, reflection-heavy code.
|
||||
- Use `init()` for non-trivial setup. Wire it in `internal/app`.
|
||||
- Create a file >500 lines.
|
||||
- Change a public route's contract without updating consumers.
|
||||
94
AGENTS.python.md
Normal file
94
AGENTS.python.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# AGENTS.python.md — Python Service Conventions
|
||||
|
||||
Applies to: `backend-compliance/`, `document-crawler/`, `dsms-gateway/`, `compliance-tts-service/`.
|
||||
|
||||
## Layered architecture (FastAPI)
|
||||
|
||||
```
|
||||
compliance/
|
||||
├── api/ # HTTP layer — routers only. Thin (≤30 LOC per handler).
|
||||
│ └── <domain>_routes.py
|
||||
├── services/ # Business logic. Pure-ish; no FastAPI imports.
|
||||
│ └── <domain>_service.py
|
||||
├── repositories/ # DB access. Owns SQLAlchemy session usage.
|
||||
│ └── <domain>_repository.py
|
||||
├── domain/ # Value objects, enums, domain exceptions.
|
||||
├── schemas/ # Pydantic models, split per domain. NEVER one giant schemas.py.
|
||||
│ └── <domain>.py
|
||||
└── db/
|
||||
└── models/ # SQLAlchemy ORM, one module per aggregate. __tablename__ frozen.
|
||||
```
|
||||
|
||||
**Dependency direction:** `api → services → repositories → db.models`. Lower layers must not import upper layers.
|
||||
|
||||
## Routers
|
||||
|
||||
- One `APIRouter` per domain file.
|
||||
- Handlers do exactly: parse request → call service → map domain errors to HTTPException → return response model.
|
||||
- Inject services via `Depends`. No globals.
|
||||
- Tag routes; document with summary + response_model.
|
||||
|
||||
```python
|
||||
@router.post("/dsr/requests", response_model=DSRRequestRead, status_code=201)
|
||||
async def create_dsr_request(
|
||||
payload: DSRRequestCreate,
|
||||
service: DSRService = Depends(get_dsr_service),
|
||||
tenant_id: UUID = Depends(get_tenant_id),
|
||||
) -> DSRRequestRead:
|
||||
try:
|
||||
return await service.create(tenant_id, payload)
|
||||
except DSRConflict as exc:
|
||||
raise HTTPException(409, str(exc)) from exc
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
- Constructor takes the repository (interface, not concrete).
|
||||
- No `Request`, `Response`, or HTTP knowledge.
|
||||
- Raise domain exceptions (e.g. `DSRConflict`, `DSRNotFound`), never `HTTPException`.
|
||||
- Return domain objects or Pydantic schemas — pick one and stay consistent inside a service.
|
||||
|
||||
## Repositories
|
||||
|
||||
- Methods are intent-named (`get_pending_for_tenant`), not CRUD-named (`select_where`).
|
||||
- Sessions injected, not constructed inside.
|
||||
- No business logic. No cross-aggregate joins for unrelated workflows — that belongs in a service.
|
||||
- Return ORM models or domain VOs; never `Row`.
|
||||
|
||||
## Schemas (Pydantic v2)
|
||||
|
||||
- One module per domain. Module ≤300 lines.
|
||||
- Use `model_config = ConfigDict(from_attributes=True, frozen=True)` for read models.
|
||||
- Separate `*Create`, `*Update`, `*Read`. No giant union schemas.
|
||||
|
||||
## Tests (`pytest`)
|
||||
|
||||
- Layout: `tests/unit/`, `tests/integration/`, `tests/contracts/`.
|
||||
- Unit tests mock the repository. Use `pytest.fixture` + `unittest.mock.AsyncMock`.
|
||||
- Integration tests run against the real Postgres from `docker-compose.yml` via a transactional fixture (rollback after each test).
|
||||
- Contract tests diff `/openapi.json` against `tests/contracts/openapi.baseline.json`.
|
||||
- Naming: `test_<unit>_<scenario>_<expected>.py::TestClass::test_method`.
|
||||
- `pytest-asyncio` mode = `auto`. Mark slow tests with `@pytest.mark.slow`.
|
||||
- Coverage target: 80% for new code; never decrease the service baseline.
|
||||
|
||||
## Tooling
|
||||
|
||||
- `ruff check` + `ruff format` (line length 100).
|
||||
- `mypy --strict` on `services/`, `repositories/`, `domain/`. Expand outward.
|
||||
- `pip-audit` in CI.
|
||||
- Async-first: prefer `httpx.AsyncClient`, `asyncpg`/`SQLAlchemy 2.x async`.
|
||||
|
||||
## Errors & logging
|
||||
|
||||
- Domain errors inherit from a single `DomainError` base per service.
|
||||
- Log via `structlog` with bound context (`tenant_id`, `request_id`). Never log secrets, PII, or full request bodies.
|
||||
- Audit-relevant actions go through the audit logger, not the application logger.
|
||||
|
||||
## What you may NOT do
|
||||
|
||||
- Add a new Alembic migration.
|
||||
- Rename a `__tablename__`, column, or enum value.
|
||||
- Change a public route's path/method/status/schema without simultaneous dashboard fix.
|
||||
- Catch `Exception` broadly — catch the specific domain or library error.
|
||||
- Put business logic in a router or in a Pydantic validator.
|
||||
- Create a new file >500 lines. Period.
|
||||
85
AGENTS.typescript.md
Normal file
85
AGENTS.typescript.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# AGENTS.typescript.md — TypeScript / Next.js Conventions
|
||||
|
||||
Applies to: `admin-compliance/`, `developer-portal/`, `breakpilot-compliance-sdk/`, `consent-sdk/`, `dsms-node/` (where applicable).
|
||||
|
||||
## Layered architecture (Next.js 15 App Router)
|
||||
|
||||
```
|
||||
app/
|
||||
├── <route>/
|
||||
│ ├── page.tsx # Server Component by default. ≤200 LOC.
|
||||
│ ├── layout.tsx
|
||||
│ ├── _components/ # Private folder; not routable. Colocated UI.
|
||||
│ │ └── <Component>.tsx # Each file ≤300 LOC.
|
||||
│ ├── _hooks/ # Client hooks for this route.
|
||||
│ ├── _server/ # Server actions, data loaders for this route.
|
||||
│ └── loading.tsx / error.tsx
|
||||
├── api/
|
||||
│ └── <domain>/route.ts # Thin handler. Delegates to lib/server/<domain>/.
|
||||
lib/
|
||||
├── <domain>/ # Pure helpers, types, schemas (zod). Reusable.
|
||||
└── server/<domain>/ # Server-only logic; uses "server-only" import.
|
||||
components/ # Truly shared, app-wide components.
|
||||
```
|
||||
|
||||
**Server vs Client:** Default is Server Component. Add `"use client"` only when you need state, effects, or browser APIs. Push the boundary as deep as possible.
|
||||
|
||||
## API routes (route.ts)
|
||||
|
||||
- One handler per HTTP method, ≤40 LOC.
|
||||
- Validate input with `zod`. Reject invalid → 400.
|
||||
- Delegate to `lib/server/<domain>/`. No business logic in `route.ts`.
|
||||
- Always return `NextResponse.json(..., { status })`. Never throw to the framework.
|
||||
|
||||
```ts
|
||||
export async function POST(req: Request) {
|
||||
const parsed = CreateDSRSchema.safeParse(await req.json());
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||
const result = await dsrService.create(parsed.data);
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
## Page components
|
||||
|
||||
- Pages >300 lines must be split into colocated `_components/`.
|
||||
- Server Components fetch data; pass plain objects to Client Components.
|
||||
- No data fetching in `useEffect` for server-renderable data.
|
||||
- State management: prefer URL state (`searchParams`) and Server Components over global stores.
|
||||
|
||||
## Types
|
||||
|
||||
- `lib/sdk/types.ts` is being split into `lib/sdk/types/<domain>.ts`. Mirror backend domain boundaries.
|
||||
- All API DTOs are zod schemas; infer types via `z.infer`.
|
||||
- No `any`. No `as unknown as`. If you reach for it, the type is wrong.
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit: **Vitest** (`*.test.ts`/`*.test.tsx`), colocated.
|
||||
- Hooks: `@testing-library/react` `renderHook`.
|
||||
- E2E: **Playwright** (`tests/e2e/`), one spec per top-level page, smoke happy path minimum.
|
||||
- Snapshot tests sparingly — only for stable output (CSV, JSON-LD).
|
||||
- Coverage target: 70% on `lib/`, smoke coverage on `app/`.
|
||||
|
||||
## Tooling
|
||||
|
||||
- `tsc --noEmit` clean (strict mode, `noUncheckedIndexedAccess: true`).
|
||||
- ESLint with `@typescript-eslint`, `eslint-config-next`, type-aware rules on.
|
||||
- `prettier`.
|
||||
- `next build` clean. No `// @ts-ignore`. `// @ts-expect-error` only with a comment explaining why.
|
||||
|
||||
## Performance
|
||||
|
||||
- Use `next/dynamic` for heavy client-only components.
|
||||
- Image: `next/image` with explicit width/height.
|
||||
- Avoid waterfalls — `Promise.all` for parallel data fetches in Server Components.
|
||||
|
||||
## What you may NOT do
|
||||
|
||||
- Put business logic in a `page.tsx` or `route.ts`.
|
||||
- Reach across module boundaries (e.g. `admin-compliance` importing from `developer-portal`).
|
||||
- Use `dangerouslySetInnerHTML` without explicit sanitization.
|
||||
- Call backend APIs directly from Client Components when a Server Component or Server Action would do.
|
||||
- Change a public API route's path/method/schema without updating SDK consumers in the same change.
|
||||
- Create a file >500 lines.
|
||||
- Disable a lint or type rule globally to silence a finding — fix the root cause.
|
||||
@@ -37,8 +37,8 @@ WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
RUN addgroup -S -g 1001 nodejs
|
||||
RUN adduser -S -u 1001 -G nodejs nextjs
|
||||
|
||||
# Copy built assets
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
51
admin-compliance/README.md
Normal file
51
admin-compliance/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# admin-compliance
|
||||
|
||||
Next.js 15 dashboard for BreakPilot Compliance — SDK module UI, company profile, DSR, DSFA, VVT, TOM, consent, AI Act, training, audit, change requests, etc. Also hosts 96+ API routes that proxy/orchestrate backend services.
|
||||
|
||||
**Port:** `3007` (container: `bp-compliance-admin`)
|
||||
**Stack:** Next.js 15 App Router, React 18, TailwindCSS, TypeScript strict.
|
||||
|
||||
## Architecture (target — Phase 3)
|
||||
|
||||
```
|
||||
app/
|
||||
├── <route>/
|
||||
│ ├── page.tsx # Server Component (≤200 LOC)
|
||||
│ ├── _components/ # Colocated UI, each ≤300 LOC
|
||||
│ ├── _hooks/ # Client hooks
|
||||
│ └── _server/ # Server actions
|
||||
├── api/<domain>/route.ts # Thin handlers → lib/server/<domain>/
|
||||
lib/
|
||||
├── <domain>/ # Pure helpers, zod schemas
|
||||
└── server/<domain>/ # "server-only" logic
|
||||
components/ # App-wide shared UI
|
||||
```
|
||||
|
||||
See `../AGENTS.typescript.md`.
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
cd admin-compliance
|
||||
npm install
|
||||
npm run dev # http://localhost:3007
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
npm test # Vitest unit + component tests
|
||||
npx playwright test # E2E
|
||||
npx tsc --noEmit # Type-check
|
||||
npx next lint
|
||||
```
|
||||
|
||||
## Known debt (Phase 3 targets)
|
||||
|
||||
- `app/sdk/company-profile/page.tsx` (3017 LOC), `tom-generator/controls/loader.ts` (2521), `lib/sdk/types.ts` (2511), `app/sdk/loeschfristen/page.tsx` (2322), `app/sdk/dsb-portal/page.tsx` (2068) — all must be split.
|
||||
- 0 test files for 182 monolithic pages. Phase 3 adds Playwright smoke + Vitest unit coverage.
|
||||
|
||||
## Don't touch
|
||||
|
||||
- Backend API paths without updating `backend-compliance/` in the same change.
|
||||
- `lib/sdk/types.ts` in large contiguous chunks — it's being domain-split.
|
||||
385
admin-compliance/lib/sdk/academy/api-courses.ts
Normal file
385
admin-compliance/lib/sdk/academy/api-courses.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Academy API Client — Course, Enrollment, Certificate, Quiz, Statistics, Generation
|
||||
*
|
||||
* API client for the Compliance E-Learning Academy module
|
||||
* Connects to the ai-compliance-sdk backend via Next.js proxy
|
||||
*/
|
||||
|
||||
import type {
|
||||
Course,
|
||||
CourseCategory,
|
||||
CourseCreateRequest,
|
||||
CourseUpdateRequest,
|
||||
GenerateCourseRequest,
|
||||
Enrollment,
|
||||
EnrollmentStatus,
|
||||
EnrollmentListResponse,
|
||||
EnrollUserRequest,
|
||||
UpdateProgressRequest,
|
||||
Certificate,
|
||||
AcademyStatistics,
|
||||
LessonType,
|
||||
SubmitQuizRequest,
|
||||
SubmitQuizResponse,
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const ACADEMY_API_BASE = '/api/sdk/v1/academy'
|
||||
const API_TIMEOUT = 30000
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getTenantId(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
||||
}
|
||||
return 'default-tenant'
|
||||
}
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': getTenantId()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
const userId = localStorage.getItem('bp_user_id')
|
||||
if (userId) {
|
||||
headers['X-User-ID'] = userId
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async function fetchWithTimeout<T>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
timeout: number = API_TIMEOUT
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep the HTTP status message
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {} as T
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BACKEND TYPES
|
||||
// =============================================================================
|
||||
|
||||
interface BackendCourse {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: CourseCategory
|
||||
duration_minutes: number
|
||||
required_for_roles: string[]
|
||||
is_active: boolean
|
||||
passing_score?: number
|
||||
status?: string
|
||||
lessons?: BackendLesson[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface BackendQuizQuestion {
|
||||
id: string
|
||||
question: string
|
||||
options: string[]
|
||||
correct_index: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface BackendLesson {
|
||||
id: string
|
||||
course_id: string
|
||||
title: string
|
||||
description?: string
|
||||
lesson_type: LessonType
|
||||
content_url?: string
|
||||
duration_minutes: number
|
||||
order_index: number
|
||||
quiz_questions?: BackendQuizQuestion[]
|
||||
}
|
||||
|
||||
function mapCourseFromBackend(bc: BackendCourse): Course {
|
||||
return {
|
||||
id: bc.id,
|
||||
title: bc.title,
|
||||
description: bc.description || '',
|
||||
category: bc.category,
|
||||
durationMinutes: bc.duration_minutes || 0,
|
||||
passingScore: bc.passing_score ?? 70,
|
||||
isActive: bc.is_active ?? true,
|
||||
status: (bc.status as 'draft' | 'published') ?? 'draft',
|
||||
requiredForRoles: bc.required_for_roles || [],
|
||||
lessons: (bc.lessons || []).map(l => ({
|
||||
id: l.id,
|
||||
courseId: l.course_id,
|
||||
title: l.title,
|
||||
type: l.lesson_type,
|
||||
contentMarkdown: l.content_url || '',
|
||||
durationMinutes: l.duration_minutes || 0,
|
||||
order: l.order_index,
|
||||
quizQuestions: (l.quiz_questions || []).map(q => ({
|
||||
id: q.id || `q-${Math.random().toString(36).slice(2)}`,
|
||||
lessonId: l.id,
|
||||
question: q.question,
|
||||
options: q.options,
|
||||
correctOptionIndex: q.correct_index,
|
||||
explanation: q.explanation,
|
||||
})),
|
||||
})),
|
||||
createdAt: bc.created_at,
|
||||
updatedAt: bc.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
|
||||
return courses.map(mapCourseFromBackend)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COURSE CRUD
|
||||
// =============================================================================
|
||||
|
||||
export async function fetchCourses(): Promise<Course[]> {
|
||||
const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
|
||||
`${ACADEMY_API_BASE}/courses`
|
||||
)
|
||||
return mapCoursesFromBackend(res.courses || [])
|
||||
}
|
||||
|
||||
export async function fetchCourse(id: string): Promise<Course> {
|
||||
const res = await fetchWithTimeout<{ course: BackendCourse }>(
|
||||
`${ACADEMY_API_BASE}/courses/${id}`
|
||||
)
|
||||
return mapCourseFromBackend(res.course)
|
||||
}
|
||||
|
||||
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/courses`,
|
||||
{ method: 'POST', body: JSON.stringify(request) }
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/courses/${id}`,
|
||||
{ method: 'PUT', body: JSON.stringify(update) }
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteCourse(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${ACADEMY_API_BASE}/courses/${id}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENROLLMENTS
|
||||
// =============================================================================
|
||||
|
||||
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (courseId) {
|
||||
params.set('course_id', courseId)
|
||||
}
|
||||
const queryString = params.toString()
|
||||
const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url)
|
||||
return res.enrollments || []
|
||||
}
|
||||
|
||||
export async function enrollUser(request: EnrollUserRequest): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments`,
|
||||
{ method: 'POST', body: JSON.stringify(request) }
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
|
||||
{ method: 'PUT', body: JSON.stringify(update) }
|
||||
)
|
||||
}
|
||||
|
||||
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
|
||||
{ method: 'POST' }
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteEnrollment(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${id}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateEnrollment(id: string, data: { deadline?: string }): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${id}`,
|
||||
{ method: 'PUT', body: JSON.stringify(data) }
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CERTIFICATES
|
||||
// =============================================================================
|
||||
|
||||
export async function fetchCertificate(id: string): Promise<Certificate> {
|
||||
return fetchWithTimeout<Certificate>(
|
||||
`${ACADEMY_API_BASE}/certificates/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
|
||||
return fetchWithTimeout<Certificate>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
|
||||
{ method: 'POST' }
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchCertificates(): Promise<Certificate[]> {
|
||||
const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>(
|
||||
`${ACADEMY_API_BASE}/certificates`
|
||||
)
|
||||
return res.certificates || []
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUIZ
|
||||
// =============================================================================
|
||||
|
||||
export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise<SubmitQuizResponse> {
|
||||
return fetchWithTimeout<SubmitQuizResponse>(
|
||||
`${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`,
|
||||
{ method: 'POST', body: JSON.stringify(answers) }
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateLesson(lessonId: string, update: {
|
||||
title?: string
|
||||
description?: string
|
||||
content_url?: string
|
||||
duration_minutes?: number
|
||||
quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }>
|
||||
}): Promise<{ lesson: any }> {
|
||||
return fetchWithTimeout(
|
||||
`${ACADEMY_API_BASE}/lessons/${lessonId}`,
|
||||
{ method: 'PUT', body: JSON.stringify(update) }
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
|
||||
const res = await fetchWithTimeout<{
|
||||
total_courses: number
|
||||
total_enrollments: number
|
||||
completion_rate: number
|
||||
overdue_count: number
|
||||
avg_completion_days: number
|
||||
by_category?: Record<string, number>
|
||||
by_status?: Record<string, number>
|
||||
}>(`${ACADEMY_API_BASE}/stats`)
|
||||
|
||||
return {
|
||||
totalCourses: res.total_courses || 0,
|
||||
totalEnrollments: res.total_enrollments || 0,
|
||||
completionRate: res.completion_rate || 0,
|
||||
overdueCount: res.overdue_count || 0,
|
||||
byCategory: (res.by_category || {}) as Record<CourseCategory, number>,
|
||||
byStatus: (res.by_status || {}) as Record<EnrollmentStatus, number>,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COURSE GENERATION
|
||||
// =============================================================================
|
||||
|
||||
export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> {
|
||||
return fetchWithTimeout<{ course: Course }>(
|
||||
`${ACADEMY_API_BASE}/courses/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ module_id: request.moduleId || request.title })
|
||||
},
|
||||
120000
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> {
|
||||
return fetchWithTimeout(
|
||||
`${ACADEMY_API_BASE}/courses/generate-all`,
|
||||
{ method: 'POST' },
|
||||
300000
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> {
|
||||
return fetchWithTimeout<{ status: string; jobId?: string }>(
|
||||
`${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`,
|
||||
{ method: 'POST' },
|
||||
300000
|
||||
)
|
||||
}
|
||||
|
||||
export async function getVideoStatus(courseId: string): Promise<{
|
||||
status: string
|
||||
total: number
|
||||
completed: number
|
||||
failed: number
|
||||
videos: Array<{ lessonId: string; status: string; url?: string }>
|
||||
}> {
|
||||
return fetchWithTimeout(
|
||||
`${ACADEMY_API_BASE}/courses/${courseId}/video-status`
|
||||
)
|
||||
}
|
||||
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Academy API - Shared configuration, helpers, and backend type mapping
|
||||
*/
|
||||
|
||||
import type {
|
||||
Course,
|
||||
CourseCategory,
|
||||
LessonType,
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export const ACADEMY_API_BASE = '/api/sdk/v1/academy'
|
||||
export const API_TIMEOUT = 30000 // 30 seconds
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getTenantId(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
||||
}
|
||||
return 'default-tenant'
|
||||
}
|
||||
|
||||
export function getAuthHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': getTenantId()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
const userId = localStorage.getItem('bp_user_id')
|
||||
if (userId) {
|
||||
headers['X-User-ID'] = userId
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function fetchWithTimeout<T>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
timeout: number = API_TIMEOUT
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep the HTTP status message
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {} as T
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BACKEND TYPE MAPPING (snake_case -> camelCase)
|
||||
// =============================================================================
|
||||
|
||||
export interface BackendCourse {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: CourseCategory
|
||||
duration_minutes: number
|
||||
required_for_roles: string[]
|
||||
is_active: boolean
|
||||
passing_score?: number
|
||||
status?: string
|
||||
lessons?: BackendLesson[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface BackendQuizQuestion {
|
||||
id: string
|
||||
question: string
|
||||
options: string[]
|
||||
correct_index: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface BackendLesson {
|
||||
id: string
|
||||
course_id: string
|
||||
title: string
|
||||
description?: string
|
||||
lesson_type: LessonType
|
||||
content_url?: string
|
||||
duration_minutes: number
|
||||
order_index: number
|
||||
quiz_questions?: BackendQuizQuestion[]
|
||||
}
|
||||
|
||||
export function mapCourseFromBackend(bc: BackendCourse): Course {
|
||||
return {
|
||||
id: bc.id,
|
||||
title: bc.title,
|
||||
description: bc.description || '',
|
||||
category: bc.category,
|
||||
durationMinutes: bc.duration_minutes || 0,
|
||||
passingScore: bc.passing_score ?? 70,
|
||||
isActive: bc.is_active ?? true,
|
||||
status: (bc.status as 'draft' | 'published') ?? 'draft',
|
||||
requiredForRoles: bc.required_for_roles || [],
|
||||
lessons: (bc.lessons || []).map(l => ({
|
||||
id: l.id,
|
||||
courseId: l.course_id,
|
||||
title: l.title,
|
||||
type: l.lesson_type,
|
||||
contentMarkdown: l.content_url || '',
|
||||
durationMinutes: l.duration_minutes || 0,
|
||||
order: l.order_index,
|
||||
quizQuestions: (l.quiz_questions || []).map(q => ({
|
||||
id: q.id || `q-${Math.random().toString(36).slice(2)}`,
|
||||
lessonId: l.id,
|
||||
question: q.question,
|
||||
options: q.options,
|
||||
correctOptionIndex: q.correct_index,
|
||||
explanation: q.explanation,
|
||||
})),
|
||||
})),
|
||||
createdAt: bc.created_at,
|
||||
updatedAt: bc.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
|
||||
return courses.map(mapCourseFromBackend)
|
||||
}
|
||||
157
admin-compliance/lib/sdk/academy/api-mock-data.ts
Normal file
157
admin-compliance/lib/sdk/academy/api-mock-data.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Academy API — Mock Data & SDK Proxy
|
||||
*
|
||||
* Fallback mock data for development and SDK proxy function
|
||||
*/
|
||||
|
||||
import type {
|
||||
Course,
|
||||
CourseCategory,
|
||||
Enrollment,
|
||||
EnrollmentStatus,
|
||||
AcademyStatistics,
|
||||
} from './types'
|
||||
import { isEnrollmentOverdue } from './types'
|
||||
import {
|
||||
fetchCourses,
|
||||
fetchEnrollments,
|
||||
fetchAcademyStatistics,
|
||||
} from './api-courses'
|
||||
|
||||
// =============================================================================
|
||||
// SDK PROXY FUNCTION
|
||||
// =============================================================================
|
||||
|
||||
export async function fetchSDKAcademyList(): Promise<{
|
||||
courses: Course[]
|
||||
enrollments: Enrollment[]
|
||||
statistics: AcademyStatistics
|
||||
}> {
|
||||
try {
|
||||
const [courses, enrollments, statistics] = await Promise.all([
|
||||
fetchCourses(),
|
||||
fetchEnrollments(),
|
||||
fetchAcademyStatistics()
|
||||
])
|
||||
|
||||
return { courses, enrollments, statistics }
|
||||
} catch (error) {
|
||||
console.error('Failed to load Academy data from backend, using mock data:', error)
|
||||
|
||||
const courses = createMockCourses()
|
||||
const enrollments = createMockEnrollments()
|
||||
const statistics = createMockStatistics(courses, enrollments)
|
||||
|
||||
return { courses, enrollments, statistics }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA
|
||||
// =============================================================================
|
||||
|
||||
export function createMockCourses(): Course[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'course-001',
|
||||
title: 'DSGVO-Grundlagen fuer Mitarbeiter',
|
||||
description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.',
|
||||
category: 'dsgvo_basics',
|
||||
durationMinutes: 90,
|
||||
passingScore: 80,
|
||||
isActive: true,
|
||||
status: 'published',
|
||||
requiredForRoles: ['all'],
|
||||
createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{ id: 'lesson-001-01', courseId: 'course-001', order: 1, title: 'Was ist die DSGVO?', type: 'text', contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...', durationMinutes: 15 },
|
||||
{ id: 'lesson-001-02', courseId: 'course-001', order: 2, title: 'Die 7 Grundsaetze der DSGVO', type: 'video', contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.', durationMinutes: 20, videoUrl: '/videos/dsgvo-grundsaetze.mp4' },
|
||||
{ id: 'lesson-001-03', courseId: 'course-001', order: 3, title: 'Betroffenenrechte (Art. 15-21)', type: 'text', contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.', durationMinutes: 20 },
|
||||
{ id: 'lesson-001-04', courseId: 'course-001', order: 4, title: 'Personenbezogene Daten im Arbeitsalltag', type: 'video', contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.', durationMinutes: 15, videoUrl: '/videos/dsgvo-praxis.mp4' },
|
||||
{ id: 'lesson-001-05', courseId: 'course-001', order: 5, title: 'Wissenstest: DSGVO-Grundlagen', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.', durationMinutes: 20 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'course-002',
|
||||
title: 'IT-Sicherheit & Cybersecurity Awareness',
|
||||
description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
|
||||
category: 'it_security',
|
||||
durationMinutes: 60,
|
||||
passingScore: 75,
|
||||
isActive: true,
|
||||
status: 'published',
|
||||
requiredForRoles: ['all'],
|
||||
createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{ id: 'lesson-002-01', courseId: 'course-002', order: 1, title: 'Phishing erkennen und vermeiden', type: 'video', contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?', durationMinutes: 15, videoUrl: '/videos/phishing-awareness.mp4' },
|
||||
{ id: 'lesson-002-02', courseId: 'course-002', order: 2, title: 'Sichere Passwoerter und MFA', type: 'text', contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.', durationMinutes: 15 },
|
||||
{ id: 'lesson-002-03', courseId: 'course-002', order: 3, title: 'Social Engineering und Manipulation', type: 'text', contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.', durationMinutes: 15 },
|
||||
{ id: 'lesson-002-04', courseId: 'course-002', order: 4, title: 'Wissenstest: IT-Sicherheit', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.', durationMinutes: 15 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'course-003',
|
||||
title: 'AI Literacy - Sicherer Umgang mit KI',
|
||||
description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.',
|
||||
category: 'ai_literacy',
|
||||
durationMinutes: 75,
|
||||
passingScore: 70,
|
||||
isActive: true,
|
||||
status: 'draft',
|
||||
requiredForRoles: ['admin', 'data_protection_officer'],
|
||||
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{ id: 'lesson-003-01', courseId: 'course-003', order: 1, title: 'Was ist Kuenstliche Intelligenz?', type: 'text', contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.', durationMinutes: 15 },
|
||||
{ id: 'lesson-003-02', courseId: 'course-003', order: 2, title: 'Der EU AI Act - Was bedeutet er fuer uns?', type: 'video', contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.', durationMinutes: 20, videoUrl: '/videos/eu-ai-act.mp4' },
|
||||
{ id: 'lesson-003-03', courseId: 'course-003', order: 3, title: 'KI-Werkzeuge sicher nutzen', type: 'text', contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.', durationMinutes: 20 },
|
||||
{ id: 'lesson-003-04', courseId: 'course-003', order: 4, title: 'Wissenstest: AI Literacy', type: 'quiz', contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.', durationMinutes: 20 },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function createMockEnrollments(): Enrollment[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{ id: 'enr-001', courseId: 'course-001', userId: 'user-001', userName: 'Maria Fischer', userEmail: 'maria.fischer@example.de', status: 'in_progress', progress: 40, startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString() },
|
||||
{ id: 'enr-002', courseId: 'course-002', userId: 'user-002', userName: 'Stefan Mueller', userEmail: 'stefan.mueller@example.de', status: 'completed', progress: 100, startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(), certificateId: 'cert-001', deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString() },
|
||||
{ id: 'enr-003', courseId: 'course-001', userId: 'user-003', userName: 'Laura Schneider', userEmail: 'laura.schneider@example.de', status: 'not_started', progress: 0, startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString() },
|
||||
{ id: 'enr-004', courseId: 'course-003', userId: 'user-004', userName: 'Thomas Wagner', userEmail: 'thomas.wagner@example.de', status: 'expired', progress: 25, startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString() },
|
||||
{ id: 'enr-005', courseId: 'course-002', userId: 'user-005', userName: 'Julia Becker', userEmail: 'julia.becker@example.de', status: 'in_progress', progress: 50, startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString() },
|
||||
]
|
||||
}
|
||||
|
||||
export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics {
|
||||
const c = courses || createMockCourses()
|
||||
const e = enrollments || createMockEnrollments()
|
||||
|
||||
const completedCount = e.filter(en => en.status === 'completed').length
|
||||
const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0
|
||||
const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length
|
||||
|
||||
return {
|
||||
totalCourses: c.length,
|
||||
totalEnrollments: e.length,
|
||||
completionRate,
|
||||
overdueCount,
|
||||
byCategory: {
|
||||
dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length,
|
||||
it_security: c.filter(co => co.category === 'it_security').length,
|
||||
ai_literacy: c.filter(co => co.category === 'ai_literacy').length,
|
||||
whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length,
|
||||
custom: c.filter(co => co.category === 'custom').length,
|
||||
},
|
||||
byStatus: {
|
||||
not_started: e.filter(en => en.status === 'not_started').length,
|
||||
in_progress: e.filter(en => en.status === 'in_progress').length,
|
||||
completed: e.filter(en => en.status === 'completed').length,
|
||||
expired: e.filter(en => en.status === 'expired').length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,787 +1,38 @@
|
||||
/**
|
||||
* Academy API Client
|
||||
* Academy API Client — barrel re-export
|
||||
*
|
||||
* API client for the Compliance E-Learning Academy module
|
||||
* Connects to the ai-compliance-sdk backend via Next.js proxy
|
||||
* Split into:
|
||||
* - api-courses.ts (CRUD, enrollments, certificates, quiz, stats, generation)
|
||||
* - api-mock-data.ts (mock data + SDK proxy)
|
||||
*/
|
||||
|
||||
import type {
|
||||
Course,
|
||||
CourseCategory,
|
||||
CourseCreateRequest,
|
||||
CourseUpdateRequest,
|
||||
GenerateCourseRequest,
|
||||
Enrollment,
|
||||
EnrollmentStatus,
|
||||
EnrollmentListResponse,
|
||||
EnrollUserRequest,
|
||||
UpdateProgressRequest,
|
||||
Certificate,
|
||||
AcademyStatistics,
|
||||
LessonType,
|
||||
SubmitQuizRequest,
|
||||
SubmitQuizResponse,
|
||||
} from './types'
|
||||
import { isEnrollmentOverdue } from './types'
|
||||
export {
|
||||
fetchCourses,
|
||||
fetchCourse,
|
||||
createCourse,
|
||||
updateCourse,
|
||||
deleteCourse,
|
||||
fetchEnrollments,
|
||||
enrollUser,
|
||||
updateProgress,
|
||||
completeEnrollment,
|
||||
deleteEnrollment,
|
||||
updateEnrollment,
|
||||
fetchCertificate,
|
||||
generateCertificate,
|
||||
fetchCertificates,
|
||||
submitQuiz,
|
||||
updateLesson,
|
||||
fetchAcademyStatistics,
|
||||
generateCourse,
|
||||
generateAllCourses,
|
||||
generateVideos,
|
||||
getVideoStatus,
|
||||
} from './api-courses'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const ACADEMY_API_BASE = '/api/sdk/v1/academy'
|
||||
const API_TIMEOUT = 30000 // 30 seconds
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getTenantId(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
||||
}
|
||||
return 'default-tenant'
|
||||
}
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': getTenantId()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
const userId = localStorage.getItem('bp_user_id')
|
||||
if (userId) {
|
||||
headers['X-User-ID'] = userId
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async function fetchWithTimeout<T>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
timeout: number = API_TIMEOUT
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep the HTTP status message
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {} as T
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COURSE CRUD
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Alle Kurse abrufen
|
||||
*/
|
||||
export async function fetchCourses(): Promise<Course[]> {
|
||||
const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
|
||||
`${ACADEMY_API_BASE}/courses`
|
||||
)
|
||||
return mapCoursesFromBackend(res.courses || [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelnen Kurs abrufen
|
||||
*/
|
||||
export async function fetchCourse(id: string): Promise<Course> {
|
||||
const res = await fetchWithTimeout<{ course: BackendCourse }>(
|
||||
`${ACADEMY_API_BASE}/courses/${id}`
|
||||
)
|
||||
return mapCourseFromBackend(res.course)
|
||||
}
|
||||
|
||||
// Backend returns snake_case, frontend uses camelCase
|
||||
interface BackendCourse {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: CourseCategory
|
||||
duration_minutes: number
|
||||
required_for_roles: string[]
|
||||
is_active: boolean
|
||||
passing_score?: number
|
||||
status?: string
|
||||
lessons?: BackendLesson[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface BackendQuizQuestion {
|
||||
id: string
|
||||
question: string
|
||||
options: string[]
|
||||
correct_index: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface BackendLesson {
|
||||
id: string
|
||||
course_id: string
|
||||
title: string
|
||||
description?: string
|
||||
lesson_type: LessonType
|
||||
content_url?: string
|
||||
duration_minutes: number
|
||||
order_index: number
|
||||
quiz_questions?: BackendQuizQuestion[]
|
||||
}
|
||||
|
||||
function mapCourseFromBackend(bc: BackendCourse): Course {
|
||||
return {
|
||||
id: bc.id,
|
||||
title: bc.title,
|
||||
description: bc.description || '',
|
||||
category: bc.category,
|
||||
durationMinutes: bc.duration_minutes || 0,
|
||||
passingScore: bc.passing_score ?? 70,
|
||||
isActive: bc.is_active ?? true,
|
||||
status: (bc.status as 'draft' | 'published') ?? 'draft',
|
||||
requiredForRoles: bc.required_for_roles || [],
|
||||
lessons: (bc.lessons || []).map(l => ({
|
||||
id: l.id,
|
||||
courseId: l.course_id,
|
||||
title: l.title,
|
||||
type: l.lesson_type,
|
||||
contentMarkdown: l.content_url || '',
|
||||
durationMinutes: l.duration_minutes || 0,
|
||||
order: l.order_index,
|
||||
quizQuestions: (l.quiz_questions || []).map(q => ({
|
||||
id: q.id || `q-${Math.random().toString(36).slice(2)}`,
|
||||
lessonId: l.id,
|
||||
question: q.question,
|
||||
options: q.options,
|
||||
correctOptionIndex: q.correct_index,
|
||||
explanation: q.explanation,
|
||||
})),
|
||||
})),
|
||||
createdAt: bc.created_at,
|
||||
updatedAt: bc.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
|
||||
return courses.map(mapCourseFromBackend)
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Kurs erstellen
|
||||
*/
|
||||
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/courses`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurs aktualisieren
|
||||
*/
|
||||
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/courses/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurs loeschen
|
||||
*/
|
||||
export async function deleteCourse(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${ACADEMY_API_BASE}/courses/${id}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENROLLMENTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Einschreibungen abrufen (optional gefiltert nach Kurs-ID)
|
||||
*/
|
||||
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (courseId) {
|
||||
params.set('course_id', courseId)
|
||||
}
|
||||
const queryString = params.toString()
|
||||
const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url)
|
||||
return res.enrollments || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer in einen Kurs einschreiben
|
||||
*/
|
||||
export async function enrollUser(request: EnrollUserRequest): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fortschritt einer Einschreibung aktualisieren
|
||||
*/
|
||||
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einschreibung als abgeschlossen markieren
|
||||
*/
|
||||
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CERTIFICATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Zertifikat abrufen
|
||||
*/
|
||||
export async function fetchCertificate(id: string): Promise<Certificate> {
|
||||
return fetchWithTimeout<Certificate>(
|
||||
`${ACADEMY_API_BASE}/certificates/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zertifikat generieren nach erfolgreichem Kursabschluss
|
||||
*/
|
||||
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
|
||||
return fetchWithTimeout<Certificate>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Zertifikate abrufen
|
||||
*/
|
||||
export async function fetchCertificates(): Promise<Certificate[]> {
|
||||
const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>(
|
||||
`${ACADEMY_API_BASE}/certificates`
|
||||
)
|
||||
return res.certificates || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Einschreibung loeschen
|
||||
*/
|
||||
export async function deleteEnrollment(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${id}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einschreibung aktualisieren (z.B. Deadline)
|
||||
*/
|
||||
export async function updateEnrollment(id: string, data: { deadline?: string }): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/enrollments/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUIZ
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Quiz-Antworten einreichen und auswerten (ohne Enrollment)
|
||||
*/
|
||||
export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise<SubmitQuizResponse> {
|
||||
return fetchWithTimeout<SubmitQuizResponse>(
|
||||
`${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(answers)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lektion aktualisieren (Content, Titel, Quiz-Fragen)
|
||||
*/
|
||||
export async function updateLesson(lessonId: string, update: {
|
||||
title?: string
|
||||
description?: string
|
||||
content_url?: string
|
||||
duration_minutes?: number
|
||||
quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }>
|
||||
}): Promise<{ lesson: any }> {
|
||||
return fetchWithTimeout(
|
||||
`${ACADEMY_API_BASE}/lessons/${lessonId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Academy-Statistiken abrufen
|
||||
*/
|
||||
export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
|
||||
const res = await fetchWithTimeout<{
|
||||
total_courses: number
|
||||
total_enrollments: number
|
||||
completion_rate: number
|
||||
overdue_count: number
|
||||
avg_completion_days: number
|
||||
by_category?: Record<string, number>
|
||||
by_status?: Record<string, number>
|
||||
}>(`${ACADEMY_API_BASE}/stats`)
|
||||
|
||||
return {
|
||||
totalCourses: res.total_courses || 0,
|
||||
totalEnrollments: res.total_enrollments || 0,
|
||||
completionRate: res.completion_rate || 0,
|
||||
overdueCount: res.overdue_count || 0,
|
||||
byCategory: (res.by_category || {}) as Record<CourseCategory, number>,
|
||||
byStatus: (res.by_status || {}) as Record<EnrollmentStatus, number>,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COURSE GENERATION (via Training Engine)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Academy-Kurs aus einem Training-Modul generieren
|
||||
*/
|
||||
export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> {
|
||||
return fetchWithTimeout<{ course: Course }>(
|
||||
`${ACADEMY_API_BASE}/courses/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ module_id: request.moduleId || request.title })
|
||||
},
|
||||
120000 // 2 min timeout
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Academy-Kurse aus Training-Modulen generieren
|
||||
*/
|
||||
export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> {
|
||||
return fetchWithTimeout(
|
||||
`${ACADEMY_API_BASE}/courses/generate-all`,
|
||||
{ method: 'POST' },
|
||||
300000 // 5 min timeout
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Videos fuer alle Lektionen eines Kurses generieren
|
||||
*/
|
||||
export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> {
|
||||
return fetchWithTimeout<{ status: string; jobId?: string }>(
|
||||
`${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`,
|
||||
{
|
||||
method: 'POST'
|
||||
},
|
||||
300000 // 5 min timeout for video generation
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Video-Generierungsstatus abrufen
|
||||
*/
|
||||
export async function getVideoStatus(courseId: string): Promise<{
|
||||
status: string
|
||||
total: number
|
||||
completed: number
|
||||
failed: number
|
||||
videos: Array<{ lessonId: string; status: string; url?: string }>
|
||||
}> {
|
||||
return fetchWithTimeout(
|
||||
`${ACADEMY_API_BASE}/courses/${courseId}/video-status`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SDK PROXY FUNCTION (wraps fetchCourses + fetchAcademyStatistics)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kurse und Statistiken laden - mit Fallback auf Mock-Daten
|
||||
*/
|
||||
export async function fetchSDKAcademyList(): Promise<{
|
||||
courses: Course[]
|
||||
enrollments: Enrollment[]
|
||||
statistics: AcademyStatistics
|
||||
}> {
|
||||
try {
|
||||
const [courses, enrollments, statistics] = await Promise.all([
|
||||
fetchCourses(),
|
||||
fetchEnrollments(),
|
||||
fetchAcademyStatistics()
|
||||
])
|
||||
|
||||
return { courses, enrollments, statistics }
|
||||
} catch (error) {
|
||||
console.error('Failed to load Academy data from backend, using mock data:', error)
|
||||
|
||||
// Fallback to mock data
|
||||
const courses = createMockCourses()
|
||||
const enrollments = createMockEnrollments()
|
||||
const statistics = createMockStatistics(courses, enrollments)
|
||||
|
||||
return { courses, enrollments, statistics }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA (Fallback / Demo)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Demo-Kurse mit deutschen Titeln erstellen
|
||||
*/
|
||||
export function createMockCourses(): Course[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'course-001',
|
||||
title: 'DSGVO-Grundlagen fuer Mitarbeiter',
|
||||
description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.',
|
||||
category: 'dsgvo_basics',
|
||||
durationMinutes: 90,
|
||||
passingScore: 80,
|
||||
isActive: true,
|
||||
status: 'published',
|
||||
requiredForRoles: ['all'],
|
||||
createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{
|
||||
id: 'lesson-001-01',
|
||||
courseId: 'course-001',
|
||||
order: 1,
|
||||
title: 'Was ist die DSGVO?',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-02',
|
||||
courseId: 'course-001',
|
||||
order: 2,
|
||||
title: 'Die 7 Grundsaetze der DSGVO',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.',
|
||||
durationMinutes: 20,
|
||||
videoUrl: '/videos/dsgvo-grundsaetze.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-03',
|
||||
courseId: 'course-001',
|
||||
order: 3,
|
||||
title: 'Betroffenenrechte (Art. 15-21)',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.',
|
||||
durationMinutes: 20
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-04',
|
||||
courseId: 'course-001',
|
||||
order: 4,
|
||||
title: 'Personenbezogene Daten im Arbeitsalltag',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.',
|
||||
durationMinutes: 15,
|
||||
videoUrl: '/videos/dsgvo-praxis.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-05',
|
||||
courseId: 'course-001',
|
||||
order: 5,
|
||||
title: 'Wissenstest: DSGVO-Grundlagen',
|
||||
type: 'quiz',
|
||||
contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.',
|
||||
durationMinutes: 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'course-002',
|
||||
title: 'IT-Sicherheit & Cybersecurity Awareness',
|
||||
description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
|
||||
category: 'it_security',
|
||||
durationMinutes: 60,
|
||||
passingScore: 75,
|
||||
isActive: true,
|
||||
status: 'published',
|
||||
requiredForRoles: ['all'],
|
||||
createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{
|
||||
id: 'lesson-002-01',
|
||||
courseId: 'course-002',
|
||||
order: 1,
|
||||
title: 'Phishing erkennen und vermeiden',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?',
|
||||
durationMinutes: 15,
|
||||
videoUrl: '/videos/phishing-awareness.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-002-02',
|
||||
courseId: 'course-002',
|
||||
order: 2,
|
||||
title: 'Sichere Passwoerter und MFA',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-002-03',
|
||||
courseId: 'course-002',
|
||||
order: 3,
|
||||
title: 'Social Engineering und Manipulation',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-002-04',
|
||||
courseId: 'course-002',
|
||||
order: 4,
|
||||
title: 'Wissenstest: IT-Sicherheit',
|
||||
type: 'quiz',
|
||||
contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.',
|
||||
durationMinutes: 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'course-003',
|
||||
title: 'AI Literacy - Sicherer Umgang mit KI',
|
||||
description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.',
|
||||
category: 'ai_literacy',
|
||||
durationMinutes: 75,
|
||||
passingScore: 70,
|
||||
isActive: true,
|
||||
status: 'draft',
|
||||
requiredForRoles: ['admin', 'data_protection_officer'],
|
||||
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{
|
||||
id: 'lesson-003-01',
|
||||
courseId: 'course-003',
|
||||
order: 1,
|
||||
title: 'Was ist Kuenstliche Intelligenz?',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-003-02',
|
||||
courseId: 'course-003',
|
||||
order: 2,
|
||||
title: 'Der EU AI Act - Was bedeutet er fuer uns?',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.',
|
||||
durationMinutes: 20,
|
||||
videoUrl: '/videos/eu-ai-act.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-003-03',
|
||||
courseId: 'course-003',
|
||||
order: 3,
|
||||
title: 'KI-Werkzeuge sicher nutzen',
|
||||
type: 'text',
|
||||
contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.',
|
||||
durationMinutes: 20
|
||||
},
|
||||
{
|
||||
id: 'lesson-003-04',
|
||||
courseId: 'course-003',
|
||||
order: 4,
|
||||
title: 'Wissenstest: AI Literacy',
|
||||
type: 'quiz',
|
||||
contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.',
|
||||
durationMinutes: 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo-Einschreibungen erstellen
|
||||
*/
|
||||
export function createMockEnrollments(): Enrollment[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'enr-001',
|
||||
courseId: 'course-001',
|
||||
userId: 'user-001',
|
||||
userName: 'Maria Fischer',
|
||||
userEmail: 'maria.fischer@example.de',
|
||||
status: 'in_progress',
|
||||
progress: 40,
|
||||
startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-002',
|
||||
courseId: 'course-002',
|
||||
userId: 'user-002',
|
||||
userName: 'Stefan Mueller',
|
||||
userEmail: 'stefan.mueller@example.de',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
certificateId: 'cert-001',
|
||||
deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-003',
|
||||
courseId: 'course-001',
|
||||
userId: 'user-003',
|
||||
userName: 'Laura Schneider',
|
||||
userEmail: 'laura.schneider@example.de',
|
||||
status: 'not_started',
|
||||
progress: 0,
|
||||
startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-004',
|
||||
courseId: 'course-003',
|
||||
userId: 'user-004',
|
||||
userName: 'Thomas Wagner',
|
||||
userEmail: 'thomas.wagner@example.de',
|
||||
status: 'expired',
|
||||
progress: 25,
|
||||
startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-005',
|
||||
courseId: 'course-002',
|
||||
userId: 'user-005',
|
||||
userName: 'Julia Becker',
|
||||
userEmail: 'julia.becker@example.de',
|
||||
status: 'in_progress',
|
||||
progress: 50,
|
||||
startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo-Statistiken aus Kursen und Einschreibungen berechnen
|
||||
*/
|
||||
export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics {
|
||||
const c = courses || createMockCourses()
|
||||
const e = enrollments || createMockEnrollments()
|
||||
|
||||
const completedCount = e.filter(en => en.status === 'completed').length
|
||||
const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0
|
||||
const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length
|
||||
|
||||
return {
|
||||
totalCourses: c.length,
|
||||
totalEnrollments: e.length,
|
||||
completionRate,
|
||||
overdueCount,
|
||||
byCategory: {
|
||||
dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length,
|
||||
it_security: c.filter(co => co.category === 'it_security').length,
|
||||
ai_literacy: c.filter(co => co.category === 'ai_literacy').length,
|
||||
whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length,
|
||||
custom: c.filter(co => co.category === 'custom').length,
|
||||
},
|
||||
byStatus: {
|
||||
not_started: e.filter(en => en.status === 'not_started').length,
|
||||
in_progress: e.filter(en => en.status === 'in_progress').length,
|
||||
completed: e.filter(en => en.status === 'completed').length,
|
||||
expired: e.filter(en => en.status === 'expired').length,
|
||||
}
|
||||
}
|
||||
}
|
||||
export {
|
||||
fetchSDKAcademyList,
|
||||
createMockCourses,
|
||||
createMockEnrollments,
|
||||
createMockStatistics,
|
||||
} from './api-mock-data'
|
||||
|
||||
299
admin-compliance/lib/sdk/api-client-operations.ts
Normal file
299
admin-compliance/lib/sdk/api-client-operations.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* SDK API Client — Operational methods.
|
||||
* (checkpoints, flow, modules, UCCA, document import, screening, health)
|
||||
*/
|
||||
|
||||
import {
|
||||
APIResponse,
|
||||
CheckpointValidationResult,
|
||||
FetchContext,
|
||||
CheckpointStatus,
|
||||
} from './api-client-types'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Checkpoint Validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate a specific checkpoint
|
||||
*/
|
||||
export async function validateCheckpoint(
|
||||
ctx: FetchContext,
|
||||
checkpointId: string,
|
||||
data?: unknown
|
||||
): Promise<CheckpointValidationResult> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
|
||||
`${ctx.baseUrl}/checkpoints/validate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tenantId: ctx.tenantId,
|
||||
checkpointId,
|
||||
data,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw ctx.createError(response.error || 'Checkpoint validation failed', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all checkpoint statuses
|
||||
*/
|
||||
export async function getCheckpoints(
|
||||
ctx: FetchContext
|
||||
): Promise<Record<string, CheckpointStatus>> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
|
||||
`${ctx.baseUrl}/checkpoints?tenantId=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
return response.data || {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flow Navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get current flow state
|
||||
*/
|
||||
export async function getFlowState(ctx: FetchContext): Promise<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}>>(
|
||||
`${ctx.baseUrl}/flow?tenantId=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw ctx.createError('Failed to get flow state', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next/previous step
|
||||
*/
|
||||
export async function navigateFlow(
|
||||
ctx: FetchContext,
|
||||
direction: 'next' | 'previous'
|
||||
): Promise<{ stepId: string; phase: 1 | 2 }> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}>>(
|
||||
`${ctx.baseUrl}/flow`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tenantId: ctx.tenantId,
|
||||
direction,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw ctx.createError('Failed to navigate flow', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get available compliance modules from backend
|
||||
*/
|
||||
export async function getModules(
|
||||
ctx: FetchContext,
|
||||
filters?: {
|
||||
serviceType?: string
|
||||
criticality?: string
|
||||
processesPii?: boolean
|
||||
aiComponents?: boolean
|
||||
}
|
||||
): Promise<{ modules: unknown[]; total: number }> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.serviceType) params.set('service_type', filters.serviceType)
|
||||
if (filters?.criticality) params.set('criticality', filters.criticality)
|
||||
if (filters?.processesPii !== undefined) params.set('processes_pii', String(filters.processesPii))
|
||||
if (filters?.aiComponents !== undefined) params.set('ai_components', String(filters.aiComponents))
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${ctx.baseUrl}/modules${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await ctx.fetchWithRetry<{ modules: unknown[]; total: number }>(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UCCA (Use Case Compliance Assessment)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Assess a use case
|
||||
*/
|
||||
export async function assessUseCase(
|
||||
ctx: FetchContext,
|
||||
intake: unknown
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/ucca/assess`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
body: JSON.stringify(intake),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assessments
|
||||
*/
|
||||
export async function getAssessments(ctx: FetchContext): Promise<unknown[]> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown[]>>(
|
||||
`${ctx.baseUrl}/ucca/assessments?tenantId=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single assessment
|
||||
*/
|
||||
export async function getAssessment(
|
||||
ctx: FetchContext,
|
||||
id: string
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an assessment
|
||||
*/
|
||||
export async function deleteAssessment(
|
||||
ctx: FetchContext,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
await ctx.fetchWithRetry<APIResponse<void>>(
|
||||
`${ctx.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document Import & Screening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Analyze an uploaded document
|
||||
*/
|
||||
export async function analyzeDocument(
|
||||
ctx: FetchContext,
|
||||
formData: FormData
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/import/analyze`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-Tenant-ID': ctx.tenantId },
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a dependency file (package-lock.json, requirements.txt, etc.)
|
||||
*/
|
||||
export async function scanDependencies(
|
||||
ctx: FetchContext,
|
||||
formData: FormData
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/screening/scan`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-Tenant-ID': ctx.tenantId },
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
export async function healthCheck(ctx: FetchContext): Promise<boolean> {
|
||||
try {
|
||||
const response = await ctx.fetchWithTimeout(
|
||||
`${ctx.baseUrl}/health`,
|
||||
{ method: 'GET' },
|
||||
`health-${Date.now()}`
|
||||
)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
160
admin-compliance/lib/sdk/api-client-projects.ts
Normal file
160
admin-compliance/lib/sdk/api-client-projects.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* SDK API Client — Project management methods.
|
||||
* (listProjects, createProject, updateProject, getProject,
|
||||
* archiveProject, restoreProject, permanentlyDeleteProject)
|
||||
*/
|
||||
|
||||
import { FetchContext } from './api-client-types'
|
||||
import { ProjectInfo } from './types'
|
||||
|
||||
/**
|
||||
* List all projects for the current tenant
|
||||
*/
|
||||
export async function listProjects(
|
||||
ctx: FetchContext,
|
||||
includeArchived = true
|
||||
): Promise<{ projects: ProjectInfo[]; total: number }> {
|
||||
const response = await ctx.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
|
||||
`${ctx.baseUrl}/projects?tenant_id=${encodeURIComponent(ctx.tenantId)}&include_archived=${includeArchived}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
export async function createProject(
|
||||
ctx: FetchContext,
|
||||
data: {
|
||||
name: string
|
||||
description?: string
|
||||
customer_type?: string
|
||||
copy_from_project_id?: string
|
||||
}
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: ctx.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
export async function updateProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string,
|
||||
data: { name?: string; description?: string }
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: ctx.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single project by ID
|
||||
*/
|
||||
export async function getProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-delete) a project
|
||||
*/
|
||||
export async function archiveProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
await ctx.fetchWithRetry<{ success: boolean }>(
|
||||
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an archived project
|
||||
*/
|
||||
export async function restoreProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a project and all data
|
||||
*/
|
||||
export async function permanentlyDeleteProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
await ctx.fetchWithRetry<{ success: boolean }>(
|
||||
`${ctx.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
120
admin-compliance/lib/sdk/api-client-state.ts
Normal file
120
admin-compliance/lib/sdk/api-client-state.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* SDK API Client — State management methods.
|
||||
* (getState, saveState, deleteState, exportState)
|
||||
*/
|
||||
|
||||
import {
|
||||
APIResponse,
|
||||
APIError,
|
||||
StateResponse,
|
||||
FetchContext,
|
||||
SDKState,
|
||||
} from './api-client-types'
|
||||
|
||||
/**
|
||||
* Load SDK state for the current tenant
|
||||
*/
|
||||
export async function getState(ctx: FetchContext): Promise<StateResponse | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ tenantId: ctx.tenantId })
|
||||
if (ctx.projectId) params.set('projectId', ctx.projectId)
|
||||
const response = await ctx.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${ctx.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
const apiError = error as APIError
|
||||
// 404 means no state exists yet - that's okay
|
||||
if (apiError.status === 404) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SDK state for the current tenant.
|
||||
* Supports optimistic locking via version parameter.
|
||||
*/
|
||||
export async function saveState(
|
||||
ctx: FetchContext,
|
||||
state: SDKState,
|
||||
version?: number
|
||||
): Promise<StateResponse> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${ctx.baseUrl}/state`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(version !== undefined && { 'If-Match': String(version) }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: ctx.tenantId,
|
||||
projectId: ctx.projectId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success) {
|
||||
throw ctx.createError(response.error || 'Failed to save state', 500, true)
|
||||
}
|
||||
|
||||
return response.data!
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
export async function deleteState(ctx: FetchContext): Promise<void> {
|
||||
const params = new URLSearchParams({ tenantId: ctx.tenantId })
|
||||
if (ctx.projectId) params.set('projectId', ctx.projectId)
|
||||
await ctx.fetchWithRetry<APIResponse<void>>(
|
||||
`${ctx.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export SDK state in various formats
|
||||
*/
|
||||
export async function exportState(
|
||||
ctx: FetchContext,
|
||||
format: 'json' | 'pdf' | 'zip'
|
||||
): Promise<Blob> {
|
||||
const response = await ctx.fetchWithTimeout(
|
||||
`${ctx.baseUrl}/export?tenantId=${encodeURIComponent(ctx.tenantId)}&format=${format}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept':
|
||||
format === 'json'
|
||||
? 'application/json'
|
||||
: format === 'pdf'
|
||||
? 'application/pdf'
|
||||
: 'application/zip',
|
||||
},
|
||||
},
|
||||
`export-${Date.now()}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw ctx.createError(`Export failed: ${response.statusText}`, response.status, true)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
84
admin-compliance/lib/sdk/api-client-types.ts
Normal file
84
admin-compliance/lib/sdk/api-client-types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* SDK API Client — shared types, interfaces, and configuration constants.
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface APIResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
version?: number
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export interface StateResponse {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version: number
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
export interface SaveStateRequest {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version?: number // For optimistic locking
|
||||
}
|
||||
|
||||
export interface CheckpointValidationResult {
|
||||
checkpointId: string
|
||||
passed: boolean
|
||||
errors: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
warnings: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
validatedAt: string
|
||||
validatedBy: string
|
||||
}
|
||||
|
||||
export interface APIError extends Error {
|
||||
status?: number
|
||||
code?: string
|
||||
retryable: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export const DEFAULT_BASE_URL = '/api/sdk/v1'
|
||||
export const DEFAULT_TIMEOUT = 30000 // 30 seconds
|
||||
export const MAX_RETRIES = 3
|
||||
export const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
|
||||
// =============================================================================
|
||||
// FETCH CONTEXT — passed to domain helpers
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Subset of the SDKApiClient that domain helpers need to make requests.
|
||||
* Avoids exposing the entire class and keeps helpers unit-testable.
|
||||
*/
|
||||
export interface FetchContext {
|
||||
baseUrl: string
|
||||
tenantId: string
|
||||
projectId: string | undefined
|
||||
fetchWithRetry<T>(url: string, options: RequestInit, retries?: number): Promise<T>
|
||||
fetchWithTimeout(url: string, options: RequestInit, requestId: string): Promise<Response>
|
||||
createError(message: string, status?: number, retryable?: boolean): APIError
|
||||
}
|
||||
|
||||
// Re-export types that domain helpers need from ./types
|
||||
export type { SDKState, CheckpointStatus }
|
||||
116
admin-compliance/lib/sdk/api-client-wiki.ts
Normal file
116
admin-compliance/lib/sdk/api-client-wiki.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* SDK API Client — Wiki (read-only knowledge base) methods.
|
||||
* (listWikiCategories, listWikiArticles, getWikiArticle, searchWiki)
|
||||
*/
|
||||
|
||||
import { FetchContext } from './api-client-types'
|
||||
import { WikiCategory, WikiArticle, WikiSearchResult } from './types'
|
||||
|
||||
/**
|
||||
* List all wiki categories with article counts
|
||||
*/
|
||||
export async function listWikiCategories(ctx: FetchContext): Promise<WikiCategory[]> {
|
||||
const data = await ctx.fetchWithRetry<{ categories: Array<{
|
||||
id: string; name: string; description: string; icon: string;
|
||||
sort_order: number; article_count: number
|
||||
}> }>(
|
||||
`${ctx.baseUrl}/wiki?endpoint=categories`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.categories || []).map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
icon: c.icon,
|
||||
sortOrder: c.sort_order,
|
||||
articleCount: c.article_count,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* List wiki articles, optionally filtered by category
|
||||
*/
|
||||
export async function listWikiArticles(
|
||||
ctx: FetchContext,
|
||||
categoryId?: string
|
||||
): Promise<WikiArticle[]> {
|
||||
const params = new URLSearchParams({ endpoint: 'articles' })
|
||||
if (categoryId) params.set('category_id', categoryId)
|
||||
const data = await ctx.fetchWithRetry<{ articles: Array<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}> }>(
|
||||
`${ctx.baseUrl}/wiki?${params.toString()}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.articles || []).map(a => ({
|
||||
id: a.id,
|
||||
categoryId: a.category_id,
|
||||
categoryName: a.category_name,
|
||||
title: a.title,
|
||||
summary: a.summary,
|
||||
content: a.content,
|
||||
legalRefs: a.legal_refs || [],
|
||||
tags: a.tags || [],
|
||||
relevance: a.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: a.source_urls || [],
|
||||
version: a.version,
|
||||
updatedAt: a.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single wiki article by ID
|
||||
*/
|
||||
export async function getWikiArticle(
|
||||
ctx: FetchContext,
|
||||
id: string
|
||||
): Promise<WikiArticle> {
|
||||
const data = await ctx.fetchWithRetry<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}>(
|
||||
`${ctx.baseUrl}/wiki?endpoint=article&id=${encodeURIComponent(id)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return {
|
||||
id: data.id,
|
||||
categoryId: data.category_id,
|
||||
categoryName: data.category_name,
|
||||
title: data.title,
|
||||
summary: data.summary,
|
||||
content: data.content,
|
||||
legalRefs: data.legal_refs || [],
|
||||
tags: data.tags || [],
|
||||
relevance: data.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: data.source_urls || [],
|
||||
version: data.version,
|
||||
updatedAt: data.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-text search across wiki articles
|
||||
*/
|
||||
export async function searchWiki(
|
||||
ctx: FetchContext,
|
||||
query: string
|
||||
): Promise<WikiSearchResult[]> {
|
||||
const data = await ctx.fetchWithRetry<{ results: Array<{
|
||||
id: string; title: string; summary: string; category_name: string;
|
||||
relevance: string; highlight: string
|
||||
}> }>(
|
||||
`${ctx.baseUrl}/wiki?endpoint=search&q=${encodeURIComponent(query)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.results || []).map(r => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
summary: r.summary,
|
||||
categoryName: r.category_name,
|
||||
relevance: r.relevance,
|
||||
highlight: r.highlight,
|
||||
}))
|
||||
}
|
||||
@@ -3,68 +3,36 @@
|
||||
*
|
||||
* Centralized API client for SDK state management with error handling,
|
||||
* retry logic, and optimistic locking support.
|
||||
*
|
||||
* Domain methods are implemented in sibling files and delegated to here:
|
||||
* api-client-state.ts — getState, saveState, deleteState, exportState
|
||||
* api-client-projects.ts — listProjects … permanentlyDeleteProject
|
||||
* api-client-wiki.ts — listWikiCategories … searchWiki
|
||||
* api-client-operations.ts — checkpoints, flow, modules, UCCA, import, screening
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus, ProjectInfo, WikiCategory, WikiArticle, WikiSearchResult } from './types'
|
||||
import {
|
||||
APIResponse,
|
||||
StateResponse,
|
||||
SaveStateRequest,
|
||||
CheckpointValidationResult,
|
||||
APIError,
|
||||
FetchContext,
|
||||
DEFAULT_BASE_URL,
|
||||
DEFAULT_TIMEOUT,
|
||||
MAX_RETRIES,
|
||||
RETRY_DELAYS,
|
||||
} from './api-client-types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
// Re-export public types so existing consumers keep working
|
||||
export type { APIResponse, StateResponse, SaveStateRequest, CheckpointValidationResult, APIError }
|
||||
|
||||
export interface APIResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
version?: number
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export interface StateResponse {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version: number
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
export interface SaveStateRequest {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version?: number // For optimistic locking
|
||||
}
|
||||
|
||||
export interface CheckpointValidationResult {
|
||||
checkpointId: string
|
||||
passed: boolean
|
||||
errors: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
warnings: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
validatedAt: string
|
||||
validatedBy: string
|
||||
}
|
||||
|
||||
export interface APIError extends Error {
|
||||
status?: number
|
||||
code?: string
|
||||
retryable: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const DEFAULT_BASE_URL = '/api/sdk/v1'
|
||||
const DEFAULT_TIMEOUT = 30000 // 30 seconds
|
||||
const MAX_RETRIES = 3
|
||||
const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
// Domain helpers
|
||||
import * as stateHelpers from './api-client-state'
|
||||
import * as projectHelpers from './api-client-projects'
|
||||
import * as wikiHelpers from './api-client-wiki'
|
||||
import * as opsHelpers from './api-client-operations'
|
||||
|
||||
// =============================================================================
|
||||
// API CLIENT
|
||||
@@ -90,17 +58,17 @@ export class SDKApiClient {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// Private infrastructure — also exposed via FetchContext to helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private createError(message: string, status?: number, retryable = false): APIError {
|
||||
createError(message: string, status?: number, retryable = false): APIError {
|
||||
const error = new Error(message) as APIError
|
||||
error.status = status
|
||||
error.retryable = retryable
|
||||
return error
|
||||
}
|
||||
|
||||
private async fetchWithTimeout(
|
||||
async fetchWithTimeout(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
requestId: string
|
||||
@@ -122,7 +90,7 @@ export class SDKApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWithRetry<T>(
|
||||
async fetchWithRetry<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
retries = MAX_RETRIES
|
||||
@@ -182,673 +150,83 @@ export class SDKApiClient {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - State Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load SDK state for the current tenant
|
||||
*/
|
||||
async getState(): Promise<StateResponse | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
const apiError = error as APIError
|
||||
// 404 means no state exists yet - that's okay
|
||||
if (apiError.status === 404) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
/** Build a FetchContext for passing to domain helpers */
|
||||
private get ctx(): FetchContext {
|
||||
return {
|
||||
baseUrl: this.baseUrl,
|
||||
tenantId: this.tenantId,
|
||||
projectId: this.projectId,
|
||||
fetchWithRetry: this.fetchWithRetry.bind(this),
|
||||
fetchWithTimeout: this.fetchWithTimeout.bind(this),
|
||||
createError: this.createError.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SDK state for the current tenant
|
||||
* Supports optimistic locking via version parameter
|
||||
*/
|
||||
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(version !== undefined && { 'If-Match': String(version) }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
projectId: this.projectId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success) {
|
||||
throw this.createError(response.error || 'Failed to save state', 500, true)
|
||||
}
|
||||
|
||||
return response.data!
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
async deleteState(): Promise<void> {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Checkpoint Validation
|
||||
// State Management (api-client-state.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate a specific checkpoint
|
||||
*/
|
||||
async validateCheckpoint(
|
||||
checkpointId: string,
|
||||
data?: unknown
|
||||
): Promise<CheckpointValidationResult> {
|
||||
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
|
||||
`${this.baseUrl}/checkpoints/validate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
checkpointId,
|
||||
data,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw this.createError(response.error || 'Checkpoint validation failed', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all checkpoint statuses
|
||||
*/
|
||||
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
|
||||
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
|
||||
`${this.baseUrl}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response.data || {}
|
||||
}
|
||||
async getState(): Promise<StateResponse | null> { return stateHelpers.getState(this.ctx) }
|
||||
async saveState(state: SDKState, version?: number): Promise<StateResponse> { return stateHelpers.saveState(this.ctx, state, version) }
|
||||
async deleteState(): Promise<void> { return stateHelpers.deleteState(this.ctx) }
|
||||
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> { return stateHelpers.exportState(this.ctx, format) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Flow Navigation
|
||||
// Checkpoints & Flow (api-client-operations.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get current flow state
|
||||
*/
|
||||
async getFlowState(): Promise<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}> {
|
||||
const response = await this.fetchWithRetry<APIResponse<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}>>(
|
||||
`${this.baseUrl}/flow?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw this.createError('Failed to get flow state', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next/previous step
|
||||
*/
|
||||
async navigateFlow(direction: 'next' | 'previous'): Promise<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}> {
|
||||
const response = await this.fetchWithRetry<APIResponse<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}>>(
|
||||
`${this.baseUrl}/flow`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
direction,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw this.createError('Failed to navigate flow', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
async validateCheckpoint(checkpointId: string, data?: unknown): Promise<CheckpointValidationResult> { return opsHelpers.validateCheckpoint(this.ctx, checkpointId, data) }
|
||||
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> { return opsHelpers.getCheckpoints(this.ctx) }
|
||||
async getFlowState() { return opsHelpers.getFlowState(this.ctx) }
|
||||
async navigateFlow(direction: 'next' | 'previous') { return opsHelpers.navigateFlow(this.ctx, direction) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Modules
|
||||
// Modules, UCCA, Import, Screening, Health (api-client-operations.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get available compliance modules from backend
|
||||
*/
|
||||
async getModules(filters?: {
|
||||
serviceType?: string
|
||||
criticality?: string
|
||||
processesPii?: boolean
|
||||
aiComponents?: boolean
|
||||
}): Promise<{ modules: unknown[]; total: number }> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.serviceType) params.set('service_type', filters.serviceType)
|
||||
if (filters?.criticality) params.set('criticality', filters.criticality)
|
||||
if (filters?.processesPii !== undefined) params.set('processes_pii', String(filters.processesPii))
|
||||
if (filters?.aiComponents !== undefined) params.set('ai_components', String(filters.aiComponents))
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${this.baseUrl}/modules${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await this.fetchWithRetry<{ modules: unknown[]; total: number }>(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
async getModules(filters?: Parameters<typeof opsHelpers.getModules>[1]) { return opsHelpers.getModules(this.ctx, filters) }
|
||||
async assessUseCase(intake: unknown) { return opsHelpers.assessUseCase(this.ctx, intake) }
|
||||
async getAssessments() { return opsHelpers.getAssessments(this.ctx) }
|
||||
async getAssessment(id: string) { return opsHelpers.getAssessment(this.ctx, id) }
|
||||
async deleteAssessment(id: string) { return opsHelpers.deleteAssessment(this.ctx, id) }
|
||||
async analyzeDocument(formData: FormData) { return opsHelpers.analyzeDocument(this.ctx, formData) }
|
||||
async scanDependencies(formData: FormData) { return opsHelpers.scanDependencies(this.ctx, formData) }
|
||||
async healthCheck() { return opsHelpers.healthCheck(this.ctx) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - UCCA (Use Case Compliance Assessment)
|
||||
// Projects (api-client-projects.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Assess a use case
|
||||
*/
|
||||
async assessUseCase(intake: unknown): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/ucca/assess`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify(intake),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assessments
|
||||
*/
|
||||
async getAssessments(): Promise<unknown[]> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown[]>>(
|
||||
`${this.baseUrl}/ucca/assessments?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single assessment
|
||||
*/
|
||||
async getAssessment(id: string): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an assessment
|
||||
*/
|
||||
async deleteAssessment(id: string): Promise<void> {
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
async listProjects(includeArchived = true) { return projectHelpers.listProjects(this.ctx, includeArchived) }
|
||||
async createProject(data: Parameters<typeof projectHelpers.createProject>[1]) { return projectHelpers.createProject(this.ctx, data) }
|
||||
async updateProject(projectId: string, data: Parameters<typeof projectHelpers.updateProject>[2]) { return projectHelpers.updateProject(this.ctx, projectId, data) }
|
||||
async getProject(projectId: string) { return projectHelpers.getProject(this.ctx, projectId) }
|
||||
async archiveProject(projectId: string) { return projectHelpers.archiveProject(this.ctx, projectId) }
|
||||
async restoreProject(projectId: string) { return projectHelpers.restoreProject(this.ctx, projectId) }
|
||||
async permanentlyDeleteProject(projectId: string) { return projectHelpers.permanentlyDeleteProject(this.ctx, projectId) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Document Import
|
||||
// Wiki (api-client-wiki.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Analyze an uploaded document
|
||||
*/
|
||||
async analyzeDocument(formData: FormData): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/import/analyze`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
async listWikiCategories() { return wikiHelpers.listWikiCategories(this.ctx) }
|
||||
async listWikiArticles(categoryId?: string) { return wikiHelpers.listWikiArticles(this.ctx, categoryId) }
|
||||
async getWikiArticle(id: string) { return wikiHelpers.getWikiArticle(this.ctx, id) }
|
||||
async searchWiki(query: string) { return wikiHelpers.searchWiki(this.ctx, query) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - System Screening
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Scan a dependency file (package-lock.json, requirements.txt, etc.)
|
||||
*/
|
||||
async scanDependencies(formData: FormData): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/screening/scan`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Export SDK state in various formats
|
||||
*/
|
||||
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> {
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': format === 'json' ? 'application/json' : format === 'pdf' ? 'application/pdf' : 'application/zip',
|
||||
},
|
||||
},
|
||||
`export-${Date.now()}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw this.createError(`Export failed: ${response.statusText}`, response.status, true)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cancel all pending requests
|
||||
*/
|
||||
cancelAllRequests(): void {
|
||||
this.abortControllers.forEach(controller => controller.abort())
|
||||
this.abortControllers.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tenant ID (useful when switching contexts)
|
||||
*/
|
||||
setTenantId(tenantId: string): void {
|
||||
this.tenantId = tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current tenant ID
|
||||
*/
|
||||
getTenantId(): string {
|
||||
return this.tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project ID for multi-project support
|
||||
*/
|
||||
setProjectId(projectId: string | undefined): void {
|
||||
this.projectId = projectId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current project ID
|
||||
*/
|
||||
getProjectId(): string | undefined {
|
||||
return this.projectId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Project Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List all projects for the current tenant
|
||||
*/
|
||||
async listProjects(includeArchived = true): Promise<{ projects: ProjectInfo[]; total: number }> {
|
||||
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}&include_archived=${includeArchived}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
async createProject(data: {
|
||||
name: string
|
||||
description?: string
|
||||
customer_type?: string
|
||||
copy_from_project_id?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
async updateProject(projectId: string, data: {
|
||||
name?: string
|
||||
description?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single project by ID
|
||||
*/
|
||||
async getProject(projectId: string): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-delete) a project
|
||||
*/
|
||||
async archiveProject(projectId: string): Promise<void> {
|
||||
await this.fetchWithRetry<{ success: boolean }>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an archived project
|
||||
*/
|
||||
async restoreProject(projectId: string): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a project and all data
|
||||
*/
|
||||
async permanentlyDeleteProject(projectId: string): Promise<void> {
|
||||
await this.fetchWithRetry<{ success: boolean }>(
|
||||
`${this.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WIKI (read-only knowledge base)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* List all wiki categories with article counts
|
||||
*/
|
||||
async listWikiCategories(): Promise<WikiCategory[]> {
|
||||
const data = await this.fetchWithRetry<{ categories: Array<{
|
||||
id: string; name: string; description: string; icon: string;
|
||||
sort_order: number; article_count: number
|
||||
}> }>(
|
||||
`${this.baseUrl}/wiki?endpoint=categories`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.categories || []).map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
icon: c.icon,
|
||||
sortOrder: c.sort_order,
|
||||
articleCount: c.article_count,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* List wiki articles, optionally filtered by category
|
||||
*/
|
||||
async listWikiArticles(categoryId?: string): Promise<WikiArticle[]> {
|
||||
const params = new URLSearchParams({ endpoint: 'articles' })
|
||||
if (categoryId) params.set('category_id', categoryId)
|
||||
const data = await this.fetchWithRetry<{ articles: Array<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}> }>(
|
||||
`${this.baseUrl}/wiki?${params.toString()}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.articles || []).map(a => ({
|
||||
id: a.id,
|
||||
categoryId: a.category_id,
|
||||
categoryName: a.category_name,
|
||||
title: a.title,
|
||||
summary: a.summary,
|
||||
content: a.content,
|
||||
legalRefs: a.legal_refs || [],
|
||||
tags: a.tags || [],
|
||||
relevance: a.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: a.source_urls || [],
|
||||
version: a.version,
|
||||
updatedAt: a.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single wiki article by ID
|
||||
*/
|
||||
async getWikiArticle(id: string): Promise<WikiArticle> {
|
||||
const data = await this.fetchWithRetry<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}>(
|
||||
`${this.baseUrl}/wiki?endpoint=article&id=${encodeURIComponent(id)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return {
|
||||
id: data.id,
|
||||
categoryId: data.category_id,
|
||||
categoryName: data.category_name,
|
||||
title: data.title,
|
||||
summary: data.summary,
|
||||
content: data.content,
|
||||
legalRefs: data.legal_refs || [],
|
||||
tags: data.tags || [],
|
||||
relevance: data.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: data.source_urls || [],
|
||||
version: data.version,
|
||||
updatedAt: data.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-text search across wiki articles
|
||||
*/
|
||||
async searchWiki(query: string): Promise<WikiSearchResult[]> {
|
||||
const data = await this.fetchWithRetry<{ results: Array<{
|
||||
id: string; title: string; summary: string; category_name: string;
|
||||
relevance: string; highlight: string
|
||||
}> }>(
|
||||
`${this.baseUrl}/wiki?endpoint=search&q=${encodeURIComponent(query)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.results || []).map(r => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
summary: r.summary,
|
||||
categoryName: r.category_name,
|
||||
relevance: r.relevance,
|
||||
highlight: r.highlight,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/health`,
|
||||
{ method: 'GET' },
|
||||
`health-${Date.now()}`
|
||||
)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
setTenantId(tenantId: string): void { this.tenantId = tenantId }
|
||||
getTenantId(): string { return this.tenantId }
|
||||
setProjectId(projectId: string | undefined): void { this.projectId = projectId }
|
||||
getProjectId(): string | undefined { return this.projectId }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
383
admin-compliance/lib/sdk/api-docs/endpoints-go.ts
Normal file
383
admin-compliance/lib/sdk/api-docs/endpoints-go.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Go/Gin endpoints — AI Compliance SDK service modules
|
||||
* (health, rbac, llm, go-audit, ucca, rag, roadmaps, roadmap-items,
|
||||
* workshops, portfolios, academy, training, whistleblower, iace)
|
||||
*/
|
||||
import { ApiModule } from './types'
|
||||
|
||||
export const goModules: ApiModule[] = [
|
||||
{
|
||||
id: 'go-health',
|
||||
name: 'Health — System-Status',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1',
|
||||
exposure: 'admin',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/health', description: 'API Health-Check', service: 'go', exposure: 'admin' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'rbac',
|
||||
name: 'RBAC — Tenant, Rollen & Berechtigungen',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/tenants', description: 'Alle Tenants auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/tenants/:id', description: 'Tenant laden', service: 'go' },
|
||||
{ method: 'POST', path: '/tenants', description: 'Tenant erstellen', service: 'go' },
|
||||
{ method: 'PUT', path: '/tenants/:id', description: 'Tenant aktualisieren', service: 'go' },
|
||||
{ method: 'GET', path: '/tenants/:id/namespaces', description: 'Namespaces auflisten', service: 'go' },
|
||||
{ method: 'POST', path: '/tenants/:id/namespaces', description: 'Namespace erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/namespaces/:id', description: 'Namespace laden', service: 'go' },
|
||||
{ method: 'GET', path: '/roles', description: 'Rollen auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/roles/system', description: 'System-Rollen auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/roles/:id', description: 'Rolle laden', service: 'go' },
|
||||
{ method: 'POST', path: '/roles', description: 'Rolle erstellen', service: 'go' },
|
||||
{ method: 'POST', path: '/user-roles', description: 'Rolle zuweisen', service: 'go' },
|
||||
{ method: 'DELETE', path: '/user-roles/:userId/:roleId', description: 'Rolle entziehen', service: 'go' },
|
||||
{ method: 'GET', path: '/user-roles/:userId', description: 'Benutzer-Rollen laden', service: 'go' },
|
||||
{ method: 'GET', path: '/permissions/effective', description: 'Effektive Berechtigungen laden', service: 'go' },
|
||||
{ method: 'GET', path: '/permissions/context', description: 'Benutzerkontext laden', service: 'go' },
|
||||
{ method: 'GET', path: '/permissions/check', description: 'Berechtigung pruefen', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'llm',
|
||||
name: 'LLM — KI-Textverarbeitung & Policies',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/llm',
|
||||
exposure: 'partner',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/policies', description: 'LLM-Policies auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/policies/:id', description: 'Policy laden', service: 'go' },
|
||||
{ method: 'POST', path: '/policies', description: 'Policy erstellen', service: 'go' },
|
||||
{ method: 'PUT', path: '/policies/:id', description: 'Policy aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/policies/:id', description: 'Policy loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/chat', description: 'Chat Completion', service: 'go' },
|
||||
{ method: 'POST', path: '/complete', description: 'Text Completion', service: 'go' },
|
||||
{ method: 'GET', path: '/models', description: 'Verfuegbare Modelle auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/providers/status', description: 'Provider-Status laden', service: 'go' },
|
||||
{ method: 'POST', path: '/analyze', description: 'Text analysieren', service: 'go' },
|
||||
{ method: 'POST', path: '/redact', description: 'PII schwaerzen', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'go-audit',
|
||||
name: 'Audit (Go) — LLM-Audit & Compliance-Reports',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/audit',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/llm', description: 'LLM-Audit-Logs laden', service: 'go' },
|
||||
{ method: 'GET', path: '/general', description: 'Allgemeine Audit-Logs laden', service: 'go' },
|
||||
{ method: 'GET', path: '/llm-operations', description: 'LLM-Operationen laden (Alias)', service: 'go' },
|
||||
{ method: 'GET', path: '/trail', description: 'Audit-Trail laden (Alias)', service: 'go' },
|
||||
{ method: 'GET', path: '/usage', description: 'Nutzungsstatistiken laden', service: 'go' },
|
||||
{ method: 'GET', path: '/compliance-report', description: 'Compliance-Report laden', service: 'go' },
|
||||
{ method: 'GET', path: '/export/llm', description: 'LLM-Audit exportieren', service: 'go' },
|
||||
{ method: 'GET', path: '/export/general', description: 'Allgemeines Audit exportieren', service: 'go' },
|
||||
{ method: 'GET', path: '/export/compliance', description: 'Compliance-Report exportieren', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ucca',
|
||||
name: 'UCCA — Use-Case Compliance Advisor',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/ucca',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/assess', description: 'Compliance-Bewertung durchfuehren', service: 'go' },
|
||||
{ method: 'GET', path: '/assessments', description: 'Bewertungen auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/assessments/:id', description: 'Bewertung laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/assessments/:id', description: 'Bewertung aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/assessments/:id', description: 'Bewertung loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/assessments/:id/explain', description: 'KI-Erklaerung generieren', service: 'go' },
|
||||
{ method: 'GET', path: '/patterns', description: 'Compliance-Muster laden', service: 'go' },
|
||||
{ method: 'GET', path: '/examples', description: 'Beispiele laden', service: 'go' },
|
||||
{ method: 'GET', path: '/rules', description: 'Compliance-Regeln laden', service: 'go' },
|
||||
{ method: 'GET', path: '/controls', description: 'Controls laden', service: 'go' },
|
||||
{ method: 'GET', path: '/problem-solutions', description: 'Problem-Loesungs-Paare laden', service: 'go' },
|
||||
{ method: 'GET', path: '/export/:id', description: 'Bewertung exportieren', service: 'go' },
|
||||
{ method: 'GET', path: '/escalations', description: 'Eskalationen auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/escalations/stats', description: 'Eskalations-Statistiken laden', service: 'go' },
|
||||
{ method: 'GET', path: '/escalations/:id', description: 'Eskalation laden', service: 'go' },
|
||||
{ method: 'POST', path: '/escalations', description: 'Eskalation erstellen', service: 'go' },
|
||||
{ method: 'POST', path: '/escalations/:id/assign', description: 'Eskalation zuweisen', service: 'go' },
|
||||
{ method: 'POST', path: '/escalations/:id/review', description: 'Review starten', service: 'go' },
|
||||
{ method: 'POST', path: '/escalations/:id/decide', description: 'Entscheidung treffen', service: 'go' },
|
||||
{ method: 'POST', path: '/obligations/assess', description: 'Pflichten bewerten', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/:assessmentId', description: 'Bewertungsergebnis laden', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/:assessmentId/by-regulation', description: 'Nach Regulierung gruppiert', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/:assessmentId/by-deadline', description: 'Nach Frist gruppiert', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/:assessmentId/by-responsible', description: 'Nach Verantwortlichem gruppiert', service: 'go' },
|
||||
{ method: 'POST', path: '/obligations/export/memo', description: 'C-Level-Memo exportieren', service: 'go' },
|
||||
{ method: 'POST', path: '/obligations/export/direct', description: 'Uebersicht direkt exportieren', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/regulations', description: 'Regulierungen laden', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/regulations/:regulationId/decision-tree', description: 'Entscheidungsbaum laden', service: 'go' },
|
||||
{ method: 'POST', path: '/obligations/quick-check', description: 'Schnell-Check durchfuehren', service: 'go' },
|
||||
{ method: 'POST', path: '/obligations/assess-from-scope', description: 'Aus Scope bewerten', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/tom-controls/for-obligation/:obligationId', description: 'TOM-Controls fuer Pflicht laden', service: 'go' },
|
||||
{ method: 'POST', path: '/obligations/gap-analysis', description: 'TOM-Gap-Analyse durchfuehren', service: 'go' },
|
||||
{ method: 'GET', path: '/obligations/tom-controls/:controlId/obligations', description: 'Pflichten fuer TOM-Control laden', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'rag',
|
||||
name: 'RAG — Legal Corpus & Vektorsuche',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/rag',
|
||||
exposure: 'partner',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/search', description: 'Rechtskorpus durchsuchen', service: 'go' },
|
||||
{ method: 'GET', path: '/regulations', description: 'Regulierungen auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/corpus-status', description: 'Indexierungsstatus laden', service: 'go' },
|
||||
{ method: 'GET', path: '/corpus-versions/:collection', description: 'Versionshistorie laden', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'roadmaps',
|
||||
name: 'Roadmaps — Compliance-Implementierungsplaene',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/roadmaps',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/', description: 'Roadmap erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/', description: 'Roadmaps auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/:id', description: 'Roadmap laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/:id', description: 'Roadmap aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/:id', description: 'Roadmap loeschen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/stats', description: 'Roadmap-Statistiken laden', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/items', description: 'Item erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/items', description: 'Items auflisten', service: 'go' },
|
||||
{ method: 'POST', path: '/import/upload', description: 'Import hochladen', service: 'go' },
|
||||
{ method: 'GET', path: '/import/:jobId', description: 'Import-Status laden', service: 'go' },
|
||||
{ method: 'POST', path: '/import/:jobId/confirm', description: 'Import bestaetigen', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'roadmap-items',
|
||||
name: 'Roadmap Items — Einzelne Massnahmen',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/roadmap-items',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/:id', description: 'Item laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/:id', description: 'Item aktualisieren', service: 'go' },
|
||||
{ method: 'PATCH', path: '/:id/status', description: 'Item-Status aendern', service: 'go' },
|
||||
{ method: 'DELETE', path: '/:id', description: 'Item loeschen', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'workshops',
|
||||
name: 'Workshops — Kollaborative Compliance-Workshops',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/workshops',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/', description: 'Workshop erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/', description: 'Workshops auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/:id', description: 'Workshop laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/:id', description: 'Workshop aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/:id', description: 'Workshop loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/start', description: 'Workshop starten', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/pause', description: 'Workshop pausieren', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/complete', description: 'Workshop abschliessen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/participants', description: 'Teilnehmer auflisten', service: 'go' },
|
||||
{ method: 'PUT', path: '/:id/participants/:participantId', description: 'Teilnehmer aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/:id/participants/:participantId', description: 'Teilnehmer entfernen', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/responses', description: 'Antwort einreichen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/responses', description: 'Antworten laden', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/comments', description: 'Kommentar hinzufuegen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/comments', description: 'Kommentare laden', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/advance', description: 'Zum naechsten Schritt', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/goto', description: 'Zu bestimmtem Schritt springen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/stats', description: 'Workshop-Statistiken laden', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/summary', description: 'Zusammenfassung laden', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/export', description: 'Workshop exportieren', service: 'go' },
|
||||
{ method: 'POST', path: '/join/:code', description: 'Per Zugangscode beitreten', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'portfolios',
|
||||
name: 'Portfolios — KI-Use-Case-Portfolio',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/portfolios',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/', description: 'Portfolio erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/', description: 'Portfolios auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/:id', description: 'Portfolio laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/:id', description: 'Portfolio aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/:id', description: 'Portfolio loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/items', description: 'Item hinzufuegen', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/items', description: 'Items auflisten', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/items/bulk', description: 'Items Bulk-Import', service: 'go' },
|
||||
{ method: 'DELETE', path: '/:id/items/:itemId', description: 'Item entfernen', service: 'go' },
|
||||
{ method: 'PUT', path: '/:id/items/order', description: 'Items sortieren', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/stats', description: 'Portfolio-Statistiken laden', service: 'go' },
|
||||
{ method: 'GET', path: '/:id/activity', description: 'Aktivitaets-Log laden', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/recalculate', description: 'Metriken neu berechnen', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/submit-review', description: 'Zur Pruefung einreichen', service: 'go' },
|
||||
{ method: 'POST', path: '/:id/approve', description: 'Portfolio genehmigen', service: 'go' },
|
||||
{ method: 'POST', path: '/merge', description: 'Portfolios zusammenfuehren', service: 'go' },
|
||||
{ method: 'POST', path: '/compare', description: 'Portfolios vergleichen', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'academy',
|
||||
name: 'Academy — E-Learning & Zertifikate',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/academy',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/courses', description: 'Kurs erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/courses', description: 'Kurse auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/courses/:id', description: 'Kurs laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/courses/:id', description: 'Kurs aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/courses/:id', description: 'Kurs loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/enrollments', description: 'Einschreibung erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/enrollments', description: 'Einschreibungen auflisten', service: 'go' },
|
||||
{ method: 'PUT', path: '/enrollments/:id/progress', description: 'Fortschritt aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/enrollments/:id/complete', description: 'Einschreibung abschliessen', service: 'go' },
|
||||
{ method: 'GET', path: '/certificates/:id', description: 'Zertifikat laden', service: 'go' },
|
||||
{ method: 'POST', path: '/enrollments/:id/certificate', description: 'Zertifikat generieren', service: 'go' },
|
||||
{ method: 'GET', path: '/certificates/:id/pdf', description: 'Zertifikat-PDF herunterladen', service: 'go' },
|
||||
{ method: 'POST', path: '/courses/:id/quiz', description: 'Quiz einreichen', service: 'go' },
|
||||
{ method: 'PUT', path: '/lessons/:id', description: 'Lektion aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/lessons/:id/quiz-test', description: 'Quiz testen', service: 'go' },
|
||||
{ method: 'GET', path: '/stats', description: 'Academy-Statistiken laden', service: 'go' },
|
||||
{ method: 'POST', path: '/courses/generate', description: 'Kurs aus Modul generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/courses/generate-all', description: 'Alle Kurse generieren', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'training',
|
||||
name: 'Training — Schulungsmodule & Content-Pipeline',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/training',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/modules', description: 'Schulungsmodule auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/modules/:id', description: 'Modul laden', service: 'go' },
|
||||
{ method: 'POST', path: '/modules', description: 'Modul erstellen', service: 'go' },
|
||||
{ method: 'PUT', path: '/modules/:id', description: 'Modul aktualisieren', service: 'go' },
|
||||
{ method: 'GET', path: '/matrix', description: 'Schulungsmatrix laden', service: 'go' },
|
||||
{ method: 'GET', path: '/matrix/:role', description: 'Matrix fuer Rolle laden', service: 'go' },
|
||||
{ method: 'POST', path: '/matrix', description: 'Matrix-Eintrag setzen', service: 'go' },
|
||||
{ method: 'DELETE', path: '/matrix/:role/:moduleId', description: 'Matrix-Eintrag loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/assignments/compute', description: 'Zuweisungen berechnen', service: 'go' },
|
||||
{ method: 'GET', path: '/assignments', description: 'Zuweisungen auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/assignments/:id', description: 'Zuweisung laden', service: 'go' },
|
||||
{ method: 'POST', path: '/assignments/:id/start', description: 'Zuweisung starten', service: 'go' },
|
||||
{ method: 'POST', path: '/assignments/:id/progress', description: 'Fortschritt aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/assignments/:id/complete', description: 'Zuweisung abschliessen', service: 'go' },
|
||||
{ method: 'GET', path: '/quiz/:moduleId', description: 'Quiz laden', service: 'go' },
|
||||
{ method: 'POST', path: '/quiz/:moduleId/submit', description: 'Quiz einreichen', service: 'go' },
|
||||
{ method: 'GET', path: '/quiz/attempts/:assignmentId', description: 'Quiz-Versuche laden', service: 'go' },
|
||||
{ method: 'POST', path: '/content/generate', description: 'Inhalt generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/content/generate-quiz', description: 'Quiz generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/content/generate-all', description: 'Alle Inhalte generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/content/generate-all-quiz', description: 'Alle Quizze generieren', service: 'go' },
|
||||
{ method: 'GET', path: '/content/:moduleId', description: 'Modul-Inhalt laden', service: 'go' },
|
||||
{ method: 'POST', path: '/content/:moduleId/publish', description: 'Inhalt veroeffentlichen', service: 'go' },
|
||||
{ method: 'POST', path: '/content/:moduleId/generate-audio', description: 'Audio generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/content/:moduleId/generate-video', description: 'Video generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/content/:moduleId/preview-script', description: 'Video-Script Vorschau', service: 'go' },
|
||||
{ method: 'GET', path: '/media/module/:moduleId', description: 'Medien fuer Modul laden', service: 'go' },
|
||||
{ method: 'GET', path: '/media/:mediaId/url', description: 'Medien-URL laden', service: 'go' },
|
||||
{ method: 'POST', path: '/media/:mediaId/publish', description: 'Medium veroeffentlichen', service: 'go' },
|
||||
{ method: 'GET', path: '/deadlines', description: 'Fristen laden', service: 'go' },
|
||||
{ method: 'GET', path: '/deadlines/overdue', description: 'Ueberfaellige Fristen laden', service: 'go' },
|
||||
{ method: 'POST', path: '/escalation/check', description: 'Eskalation pruefen', service: 'go' },
|
||||
{ method: 'GET', path: '/audit-log', description: 'Schulungs-Audit-Log laden', service: 'go' },
|
||||
{ method: 'GET', path: '/stats', description: 'Schulungs-Statistiken laden', service: 'go' },
|
||||
{ method: 'GET', path: '/certificates/:id/verify', description: 'Zertifikat verifizieren', service: 'go', exposure: 'partner' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'whistleblower',
|
||||
name: 'Whistleblower — Hinweisgebersystem (HinSchG)',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/whistleblower',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/reports/submit', description: 'Anonymen Hinweis einreichen', service: 'go', exposure: 'public' },
|
||||
{ method: 'GET', path: '/reports/access/:accessKey', description: 'Hinweis per Zugangscode laden', service: 'go', exposure: 'public' },
|
||||
{ method: 'POST', path: '/reports/access/:accessKey/messages', description: 'Nachricht senden (anonym)', service: 'go', exposure: 'public' },
|
||||
{ method: 'GET', path: '/reports', description: 'Alle Hinweise auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/reports/:id', description: 'Hinweis laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/reports/:id', description: 'Hinweis aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/reports/:id', description: 'Hinweis loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/reports/:id/acknowledge', description: 'Eingangsbestaetigung senden', service: 'go' },
|
||||
{ method: 'POST', path: '/reports/:id/investigate', description: 'Untersuchung starten', service: 'go' },
|
||||
{ method: 'POST', path: '/reports/:id/measures', description: 'Abhilfemassnahme hinzufuegen', service: 'go' },
|
||||
{ method: 'POST', path: '/reports/:id/close', description: 'Hinweis schliessen', service: 'go' },
|
||||
{ method: 'POST', path: '/reports/:id/messages', description: 'Admin-Nachricht senden', service: 'go' },
|
||||
{ method: 'GET', path: '/reports/:id/messages', description: 'Nachrichten laden', service: 'go' },
|
||||
{ method: 'GET', path: '/stats', description: 'Whistleblower-Statistiken laden', service: 'go' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'iace',
|
||||
name: 'IACE — Industrial AI / CE-Compliance Engine',
|
||||
service: 'go',
|
||||
basePath: '/sdk/v1/iace',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/hazard-library', description: 'Gefahrenbibliothek laden', service: 'go' },
|
||||
{ method: 'GET', path: '/controls-library', description: 'Controls-Bibliothek laden', service: 'go' },
|
||||
{ method: 'POST', path: '/projects', description: 'Projekt erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/projects', description: 'Projekte auflisten', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id', description: 'Projekt laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/projects/:id', description: 'Projekt aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/projects/:id', description: 'Projekt archivieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/init-from-profile', description: 'Aus Unternehmensprofil initialisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/completeness-check', description: 'Vollstaendigkeits-Check durchfuehren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/components', description: 'Komponente erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/components', description: 'Komponenten auflisten', service: 'go' },
|
||||
{ method: 'PUT', path: '/projects/:id/components/:cid', description: 'Komponente aktualisieren', service: 'go' },
|
||||
{ method: 'DELETE', path: '/projects/:id/components/:cid', description: 'Komponente loeschen', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/classify', description: 'Regulatorisch klassifizieren', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/classifications', description: 'Klassifizierungen laden', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/classify/:regulation', description: 'Fuer einzelne Regulierung klassifizieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/hazards', description: 'Gefaehrdung erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/hazards', description: 'Gefaehrdungen auflisten', service: 'go' },
|
||||
{ method: 'PUT', path: '/projects/:id/hazards/:hid', description: 'Gefaehrdung aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/hazards/suggest', description: 'KI-Gefaehrdungsvorschlaege generieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/hazards/:hid/assess', description: 'Risiko bewerten', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/risk-summary', description: 'Risiko-Zusammenfassung laden', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/hazards/:hid/reassess', description: 'Risiko neu bewerten', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/hazards/:hid/mitigations', description: 'Risikominderung erstellen', service: 'go' },
|
||||
{ method: 'PUT', path: '/mitigations/:mid', description: 'Risikominderung aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/mitigations/:mid/verify', description: 'Risikominderung verifizieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/evidence', description: 'Nachweis hochladen', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/evidence', description: 'Nachweise auflisten', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/verification-plan', description: 'Verifizierungsplan erstellen', service: 'go' },
|
||||
{ method: 'PUT', path: '/verification-plan/:vid', description: 'Plan aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/verification-plan/:vid/complete', description: 'Verifizierung abschliessen', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/tech-file/generate', description: 'Technische Akte generieren', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/tech-file', description: 'Akte-Abschnitte laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/projects/:id/tech-file/:section', description: 'Abschnitt aktualisieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/tech-file/:section/approve', description: 'Abschnitt genehmigen', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/tech-file/export', description: 'Technische Akte exportieren', service: 'go' },
|
||||
{ method: 'POST', path: '/projects/:id/monitoring', description: 'Monitoring-Event erstellen', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/monitoring', description: 'Monitoring-Events laden', service: 'go' },
|
||||
{ method: 'PUT', path: '/projects/:id/monitoring/:eid', description: 'Event aktualisieren', service: 'go' },
|
||||
{ method: 'GET', path: '/projects/:id/audit-trail', description: 'Projekt-Audit-Trail laden', service: 'go' },
|
||||
],
|
||||
},
|
||||
]
|
||||
191
admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts
Normal file
191
admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Python/FastAPI endpoints — Core compliance modules
|
||||
* (framework, audit, change-requests, company-profile, projects,
|
||||
* compliance-scope, dashboard, generation, extraction, modules)
|
||||
*/
|
||||
import { ApiModule } from './types'
|
||||
|
||||
export const pythonCoreModules: ApiModule[] = [
|
||||
{
|
||||
id: 'compliance-framework',
|
||||
name: 'Compliance Framework — Regulierungen, Anforderungen & Controls',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/regulations', description: 'Alle Regulierungen auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/regulations/{code}', description: 'Regulierung nach Code laden', service: 'python' },
|
||||
{ method: 'GET', path: '/regulations/{code}/requirements', description: 'Anforderungen einer Regulierung', service: 'python' },
|
||||
{ method: 'GET', path: '/requirements', description: 'Anforderungen auflisten (paginiert)', service: 'python' },
|
||||
{ method: 'GET', path: '/requirements/{requirement_id}', description: 'Einzelne Anforderung laden', service: 'python' },
|
||||
{ method: 'POST', path: '/requirements', description: 'Anforderung erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/requirements/{requirement_id}', description: 'Anforderung aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/requirements/{requirement_id}', description: 'Anforderung loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/controls', description: 'Alle Controls auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/controls/paginated', description: 'Controls paginiert laden', service: 'python' },
|
||||
{ method: 'GET', path: '/controls/{control_id}', description: 'Einzelnes Control laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/controls/{control_id}', description: 'Control aktualisieren', service: 'python' },
|
||||
{ method: 'PUT', path: '/controls/{control_id}/review', description: 'Control-Review durchfuehren', service: 'python' },
|
||||
{ method: 'GET', path: '/controls/by-domain/{domain}', description: 'Controls nach Domain filtern', service: 'python' },
|
||||
{ method: 'POST', path: '/export', description: 'Audit-Export erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/export/{export_id}', description: 'Export-Status abfragen', service: 'python' },
|
||||
{ method: 'GET', path: '/export/{export_id}/download', description: 'Export-Datei herunterladen', service: 'python' },
|
||||
{ method: 'GET', path: '/exports', description: 'Alle Exports auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/init-tables', description: 'Datenbanktabellen initialisieren', service: 'python', exposure: 'admin' },
|
||||
{ method: 'POST', path: '/create-indexes', description: 'Datenbank-Indizes erstellen', service: 'python', exposure: 'admin' },
|
||||
{ method: 'POST', path: '/seed-risks', description: 'Risikodaten einspielen', service: 'python', exposure: 'admin' },
|
||||
{ method: 'POST', path: '/seed', description: 'Systemdaten einspielen', service: 'python', exposure: 'admin' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'audit',
|
||||
name: 'Audit — Sitzungen & Checklisten',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/audit',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/sessions', description: 'Audit-Sitzung erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/sessions', description: 'Alle Audit-Sitzungen auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/sessions/{session_id}', description: 'Sitzung laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/sessions/{session_id}/start', description: 'Sitzung starten', service: 'python' },
|
||||
{ method: 'PUT', path: '/sessions/{session_id}/complete', description: 'Sitzung abschliessen', service: 'python' },
|
||||
{ method: 'PUT', path: '/sessions/{session_id}/archive', description: 'Sitzung archivieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/sessions/{session_id}', description: 'Sitzung loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/sessions/{session_id}/report/pdf', description: 'Sitzungsbericht als PDF exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/checklist/{session_id}', description: 'Checkliste einer Sitzung laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/checklist/{session_id}/items/{requirement_id}/sign-off', description: 'Anforderung abzeichnen', service: 'python' },
|
||||
{ method: 'GET', path: '/checklist/{session_id}/items/{requirement_id}', description: 'Abzeichnung-Details laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ai-systems',
|
||||
name: 'AI Act — KI-Systeme & Risikobewertung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/ai',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/systems', description: 'KI-Systeme auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/systems', description: 'KI-System erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/systems/{system_id}', description: 'KI-System laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/systems/{system_id}', description: 'KI-System aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/systems/{system_id}', description: 'KI-System loeschen', service: 'python' },
|
||||
{ method: 'POST', path: '/systems/{system_id}/assess', description: 'KI-Compliance bewerten', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'change-requests',
|
||||
name: 'Change Requests — Aenderungsantraege',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/change-requests',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/stats', description: 'CR-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{cr_id}', description: 'Einzelnen CR laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{cr_id}/accept', description: 'CR akzeptieren', service: 'python' },
|
||||
{ method: 'POST', path: '/{cr_id}/reject', description: 'CR ablehnen', service: 'python' },
|
||||
{ method: 'POST', path: '/{cr_id}/edit', description: 'CR bearbeiten', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{cr_id}', description: 'CR loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'company-profile',
|
||||
name: 'Stammdaten — Unternehmensprofil',
|
||||
service: 'python',
|
||||
basePath: '/api/v1/company-profile',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Unternehmensprofil laden', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Profil erstellen/aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/', description: 'Profil loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/template-context', description: 'Profil als Template-Kontext (flach)', service: 'python' },
|
||||
{ method: 'GET', path: '/audit', description: 'Profil-Aenderungsprotokoll laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'projects',
|
||||
name: 'Projekte — Multi-Projekt-Verwaltung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/v1/projects',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Alle Projekte des Tenants auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Neues Projekt erstellen (optional mit Stammdaten-Kopie)', service: 'python' },
|
||||
{ method: 'GET', path: '/{project_id}', description: 'Einzelnes Projekt laden', service: 'python' },
|
||||
{ method: 'PATCH', path: '/{project_id}', description: 'Projekt aktualisieren (Name, Beschreibung)', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{project_id}', description: 'Projekt archivieren (Soft Delete)', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'compliance-scope',
|
||||
name: 'Compliance Scope — Geltungsbereich',
|
||||
service: 'python',
|
||||
basePath: '/api/v1/compliance-scope',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Compliance-Scope laden', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Compliance-Scope erstellen/aktualisieren', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dashboard',
|
||||
name: 'Dashboard — Compliance-Uebersicht & Reports',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/dashboard',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/dashboard', description: 'Haupt-Dashboard laden', service: 'python' },
|
||||
{ method: 'GET', path: '/score', description: 'Compliance-Score berechnen', service: 'python' },
|
||||
{ method: 'GET', path: '/dashboard/executive', description: 'Executive-Dashboard laden', service: 'python' },
|
||||
{ method: 'GET', path: '/dashboard/trend', description: 'Compliance-Trendverlauf laden', service: 'python' },
|
||||
{ method: 'GET', path: '/reports/summary', description: 'Zusammenfassungsbericht laden', service: 'python' },
|
||||
{ method: 'GET', path: '/reports/{period}', description: 'Periodenbericht generieren', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'generation',
|
||||
name: 'Dokumentengenerierung — Automatische Erstellung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/generation',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/preview/{doc_type}', description: 'Generierungs-Vorschau laden', service: 'python' },
|
||||
{ method: 'POST', path: '/apply/{doc_type}', description: 'Dokument generieren und anwenden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'extraction',
|
||||
name: 'Extraktion — Anforderungen aus RAG',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/extract-requirements-from-rag', description: 'Anforderungen aus RAG-Korpus extrahieren', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'modules',
|
||||
name: 'Module — Compliance-Modul-Verwaltung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/modules',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/modules', description: 'Module auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/modules/overview', description: 'Modul-Uebersicht laden', service: 'python' },
|
||||
{ method: 'GET', path: '/modules/{module_id}', description: 'Modul laden', service: 'python' },
|
||||
{ method: 'POST', path: '/modules/seed', description: 'Module einspielen', service: 'python', exposure: 'admin' },
|
||||
{ method: 'POST', path: '/modules/{module_id}/activate', description: 'Modul aktivieren', service: 'python' },
|
||||
{ method: 'POST', path: '/modules/{module_id}/deactivate', description: 'Modul deaktivieren', service: 'python' },
|
||||
{ method: 'POST', path: '/modules/{module_id}/regulations', description: 'Regulierungs-Zuordnung hinzufuegen', service: 'python' },
|
||||
],
|
||||
},
|
||||
]
|
||||
262
admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts
Normal file
262
admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Python/FastAPI endpoints — GDPR, DSR, consent, and data-subject modules
|
||||
* (banner, consent-templates, dsfa, dsr, einwilligungen, loeschfristen,
|
||||
* consent-user, consent-admin, dsr-user, dsr-admin, gdpr)
|
||||
*/
|
||||
import { ApiModule } from './types'
|
||||
|
||||
export const pythonGdprModules: ApiModule[] = [
|
||||
{
|
||||
id: 'banner',
|
||||
name: 'Cookie-Banner & Consent Management',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/consent',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/consent', description: 'Einwilligung erfassen', service: 'python', exposure: 'public' },
|
||||
{ method: 'GET', path: '/consent', description: 'Einwilligungen auflisten', service: 'python' },
|
||||
{ method: 'DELETE', path: '/consent/{consent_id}', description: 'Einwilligung loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/consent/export', description: 'Einwilligungsdaten exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/config/{site_id}', description: 'Seitenkonfiguration laden', service: 'python', exposure: 'public' },
|
||||
{ method: 'GET', path: '/admin/sites', description: 'Alle Seiten auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/admin/sites', description: 'Seite erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/admin/sites/{site_id}', description: 'Seite aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/admin/sites/{site_id}', description: 'Seite loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/admin/sites/{site_id}/categories', description: 'Cookie-Kategorien auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/admin/sites/{site_id}/categories', description: 'Cookie-Kategorie erstellen', service: 'python' },
|
||||
{ method: 'DELETE', path: '/admin/categories/{category_id}', description: 'Cookie-Kategorie loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/admin/sites/{site_id}/vendors', description: 'Anbieter auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/admin/sites/{site_id}/vendors', description: 'Anbieter hinzufuegen', service: 'python' },
|
||||
{ method: 'DELETE', path: '/admin/vendors/{vendor_id}', description: 'Anbieter loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/admin/stats/{site_id}', description: 'Seiten-Statistiken laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'consent-templates',
|
||||
name: 'Einwilligungsvorlagen — Consent Templates',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/consent-templates',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/consent-templates', description: 'Vorlagen auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/consent-templates', description: 'Vorlage erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/consent-templates/{template_id}', description: 'Vorlage aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/consent-templates/{template_id}', description: 'Vorlage loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/gdpr-processes', description: 'DSGVO-Prozesse auflisten', service: 'python' },
|
||||
{ method: 'PUT', path: '/gdpr-processes/{process_id}', description: 'DSGVO-Prozess aktualisieren', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dsfa',
|
||||
name: 'DSFA — Datenschutz-Folgenabschaetzung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/dsfa',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'DSFAs auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'DSFA erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsfa_id}', description: 'DSFA laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{dsfa_id}', description: 'DSFA aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{dsfa_id}', description: 'DSFA loeschen', service: 'python' },
|
||||
{ method: 'PATCH', path: '/{dsfa_id}/status', description: 'DSFA-Status aendern', service: 'python' },
|
||||
{ method: 'PUT', path: '/{dsfa_id}/sections/{section_number}', description: 'DSFA-Abschnitt aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsfa_id}/submit-for-review', description: 'Zur Pruefung einreichen', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsfa_id}/approve', description: 'DSFA genehmigen', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsfa_id}/export', description: 'DSFA als JSON exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsfa_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsfa_id}/versions/{version_number}', description: 'Bestimmte Version laden', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'DSFA-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/audit-log', description: 'DSFA-Audit-Log laden', service: 'python' },
|
||||
{ method: 'GET', path: '/export/csv', description: 'Alle DSFAs als CSV exportieren', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dsr',
|
||||
name: 'DSR — Betroffenenrechte (Admin)',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/dsr',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/', description: 'DSR erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/', description: 'DSRs auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}', description: 'DSR laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{dsr_id}', description: 'DSR aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{dsr_id}', description: 'DSR loeschen', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/status', description: 'Status aendern', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/verify-identity', description: 'Identitaet verifizieren', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/assign', description: 'DSR zuweisen', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/extend', description: 'Frist verlaengern', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/complete', description: 'DSR abschliessen', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/reject', description: 'DSR ablehnen', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}/history', description: 'Antragshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}/communications', description: 'Kommunikation laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/communicate', description: 'Nachricht senden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}/exception-checks', description: 'Ausnahme-Checks laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/exception-checks/init', description: 'Ausnahme-Checks initialisieren', service: 'python' },
|
||||
{ method: 'PUT', path: '/{dsr_id}/exception-checks/{check_id}', description: 'Ausnahme-Check aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'DSR-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/export', description: 'DSRs exportieren', service: 'python' },
|
||||
{ method: 'POST', path: '/deadlines/process', description: 'Fristen verarbeiten', service: 'python' },
|
||||
{ method: 'GET', path: '/templates', description: 'DSR-Vorlagen laden', service: 'python' },
|
||||
{ method: 'GET', path: '/templates/published', description: 'Veroeffentlichte Vorlagen laden', service: 'python' },
|
||||
{ method: 'GET', path: '/templates/{template_id}/versions', description: 'Vorlagen-Versionen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/templates/{template_id}/versions', description: 'Vorlagen-Version erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/template-versions/{version_id}/publish', description: 'Vorlagen-Version veroeffentlichen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'einwilligungen',
|
||||
name: 'Einwilligungen — DSGVO-Einwilligungsverwaltung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/einwilligungen',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/catalog', description: 'Einwilligungskatalog laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/catalog', description: 'Katalog aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/company', description: 'Unternehmens-Consent-Einstellungen laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/company', description: 'Einstellungen aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/cookies', description: 'Cookie-Einwilligungen laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/cookies', description: 'Cookie-Einwilligungen aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/consents/stats', description: 'Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/consents', description: 'Einwilligungen auflisten (paginiert)', service: 'python' },
|
||||
{ method: 'POST', path: '/consents', description: 'Einwilligung erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/consents/{consent_id}/history', description: 'Einwilligungshistorie laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/consents/{consent_id}/revoke', description: 'Einwilligung widerrufen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'loeschfristen',
|
||||
name: 'Loeschfristen — Aufbewahrung & Loeschung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/loeschfristen',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Loeschrichtlinien auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Richtlinie erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'Loeschfristen-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{policy_id}', description: 'Richtlinie laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{policy_id}', description: 'Richtlinie aktualisieren', service: 'python' },
|
||||
{ method: 'PUT', path: '/{policy_id}/status', description: 'Richtlinien-Status aendern', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{policy_id}', description: 'Richtlinie loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/{policy_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{policy_id}/versions/{version_number}', description: 'Bestimmte Version laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'consent-user',
|
||||
name: 'Consent API — Nutzer-Einwilligungen',
|
||||
service: 'python',
|
||||
basePath: '/api/consents',
|
||||
exposure: 'public',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/token/demo', description: 'Demo-Token laden', service: 'python' },
|
||||
{ method: 'GET', path: '/check/{document_type}', description: 'Einwilligungsstatus pruefen', service: 'python' },
|
||||
{ method: 'GET', path: '/pending', description: 'Offene Einwilligungen laden', service: 'python' },
|
||||
{ method: 'GET', path: '/documents/{document_type}/latest', description: 'Aktuellstes Dokument laden', service: 'python' },
|
||||
{ method: 'POST', path: '/give', description: 'Einwilligung erteilen', service: 'python' },
|
||||
{ method: 'GET', path: '/cookies/categories', description: 'Cookie-Kategorien laden', service: 'python' },
|
||||
{ method: 'POST', path: '/cookies', description: 'Cookie-Einwilligung setzen', service: 'python' },
|
||||
{ method: 'GET', path: '/privacy/my-data', description: 'Eigene Daten laden', service: 'python' },
|
||||
{ method: 'POST', path: '/privacy/export', description: 'Datenexport anfordern', service: 'python' },
|
||||
{ method: 'POST', path: '/privacy/delete', description: 'Datenlöschung anfordern', service: 'python' },
|
||||
{ method: 'GET', path: '/health', description: 'Health-Check', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'consent-admin',
|
||||
name: 'Consent Admin — Dokumenten- & Versionsverwaltung',
|
||||
service: 'python',
|
||||
basePath: '/api/admin/consents',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/documents', description: 'Dokumente auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/documents', description: 'Dokument erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/documents/{doc_id}', description: 'Dokument aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/documents/{doc_id}', description: 'Dokument loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/documents/{doc_id}/versions', description: 'Versionen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/versions', description: 'Version erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/archive', description: 'Version archivieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/versions/{version_id}', description: 'Version loeschen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/submit-review', description: 'Zur Pruefung einreichen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' },
|
||||
{ method: 'GET', path: '/versions/{version_id}/compare', description: 'Versionen vergleichen', service: 'python' },
|
||||
{ method: 'GET', path: '/versions/{version_id}/approval-history', description: 'Genehmigungshistorie laden', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/upload-word', description: 'Word-Dokument hochladen', service: 'python' },
|
||||
{ method: 'GET', path: '/scheduled-versions', description: 'Geplante Versionen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/scheduled-publishing/process', description: 'Geplante Veroeffentlichungen verarbeiten', service: 'python' },
|
||||
{ method: 'GET', path: '/cookies/categories', description: 'Cookie-Kategorien laden', service: 'python' },
|
||||
{ method: 'POST', path: '/cookies/categories', description: 'Kategorie erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/cookies/categories/{cat_id}', description: 'Kategorie aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/cookies/categories/{cat_id}', description: 'Kategorie loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/statistics', description: 'Admin-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/audit-log', description: 'Audit-Log laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dsr-user',
|
||||
name: 'DSR API — Nutzer-Betroffenenrechte',
|
||||
service: 'python',
|
||||
basePath: '/api/dsr',
|
||||
exposure: 'public',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/', description: 'Antrag stellen', service: 'python' },
|
||||
{ method: 'GET', path: '/', description: 'Eigene Antraege laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}', description: 'Antrag laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/cancel', description: 'Antrag stornieren', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dsr-admin',
|
||||
name: 'DSR Admin — Antrags-Verwaltung',
|
||||
service: 'python',
|
||||
basePath: '/api/admin/dsr',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Alle Antraege laden', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'DSR-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}', description: 'Antrag laden', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Antrag erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/{dsr_id}', description: 'Antrag aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/status', description: 'Status aendern', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/verify-identity', description: 'Identitaet verifizieren', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/assign', description: 'Zuweisen', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/extend', description: 'Frist verlaengern', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/complete', description: 'Abschliessen', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/reject', description: 'Ablehnen', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}/history', description: 'Historie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}/communications', description: 'Kommunikation laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/communicate', description: 'Nachricht senden', service: 'python' },
|
||||
{ method: 'GET', path: '/{dsr_id}/exception-checks', description: 'Ausnahme-Checks laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{dsr_id}/exception-checks/init', description: 'Checks initialisieren', service: 'python' },
|
||||
{ method: 'PUT', path: '/{dsr_id}/exception-checks/{check_id}', description: 'Check aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/deadlines/process', description: 'Fristen verarbeiten', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'gdpr',
|
||||
name: 'GDPR / Datenschutz — Nutzerdaten & Export',
|
||||
service: 'python',
|
||||
basePath: '/api/gdpr',
|
||||
exposure: 'public',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/export-pdf', description: 'Nutzerdaten als PDF exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/export-html', description: 'Nutzerdaten als HTML exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/data-categories', description: 'Datenkategorien laden', service: 'python' },
|
||||
{ method: 'GET', path: '/data-categories/{category}', description: 'Kategorie-Details laden', service: 'python' },
|
||||
{ method: 'POST', path: '/request-deletion', description: 'Datenlöschung beantragen', service: 'python' },
|
||||
],
|
||||
},
|
||||
]
|
||||
449
admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts
Normal file
449
admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* Python/FastAPI endpoints — Operational compliance modules
|
||||
* (tom, vvt, vendor-compliance, risks, evidence, incidents, escalations,
|
||||
* email-templates, legal-documents, legal-templates, import, screening,
|
||||
* scraper, source-policy, security-backlog, notfallplan, obligations,
|
||||
* isms, quality)
|
||||
*/
|
||||
import { ApiModule } from './types'
|
||||
|
||||
export const pythonOpsModules: ApiModule[] = [
|
||||
{
|
||||
id: 'email-templates',
|
||||
name: 'E-Mail-Vorlagen — Template-Verwaltung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/email-templates',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/types', description: 'Vorlagentypen laden', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'E-Mail-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/settings', description: 'E-Mail-Einstellungen laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/settings', description: 'E-Mail-Einstellungen aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/logs', description: 'Versandprotokoll laden', service: 'python' },
|
||||
{ method: 'POST', path: '/initialize', description: 'Standard-Vorlagen initialisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/', description: 'Vorlagen auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Vorlage erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/{template_id}', description: 'Vorlage laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{template_id}/versions', description: 'Vorlagen-Versionen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/{template_id}/versions', description: 'Version erstellen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions', description: 'Version erstellen (alternativ)', service: 'python' },
|
||||
{ method: 'GET', path: '/versions/{version_id}', description: 'Version laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/submit', description: 'Version einreichen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/preview', description: 'Version-Vorschau generieren', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/send-test', description: 'Test-E-Mail senden', service: 'python' },
|
||||
{ method: 'GET', path: '/default/{template_type}', description: 'Standard-Vorlage laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'escalations',
|
||||
name: 'Eskalationen — Eskalationsmanagement',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/escalations',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Eskalationen auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Eskalation erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'Eskalations-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{escalation_id}', description: 'Eskalation laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{escalation_id}', description: 'Eskalation aktualisieren', service: 'python' },
|
||||
{ method: 'PUT', path: '/{escalation_id}/status', description: 'Eskalations-Status aendern', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{escalation_id}', description: 'Eskalation loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'evidence',
|
||||
name: 'Nachweise — Evidence Management',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/evidence',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/evidence', description: 'Nachweise auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/evidence', description: 'Nachweis erstellen', service: 'python' },
|
||||
{ method: 'DELETE', path: '/evidence/{evidence_id}', description: 'Nachweis loeschen', service: 'python' },
|
||||
{ method: 'POST', path: '/evidence/upload', description: 'Nachweis-Datei hochladen', service: 'python' },
|
||||
{ method: 'POST', path: '/evidence/collect', description: 'CI-Nachweis sammeln', service: 'python', exposure: 'partner' },
|
||||
{ method: 'GET', path: '/evidence/ci-status', description: 'CI-Nachweis-Status laden', service: 'python', exposure: 'partner' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'import',
|
||||
name: 'Dokument-Import & Gap-Analyse',
|
||||
service: 'python',
|
||||
basePath: '/api/import',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/analyze', description: 'Dokument analysieren', service: 'python' },
|
||||
{ method: 'GET', path: '/gap-analysis/{document_id}', description: 'Gap-Analyse laden', service: 'python' },
|
||||
{ method: 'GET', path: '/documents', description: 'Importierte Dokumente auflisten', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{document_id}', description: 'Dokument loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'incidents',
|
||||
name: 'Datenschutz-Vorfaelle — Incident Management',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/incidents',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/', description: 'Vorfall erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/', description: 'Vorfaelle auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'Vorfall-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{incident_id}', description: 'Vorfall laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{incident_id}', description: 'Vorfall aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{incident_id}', description: 'Vorfall loeschen', service: 'python' },
|
||||
{ method: 'PUT', path: '/{incident_id}/status', description: 'Vorfall-Status aendern', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/assess-risk', description: 'Risikobewertung durchfuehren', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/notify-authority', description: 'Behoerde benachrichtigen', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/notify-subjects', description: 'Betroffene benachrichtigen', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/measures', description: 'Massnahme hinzufuegen', service: 'python' },
|
||||
{ method: 'PUT', path: '/{incident_id}/measures/{measure_id}', description: 'Massnahme aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/measures/{measure_id}/complete', description: 'Massnahme abschliessen', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/timeline', description: 'Zeitachsen-Eintrag hinzufuegen', service: 'python' },
|
||||
{ method: 'POST', path: '/{incident_id}/close', description: 'Vorfall schliessen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'isms',
|
||||
name: 'ISMS — ISO 27001 Managementsystem',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/isms',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/scope', description: 'ISMS-Scope laden', service: 'python' },
|
||||
{ method: 'POST', path: '/scope', description: 'ISMS-Scope erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/scope/{scope_id}', description: 'ISMS-Scope aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/scope/{scope_id}/approve', description: 'ISMS-Scope genehmigen', service: 'python' },
|
||||
{ method: 'GET', path: '/context', description: 'ISMS-Kontext laden', service: 'python' },
|
||||
{ method: 'POST', path: '/context', description: 'ISMS-Kontext erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/policies', description: 'Richtlinien auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/policies', description: 'Richtlinie erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/policies/{policy_id}', description: 'Richtlinie laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/policies/{policy_id}', description: 'Richtlinie aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/policies/{policy_id}/approve', description: 'Richtlinie genehmigen', service: 'python' },
|
||||
{ method: 'GET', path: '/objectives', description: 'Sicherheitsziele laden', service: 'python' },
|
||||
{ method: 'POST', path: '/objectives', description: 'Sicherheitsziel erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/objectives/{objective_id}', description: 'Sicherheitsziel aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/soa', description: 'Statement of Applicability laden', service: 'python' },
|
||||
{ method: 'POST', path: '/soa', description: 'SoA-Eintrag erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/soa/{entry_id}', description: 'SoA-Eintrag aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/soa/{entry_id}/approve', description: 'SoA-Eintrag genehmigen', service: 'python' },
|
||||
{ method: 'GET', path: '/findings', description: 'Audit-Feststellungen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/findings', description: 'Feststellung erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/findings/{finding_id}', description: 'Feststellung aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/findings/{finding_id}/close', description: 'Feststellung schliessen', service: 'python' },
|
||||
{ method: 'GET', path: '/capa', description: 'Korrekturmassnahmen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/capa', description: 'CAPA erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/capa/{capa_id}', description: 'CAPA aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/capa/{capa_id}/verify', description: 'CAPA verifizieren', service: 'python' },
|
||||
{ method: 'GET', path: '/management-reviews', description: 'Management-Reviews laden', service: 'python' },
|
||||
{ method: 'POST', path: '/management-reviews', description: 'Review erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/management-reviews/{review_id}', description: 'Review laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/management-reviews/{review_id}', description: 'Review aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/management-reviews/{review_id}/approve', description: 'Review genehmigen', service: 'python' },
|
||||
{ method: 'GET', path: '/internal-audits', description: 'Interne Audits laden', service: 'python' },
|
||||
{ method: 'POST', path: '/internal-audits', description: 'Internes Audit erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/internal-audits/{audit_id}', description: 'Audit aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/internal-audits/{audit_id}/complete', description: 'Audit abschliessen', service: 'python' },
|
||||
{ method: 'POST', path: '/readiness-check', description: 'Bereitschafts-Check ausfuehren', service: 'python' },
|
||||
{ method: 'GET', path: '/readiness-check/latest', description: 'Letzten Check laden', service: 'python' },
|
||||
{ method: 'GET', path: '/audit-trail', description: 'Audit-Trail laden', service: 'python' },
|
||||
{ method: 'GET', path: '/overview', description: 'ISO 27001 Uebersicht laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'legal-documents',
|
||||
name: 'Rechtliche Dokumente — Verwaltung & Versionen',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/legal-documents',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/documents', description: 'Dokumente auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/documents', description: 'Dokument erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/documents/{document_id}', description: 'Dokument laden', service: 'python' },
|
||||
{ method: 'DELETE', path: '/documents/{document_id}', description: 'Dokument loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/documents/{document_id}/versions', description: 'Versionen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/versions', description: 'Version erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/versions/{version_id}', description: 'Version laden', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/upload-word', description: 'Word-Dokument hochladen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/submit-review', description: 'Zur Pruefung einreichen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' },
|
||||
{ method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' },
|
||||
{ method: 'GET', path: '/versions/{version_id}/approval-history', description: 'Genehmigungshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/public', description: 'Oeffentliche Dokumente laden', service: 'python', exposure: 'public' },
|
||||
{ method: 'GET', path: '/public/{document_type}/latest', description: 'Aktuellstes Dokument laden', service: 'python', exposure: 'public' },
|
||||
{ method: 'POST', path: '/consents', description: 'Einwilligung erfassen', service: 'python' },
|
||||
{ method: 'GET', path: '/consents/my', description: 'Eigene Einwilligungen laden', service: 'python' },
|
||||
{ method: 'GET', path: '/consents/check/{document_type}', description: 'Einwilligungsstatus pruefen', service: 'python' },
|
||||
{ method: 'DELETE', path: '/consents/{consent_id}', description: 'Einwilligung widerrufen', service: 'python' },
|
||||
{ method: 'GET', path: '/stats/consents', description: 'Einwilligungs-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/audit-log', description: 'Audit-Log laden', service: 'python' },
|
||||
{ method: 'GET', path: '/cookie-categories', description: 'Cookie-Kategorien auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/cookie-categories', description: 'Cookie-Kategorie erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/cookie-categories/{category_id}', description: 'Kategorie aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/cookie-categories/{category_id}', description: 'Kategorie loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'legal-templates',
|
||||
name: 'Dokumentvorlagen — DSGVO-Generatoren',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/legal-templates',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Vorlagen auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/status', description: 'Vorlagenstatus laden', service: 'python' },
|
||||
{ method: 'GET', path: '/sources', description: 'Vorlagenquellen laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{template_id}', description: 'Vorlage laden', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Vorlage erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/{template_id}', description: 'Vorlage aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{template_id}', description: 'Vorlage loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'notfallplan',
|
||||
name: 'Notfallplan — Kontakte, Szenarien & Uebungen',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/notfallplan',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/contacts', description: 'Notfallkontakte laden', service: 'python' },
|
||||
{ method: 'POST', path: '/contacts', description: 'Kontakt erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/contacts/{contact_id}', description: 'Kontakt aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/contacts/{contact_id}', description: 'Kontakt loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/scenarios', description: 'Notfallszenarien laden', service: 'python' },
|
||||
{ method: 'POST', path: '/scenarios', description: 'Szenario erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/scenarios/{scenario_id}', description: 'Szenario aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/scenarios/{scenario_id}', description: 'Szenario loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/checklists', description: 'Checklisten laden', service: 'python' },
|
||||
{ method: 'POST', path: '/checklists', description: 'Checkliste erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/checklists/{checklist_id}', description: 'Checkliste aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/checklists/{checklist_id}', description: 'Checkliste loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/exercises', description: 'Uebungen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/exercises', description: 'Uebung erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/incidents', description: 'Notfall-Vorfaelle laden', service: 'python' },
|
||||
{ method: 'POST', path: '/incidents', description: 'Vorfall erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/incidents/{incident_id}', description: 'Vorfall aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/incidents/{incident_id}', description: 'Vorfall loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/templates', description: 'Vorlagen laden', service: 'python' },
|
||||
{ method: 'POST', path: '/templates', description: 'Vorlage erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/templates/{template_id}', description: 'Vorlage aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/templates/{template_id}', description: 'Vorlage loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'Notfallplan-Statistiken laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'obligations',
|
||||
name: 'Pflichten — Compliance-Obligations',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/obligations',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Pflichten auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Pflicht erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'Pflichten-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{obligation_id}', description: 'Pflicht laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{obligation_id}', description: 'Pflicht aktualisieren', service: 'python' },
|
||||
{ method: 'PUT', path: '/{obligation_id}/status', description: 'Pflicht-Status aendern', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{obligation_id}', description: 'Pflicht loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/{obligation_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/{obligation_id}/versions/{version_number}', description: 'Version laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'quality',
|
||||
name: 'Quality — KI-Qualitaetsmetriken & Tests',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/quality',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/stats', description: 'Qualitaets-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/metrics', description: 'Metriken auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/metrics', description: 'Metrik erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/metrics/{metric_id}', description: 'Metrik aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/metrics/{metric_id}', description: 'Metrik loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/tests', description: 'Tests auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/tests', description: 'Test erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/tests/{test_id}', description: 'Test aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/tests/{test_id}', description: 'Test loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'risks',
|
||||
name: 'Risikomanagement — Bewertung & Matrix',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/risks',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/risks', description: 'Risiken auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/risks', description: 'Risiko erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/risks/{risk_id}', description: 'Risiko aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/risks/{risk_id}', description: 'Risiko loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/risks/matrix', description: 'Risikomatrix laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'screening',
|
||||
name: 'Screening — Abhaengigkeiten-Pruefung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/screening',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/scan', description: 'Abhaengigkeiten scannen', service: 'python', exposure: 'partner' },
|
||||
{ method: 'GET', path: '/{screening_id}', description: 'Screening-Ergebnis laden', service: 'python' },
|
||||
{ method: 'GET', path: '/', description: 'Screenings auflisten', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'scraper',
|
||||
name: 'Scraper — Rechtsquellen-Aktualisierung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/scraper',
|
||||
exposure: 'partner',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/scraper/status', description: 'Scraper-Status laden', service: 'python' },
|
||||
{ method: 'GET', path: '/scraper/sources', description: 'Quellen auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/scraper/scrape-all', description: 'Alle Quellen scrapen', service: 'python' },
|
||||
{ method: 'POST', path: '/scraper/scrape/{code}', description: 'Einzelne Quelle scrapen', service: 'python' },
|
||||
{ method: 'POST', path: '/scraper/extract-bsi', description: 'BSI-Anforderungen extrahieren', service: 'python' },
|
||||
{ method: 'POST', path: '/scraper/extract-pdf', description: 'PDF-Anforderungen extrahieren', service: 'python' },
|
||||
{ method: 'GET', path: '/scraper/pdf-documents', description: 'PDF-Dokumente auflisten', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'security-backlog',
|
||||
name: 'Security Backlog — Sicherheitsmassnahmen',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/security-backlog',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/', description: 'Backlog-Eintraege auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/', description: 'Eintrag erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'Backlog-Statistiken laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/{item_id}', description: 'Eintrag aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/{item_id}', description: 'Eintrag loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'source-policy',
|
||||
name: 'Source Policy — Datenquellen & PII-Regeln',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/source-policy',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/sources', description: 'Datenquellen auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/sources', description: 'Quelle erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/sources/{source_id}', description: 'Quelle laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/sources/{source_id}', description: 'Quelle aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/sources/{source_id}', description: 'Quelle loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/operations-matrix', description: 'Operationsmatrix laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/operations/{operation_id}', description: 'Operation aktualisieren', service: 'python' },
|
||||
{ method: 'GET', path: '/pii-rules', description: 'PII-Regeln auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/pii-rules', description: 'PII-Regel erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/pii-rules/{rule_id}', description: 'PII-Regel aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/pii-rules/{rule_id}', description: 'PII-Regel loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/blocked-content', description: 'Gesperrte Inhalte laden', service: 'python' },
|
||||
{ method: 'GET', path: '/policy-audit', description: 'Richtlinien-Audit-Log laden', service: 'python' },
|
||||
{ method: 'GET', path: '/policy-stats', description: 'Richtlinien-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/compliance-report', description: 'Compliance-Bericht laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'tom',
|
||||
name: 'TOM — Technisch-Organisatorische Massnahmen',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/tom',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/state', description: 'TOM-Zustand laden', service: 'python' },
|
||||
{ method: 'POST', path: '/state', description: 'TOM-Zustand speichern', service: 'python' },
|
||||
{ method: 'DELETE', path: '/state', description: 'TOM-Zustand loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/measures', description: 'Massnahmen auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/measures', description: 'Massnahme erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/measures/{measure_id}', description: 'Massnahme aktualisieren', service: 'python' },
|
||||
{ method: 'POST', path: '/measures/bulk', description: 'Massnahmen Bulk-Upsert', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'TOM-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/export', description: 'Massnahmen exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/measures/{measure_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/measures/{measure_id}/versions/{version_number}', description: 'Version laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'vendor-compliance',
|
||||
name: 'Vendor Compliance — Auftragsverarbeitung',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/vendors',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/vendors/stats', description: 'Anbieter-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/vendors', description: 'Anbieter auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/vendors/{vendor_id}', description: 'Anbieter laden', service: 'python' },
|
||||
{ method: 'POST', path: '/vendors', description: 'Anbieter erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/vendors/{vendor_id}', description: 'Anbieter aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/vendors/{vendor_id}', description: 'Anbieter loeschen', service: 'python' },
|
||||
{ method: 'PATCH', path: '/vendors/{vendor_id}/status', description: 'Anbieter-Status aendern', service: 'python' },
|
||||
{ method: 'GET', path: '/contracts', description: 'Vertraege auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/contracts/{contract_id}', description: 'Vertrag laden', service: 'python' },
|
||||
{ method: 'POST', path: '/contracts', description: 'Vertrag erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/contracts/{contract_id}', description: 'Vertrag aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/contracts/{contract_id}', description: 'Vertrag loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/findings', description: 'Feststellungen auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/findings/{finding_id}', description: 'Feststellung laden', service: 'python' },
|
||||
{ method: 'POST', path: '/findings', description: 'Feststellung erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/findings/{finding_id}', description: 'Feststellung aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/findings/{finding_id}', description: 'Feststellung loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/control-instances', description: 'Kontroll-Instanzen auflisten', service: 'python' },
|
||||
{ method: 'GET', path: '/control-instances/{instance_id}', description: 'Instanz laden', service: 'python' },
|
||||
{ method: 'POST', path: '/control-instances', description: 'Instanz erstellen', service: 'python' },
|
||||
{ method: 'PUT', path: '/control-instances/{instance_id}', description: 'Instanz aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/control-instances/{instance_id}', description: 'Instanz loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/controls', description: 'Controls auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/controls', description: 'Control erstellen', service: 'python' },
|
||||
{ method: 'DELETE', path: '/controls/{control_id}', description: 'Control loeschen', service: 'python' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'vvt',
|
||||
name: 'VVT — Verarbeitungsverzeichnis (Art. 30 DSGVO)',
|
||||
service: 'python',
|
||||
basePath: '/api/compliance/vvt',
|
||||
exposure: 'internal',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/organization', description: 'Organisationskopf laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/organization', description: 'Organisationskopf speichern', service: 'python' },
|
||||
{ method: 'GET', path: '/activities', description: 'Verarbeitungstaetigkeiten auflisten', service: 'python' },
|
||||
{ method: 'POST', path: '/activities', description: 'Taetigkeit erstellen', service: 'python' },
|
||||
{ method: 'GET', path: '/activities/{activity_id}', description: 'Taetigkeit laden', service: 'python' },
|
||||
{ method: 'PUT', path: '/activities/{activity_id}', description: 'Taetigkeit aktualisieren', service: 'python' },
|
||||
{ method: 'DELETE', path: '/activities/{activity_id}', description: 'Taetigkeit loeschen', service: 'python' },
|
||||
{ method: 'GET', path: '/audit-log', description: 'VVT-Audit-Log laden', service: 'python' },
|
||||
{ method: 'GET', path: '/export', description: 'VVT exportieren', service: 'python' },
|
||||
{ method: 'GET', path: '/stats', description: 'VVT-Statistiken laden', service: 'python' },
|
||||
{ method: 'GET', path: '/activities/{activity_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||
{ method: 'GET', path: '/activities/{activity_id}/versions/{version_number}', description: 'Version laden', service: 'python' },
|
||||
],
|
||||
},
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
133
admin-compliance/lib/sdk/compliance-scope-data.ts
Normal file
133
admin-compliance/lib/sdk/compliance-scope-data.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// ============================================================================
|
||||
// SCORE WEIGHTS PRO FRAGE
|
||||
// ============================================================================
|
||||
|
||||
export const QUESTION_SCORE_WEIGHTS: Record<
|
||||
string,
|
||||
{ risk: number; complexity: number; assurance: number }
|
||||
> = {
|
||||
// Organisationsprofil (6 Fragen)
|
||||
org_employee_count: { risk: 3, complexity: 5, assurance: 4 },
|
||||
org_industry: { risk: 6, complexity: 4, assurance: 5 },
|
||||
org_business_model: { risk: 5, complexity: 3, assurance: 4 },
|
||||
org_customer_count: { risk: 4, complexity: 6, assurance: 5 },
|
||||
org_cert_target: { risk: 2, complexity: 8, assurance: 9 },
|
||||
org_has_dpo: { risk: 7, complexity: 2, assurance: 8 },
|
||||
|
||||
// Datenarten (5 Fragen)
|
||||
data_art9: { risk: 10, complexity: 7, assurance: 9 },
|
||||
data_minors: { risk: 10, complexity: 6, assurance: 9 },
|
||||
data_volume: { risk: 6, complexity: 7, assurance: 6 },
|
||||
data_retention_years: { risk: 5, complexity: 4, assurance: 5 },
|
||||
data_sources: { risk: 4, complexity: 5, assurance: 4 },
|
||||
|
||||
// Verarbeitungszwecke (9 Fragen)
|
||||
proc_adm_scoring: { risk: 9, complexity: 7, assurance: 8 },
|
||||
proc_ai_usage: { risk: 8, complexity: 8, assurance: 8 },
|
||||
proc_video_surveillance: { risk: 7, complexity: 5, assurance: 7 },
|
||||
proc_employee_monitoring: { risk: 7, complexity: 5, assurance: 7 },
|
||||
proc_tracking: { risk: 6, complexity: 4, assurance: 6 },
|
||||
proc_dsar_process: { risk: 8, complexity: 6, assurance: 8 },
|
||||
proc_deletion_concept: { risk: 7, complexity: 5, assurance: 7 },
|
||||
proc_incident_response: { risk: 9, complexity: 6, assurance: 9 },
|
||||
proc_regular_audits: { risk: 5, complexity: 7, assurance: 8 },
|
||||
|
||||
// Technik (7 Fragen)
|
||||
tech_hosting_location: { risk: 7, complexity: 5, assurance: 7 },
|
||||
tech_third_country: { risk: 8, complexity: 6, assurance: 8 },
|
||||
tech_encryption_transit: { risk: 8, complexity: 4, assurance: 8 },
|
||||
tech_encryption_rest: { risk: 8, complexity: 4, assurance: 8 },
|
||||
tech_access_control: { risk: 7, complexity: 5, assurance: 7 },
|
||||
tech_logging: { risk: 6, complexity: 5, assurance: 7 },
|
||||
tech_backup_recovery: { risk: 6, complexity: 5, assurance: 7 },
|
||||
|
||||
// Produkt/Features (5 Fragen)
|
||||
prod_webshop: { risk: 5, complexity: 4, assurance: 5 },
|
||||
prod_data_broker: { risk: 9, complexity: 7, assurance: 8 },
|
||||
prod_api_external: { risk: 6, complexity: 5, assurance: 6 },
|
||||
prod_consent_management: { risk: 7, complexity: 5, assurance: 8 },
|
||||
prod_data_portability: { risk: 4, complexity: 5, assurance: 5 },
|
||||
|
||||
// Compliance Reife (3 Fragen)
|
||||
comp_training: { risk: 5, complexity: 4, assurance: 7 },
|
||||
comp_vendor_management: { risk: 6, complexity: 6, assurance: 7 },
|
||||
comp_documentation_level: { risk: 6, complexity: 7, assurance: 8 },
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ANSWER MULTIPLIERS FÜR SINGLE-CHOICE FRAGEN
|
||||
// ============================================================================
|
||||
|
||||
export const ANSWER_MULTIPLIERS: Record<string, Record<string, number>> = {
|
||||
org_employee_count: {
|
||||
'1-9': 0.1,
|
||||
'10-49': 0.3,
|
||||
'50-249': 0.5,
|
||||
'250-999': 0.7,
|
||||
'1000+': 1.0,
|
||||
},
|
||||
org_industry: {
|
||||
tech: 0.4,
|
||||
finance: 0.8,
|
||||
healthcare: 0.9,
|
||||
public: 0.7,
|
||||
retail: 0.5,
|
||||
education: 0.6,
|
||||
other: 0.3,
|
||||
},
|
||||
org_business_model: {
|
||||
b2b: 0.4,
|
||||
b2c: 0.7,
|
||||
b2b2c: 0.6,
|
||||
internal: 0.3,
|
||||
},
|
||||
org_customer_count: {
|
||||
'0-100': 0.1,
|
||||
'100-1000': 0.2,
|
||||
'1000-10000': 0.4,
|
||||
'10000-100000': 0.7,
|
||||
'100000+': 1.0,
|
||||
},
|
||||
data_volume: {
|
||||
'<1000': 0.1,
|
||||
'1000-10000': 0.2,
|
||||
'10000-100000': 0.4,
|
||||
'100000-1000000': 0.7,
|
||||
'>1000000': 1.0,
|
||||
},
|
||||
data_retention_years: {
|
||||
'<1': 0.2,
|
||||
'1-3': 0.4,
|
||||
'3-5': 0.6,
|
||||
'5-10': 0.8,
|
||||
'>10': 1.0,
|
||||
},
|
||||
tech_hosting_location: {
|
||||
eu: 0.2,
|
||||
eu_us_adequacy: 0.4,
|
||||
us_adequacy: 0.6,
|
||||
drittland: 1.0,
|
||||
},
|
||||
tech_access_control: {
|
||||
none: 1.0,
|
||||
basic: 0.6,
|
||||
rbac: 0.3,
|
||||
advanced: 0.1,
|
||||
},
|
||||
tech_logging: {
|
||||
none: 1.0,
|
||||
basic: 0.6,
|
||||
comprehensive: 0.2,
|
||||
},
|
||||
tech_backup_recovery: {
|
||||
none: 1.0,
|
||||
basic: 0.5,
|
||||
tested: 0.2,
|
||||
},
|
||||
comp_documentation_level: {
|
||||
none: 1.0,
|
||||
basic: 0.6,
|
||||
structured: 0.3,
|
||||
comprehensive: 0.1,
|
||||
},
|
||||
}
|
||||
497
admin-compliance/lib/sdk/compliance-scope-documents.ts
Normal file
497
admin-compliance/lib/sdk/compliance-scope-documents.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* Document-scope calculation, risk flags, gap analysis, next actions,
|
||||
* and reasoning (audit trail) helpers for the ComplianceScopeEngine.
|
||||
*/
|
||||
import type {
|
||||
ComplianceDepthLevel,
|
||||
ComplianceScores,
|
||||
ScopeProfilingAnswer,
|
||||
TriggeredHardTrigger,
|
||||
RequiredDocument,
|
||||
RiskFlag,
|
||||
ScopeGap,
|
||||
NextAction,
|
||||
ScopeReasoning,
|
||||
ScopeDocumentType,
|
||||
HardTriggerRule,
|
||||
} from './compliance-scope-types'
|
||||
import {
|
||||
getDepthLevelNumeric,
|
||||
DOCUMENT_SCOPE_MATRIX,
|
||||
DOCUMENT_TYPE_LABELS,
|
||||
DOCUMENT_SDK_STEP_MAP,
|
||||
} from './compliance-scope-types'
|
||||
import { HARD_TRIGGER_RULES } from './compliance-scope-triggers'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Parse employee-count bucket string to a representative number. */
|
||||
export function parseEmployeeCount(value: string): number {
|
||||
if (value === '1-9') return 9
|
||||
if (value === '10-49') return 49
|
||||
if (value === '50-249') return 249
|
||||
if (value === '250-999') return 999
|
||||
if (value === '1000+') return 1000
|
||||
return 0
|
||||
}
|
||||
|
||||
/** Derive level purely from composite score. */
|
||||
export function getLevelFromScore(composite: number): ComplianceDepthLevel {
|
||||
if (composite <= 25) return 'L1'
|
||||
if (composite <= 50) return 'L2'
|
||||
if (composite <= 75) return 'L3'
|
||||
return 'L4'
|
||||
}
|
||||
|
||||
/** Highest level among the given triggers. */
|
||||
export function getMaxTriggerLevel(triggers: TriggeredHardTrigger[]): ComplianceDepthLevel {
|
||||
if (triggers.length === 0) return 'L1'
|
||||
let max: ComplianceDepthLevel = 'L1'
|
||||
for (const t of triggers) {
|
||||
if (getDepthLevelNumeric(t.minimumLevel) > getDepthLevelNumeric(max)) {
|
||||
max = t.minimumLevel
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// normalizeDocType
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Maps UPPERCASE document-type identifiers from the hard-trigger rules
|
||||
* to the lowercase ScopeDocumentType keys.
|
||||
*/
|
||||
export function normalizeDocType(raw: string): ScopeDocumentType | null {
|
||||
const mapping: Record<string, ScopeDocumentType> = {
|
||||
VVT: 'vvt',
|
||||
TOM: 'tom',
|
||||
DSFA: 'dsfa',
|
||||
DSE: 'dsi',
|
||||
AGB: 'vertragsmanagement',
|
||||
AVV: 'av_vertrag',
|
||||
COOKIE_BANNER: 'einwilligung',
|
||||
EINWILLIGUNGEN: 'einwilligung',
|
||||
TRANSFER_DOKU: 'daten_transfer',
|
||||
AUDIT_CHECKLIST: 'audit_log',
|
||||
VENDOR_MANAGEMENT: 'vertragsmanagement',
|
||||
LOESCHKONZEPT: 'lf',
|
||||
DSR_PROZESS: 'betroffenenrechte',
|
||||
NOTFALLPLAN: 'notfallplan',
|
||||
AI_ACT_DOKU: 'ai_act_doku',
|
||||
WIDERRUFSBELEHRUNG: 'widerrufsbelehrung',
|
||||
PREISANGABEN: 'preisangaben',
|
||||
FERNABSATZ_INFO: 'fernabsatz_info',
|
||||
STREITBEILEGUNG: 'streitbeilegung',
|
||||
PRODUKTSICHERHEIT: 'produktsicherheit',
|
||||
}
|
||||
if (raw in DOCUMENT_SCOPE_MATRIX) return raw as ScopeDocumentType
|
||||
return mapping[raw] ?? null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document scope
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getDocumentPriority(
|
||||
docType: ScopeDocumentType,
|
||||
isMandatoryFromTrigger: boolean,
|
||||
): 'high' | 'medium' | 'low' {
|
||||
if (isMandatoryFromTrigger) return 'high'
|
||||
if (['VVT', 'TOM', 'DSE'].includes(docType)) return 'high'
|
||||
if (['DSFA', 'AVV', 'EINWILLIGUNGEN'].includes(docType)) return 'high'
|
||||
return 'medium'
|
||||
}
|
||||
|
||||
function estimateEffort(docType: ScopeDocumentType): number {
|
||||
const effortMap: Partial<Record<ScopeDocumentType, number>> = {
|
||||
vvt: 8,
|
||||
tom: 12,
|
||||
dsfa: 16,
|
||||
av_vertrag: 4,
|
||||
dsi: 6,
|
||||
einwilligung: 6,
|
||||
lf: 10,
|
||||
daten_transfer: 8,
|
||||
betroffenenrechte: 8,
|
||||
notfallplan: 12,
|
||||
vertragsmanagement: 10,
|
||||
audit_log: 8,
|
||||
risikoanalyse: 6,
|
||||
schulung: 4,
|
||||
datenpannen: 6,
|
||||
zertifizierung: 8,
|
||||
datenschutzmanagement: 12,
|
||||
iace_ce_assessment: 8,
|
||||
widerrufsbelehrung: 3,
|
||||
preisangaben: 2,
|
||||
fernabsatz_info: 4,
|
||||
streitbeilegung: 1,
|
||||
produktsicherheit: 8,
|
||||
ai_act_doku: 12,
|
||||
}
|
||||
return effortMap[docType] ?? 6
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full document-scope list based on compliance level and triggers.
|
||||
*/
|
||||
export function buildDocumentScope(
|
||||
level: ComplianceDepthLevel,
|
||||
triggers: TriggeredHardTrigger[],
|
||||
_answers: ScopeProfilingAnswer[],
|
||||
): RequiredDocument[] {
|
||||
const requiredDocs: RequiredDocument[] = []
|
||||
const mandatoryFromTriggers = new Set<ScopeDocumentType>()
|
||||
const triggerDocOrigins = new Map<ScopeDocumentType, string[]>()
|
||||
|
||||
for (const trigger of triggers) {
|
||||
for (const doc of trigger.mandatoryDocuments) {
|
||||
const normalized = normalizeDocType(doc)
|
||||
if (normalized) {
|
||||
mandatoryFromTriggers.add(normalized)
|
||||
if (!triggerDocOrigins.has(normalized)) triggerDocOrigins.set(normalized, [])
|
||||
triggerDocOrigins.get(normalized)!.push(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const docType of Object.keys(DOCUMENT_SCOPE_MATRIX) as ScopeDocumentType[]) {
|
||||
const requirement = DOCUMENT_SCOPE_MATRIX[docType][level]
|
||||
const isMandatoryFromTrigger = mandatoryFromTriggers.has(docType)
|
||||
|
||||
if (requirement === 'mandatory' || isMandatoryFromTrigger) {
|
||||
const originDocs = triggerDocOrigins.get(docType) ?? []
|
||||
requiredDocs.push({
|
||||
documentType: docType,
|
||||
label: DOCUMENT_TYPE_LABELS[docType],
|
||||
requirement: 'mandatory',
|
||||
priority: getDocumentPriority(docType, isMandatoryFromTrigger),
|
||||
estimatedEffort: estimateEffort(docType),
|
||||
sdkStepUrl: DOCUMENT_SDK_STEP_MAP[docType],
|
||||
triggeredBy: isMandatoryFromTrigger
|
||||
? triggers
|
||||
.filter((t) => t.mandatoryDocuments.some((d) => originDocs.includes(d)))
|
||||
.map((t) => t.ruleId)
|
||||
: [],
|
||||
})
|
||||
} else if (requirement === 'recommended') {
|
||||
requiredDocs.push({
|
||||
documentType: docType,
|
||||
label: DOCUMENT_TYPE_LABELS[docType],
|
||||
requirement: 'recommended',
|
||||
priority: 'medium',
|
||||
estimatedEffort: estimateEffort(docType),
|
||||
sdkStepUrl: DOCUMENT_SDK_STEP_MAP[docType],
|
||||
triggeredBy: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
requiredDocs.sort((a, b) => {
|
||||
if (a.requirement === 'mandatory' && b.requirement !== 'mandatory') return -1
|
||||
if (a.requirement !== 'mandatory' && b.requirement === 'mandatory') return 1
|
||||
const priorityOrder: Record<string, number> = { high: 3, medium: 2, low: 1 }
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority]
|
||||
})
|
||||
|
||||
return requiredDocs
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Risk flags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getMaturityRecommendation(ruleId: string): string {
|
||||
const recommendations: Record<string, string> = {
|
||||
'HT-I01': 'Prozess für Betroffenenrechte (DSAR) etablieren und dokumentieren',
|
||||
'HT-I02': 'Löschkonzept gemäß Art. 17 DSGVO entwickeln und implementieren',
|
||||
'HT-I03':
|
||||
'Incident-Response-Plan für Datenschutzverletzungen (Art. 33 DSGVO) erstellen',
|
||||
'HT-I04': 'Regelmäßige interne Audits und Reviews einführen',
|
||||
'HT-I05': 'Schulungsprogramm für Mitarbeiter zum Datenschutz etablieren',
|
||||
}
|
||||
return recommendations[ruleId] || 'Prozess etablieren und dokumentieren'
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate risk flags based on process-maturity gaps and other risks.
|
||||
*
|
||||
* `checkTriggerFn` is injected to avoid a circular dependency on the engine.
|
||||
*/
|
||||
export function evaluateRiskFlags(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
level: ComplianceDepthLevel,
|
||||
checkTriggerFn: (
|
||||
rule: HardTriggerRule,
|
||||
answerMap: Map<string, any>,
|
||||
answers: ScopeProfilingAnswer[],
|
||||
) => boolean,
|
||||
): RiskFlag[] {
|
||||
const flags: RiskFlag[] = []
|
||||
const answerMap = new Map(answers.map((a) => [a.questionId, a.value]))
|
||||
|
||||
const maturityRules = HARD_TRIGGER_RULES.filter((r) => r.category === 'process_maturity')
|
||||
for (const rule of maturityRules) {
|
||||
if (checkTriggerFn(rule, answerMap, answers)) {
|
||||
flags.push({
|
||||
severity: 'medium',
|
||||
category: 'process',
|
||||
message: rule.description,
|
||||
legalReference: rule.legalReference,
|
||||
recommendation: getMaturityRecommendation(rule.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (getDepthLevelNumeric(level) >= 2) {
|
||||
const encTransit = answerMap.get('tech_encryption_transit')
|
||||
const encRest = answerMap.get('tech_encryption_rest')
|
||||
|
||||
if (encTransit === false) {
|
||||
flags.push({
|
||||
severity: 'high',
|
||||
category: 'technical',
|
||||
message: 'Fehlende Verschlüsselung bei Datenübertragung',
|
||||
legalReference: 'Art. 32 DSGVO',
|
||||
recommendation: 'TLS 1.2+ für alle Datenübertragungen implementieren',
|
||||
})
|
||||
}
|
||||
|
||||
if (encRest === false) {
|
||||
flags.push({
|
||||
severity: 'high',
|
||||
category: 'technical',
|
||||
message: 'Fehlende Verschlüsselung gespeicherter Daten',
|
||||
legalReference: 'Art. 32 DSGVO',
|
||||
recommendation: 'Verschlüsselung at-rest für sensitive Daten implementieren',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const thirdCountry = answerMap.get('tech_third_country')
|
||||
const hostingLocation = answerMap.get('tech_hosting_location')
|
||||
if (
|
||||
thirdCountry === true &&
|
||||
hostingLocation !== 'eu' &&
|
||||
hostingLocation !== 'eu_us_adequacy'
|
||||
) {
|
||||
flags.push({
|
||||
severity: 'high',
|
||||
category: 'legal',
|
||||
message: 'Drittlandtransfer ohne angemessene Garantien',
|
||||
legalReference: 'Art. 44 ff. DSGVO',
|
||||
recommendation:
|
||||
'Standardvertragsklauseln (SCCs) oder Binding Corporate Rules (BCRs) implementieren',
|
||||
})
|
||||
}
|
||||
|
||||
const hasDPO = answerMap.get('org_has_dpo')
|
||||
const employeeCount = answerMap.get('org_employee_count')
|
||||
if (hasDPO === false && parseEmployeeCount(employeeCount as string) >= 250) {
|
||||
flags.push({
|
||||
severity: 'medium',
|
||||
category: 'organizational',
|
||||
message: 'Kein Datenschutzbeauftragter bei großer Organisation',
|
||||
legalReference: 'Art. 37 DSGVO',
|
||||
recommendation: 'Bestellung eines Datenschutzbeauftragten prüfen',
|
||||
})
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gap analysis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function calculateGaps(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
level: ComplianceDepthLevel,
|
||||
): ScopeGap[] {
|
||||
const gaps: ScopeGap[] = []
|
||||
const answerMap = new Map(answers.map((a) => [a.questionId, a.value]))
|
||||
|
||||
if (getDepthLevelNumeric(level) >= 3) {
|
||||
const hasDSFA = answerMap.get('proc_regular_audits')
|
||||
if (hasDSFA === false) {
|
||||
gaps.push({
|
||||
gapType: 'documentation',
|
||||
severity: 'high',
|
||||
description: 'Datenschutz-Folgenabschätzung (DSFA) fehlt',
|
||||
requiredFor: level,
|
||||
currentState: 'Keine DSFA durchgeführt',
|
||||
targetState: 'DSFA für Hochrisiko-Verarbeitungen durchgeführt und dokumentiert',
|
||||
effort: 16,
|
||||
priority: 'high',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const hasDeletion = answerMap.get('proc_deletion_concept')
|
||||
if (hasDeletion === false && getDepthLevelNumeric(level) >= 2) {
|
||||
gaps.push({
|
||||
gapType: 'process',
|
||||
severity: 'medium',
|
||||
description: 'Löschkonzept fehlt',
|
||||
requiredFor: level,
|
||||
currentState: 'Kein systematisches Löschkonzept',
|
||||
targetState: 'Dokumentiertes Löschkonzept mit definierten Fristen',
|
||||
effort: 10,
|
||||
priority: 'high',
|
||||
})
|
||||
}
|
||||
|
||||
const hasDSAR = answerMap.get('proc_dsar_process')
|
||||
if (hasDSAR === false) {
|
||||
gaps.push({
|
||||
gapType: 'process',
|
||||
severity: 'high',
|
||||
description: 'Prozess für Betroffenenrechte fehlt',
|
||||
requiredFor: level,
|
||||
currentState: 'Kein etablierter DSAR-Prozess',
|
||||
targetState: 'Dokumentierter Prozess zur Bearbeitung von Betroffenenrechten',
|
||||
effort: 8,
|
||||
priority: 'high',
|
||||
})
|
||||
}
|
||||
|
||||
const hasIncident = answerMap.get('proc_incident_response')
|
||||
if (hasIncident === false) {
|
||||
gaps.push({
|
||||
gapType: 'process',
|
||||
severity: 'high',
|
||||
description: 'Incident-Response-Plan fehlt',
|
||||
requiredFor: level,
|
||||
currentState: 'Kein Prozess für Datenschutzverletzungen',
|
||||
targetState: 'Dokumentierter Incident-Response-Plan gemäß Art. 33 DSGVO',
|
||||
effort: 12,
|
||||
priority: 'high',
|
||||
})
|
||||
}
|
||||
|
||||
const hasTraining = answerMap.get('comp_training')
|
||||
if (hasTraining === false && getDepthLevelNumeric(level) >= 2) {
|
||||
gaps.push({
|
||||
gapType: 'organizational',
|
||||
severity: 'medium',
|
||||
description: 'Datenschutzschulungen fehlen',
|
||||
requiredFor: level,
|
||||
currentState: 'Keine regelmäßigen Schulungen',
|
||||
targetState: 'Etabliertes Schulungsprogramm für alle Mitarbeiter',
|
||||
effort: 6,
|
||||
priority: 'medium',
|
||||
})
|
||||
}
|
||||
|
||||
return gaps
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Next actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildNextActions(
|
||||
requiredDocuments: RequiredDocument[],
|
||||
gaps: ScopeGap[],
|
||||
): NextAction[] {
|
||||
const actions: NextAction[] = []
|
||||
|
||||
for (const doc of requiredDocuments) {
|
||||
if (doc.requirement === 'mandatory') {
|
||||
actions.push({
|
||||
actionType: 'create_document',
|
||||
title: `${doc.label} erstellen`,
|
||||
description: `Pflichtdokument für Compliance-Level erstellen`,
|
||||
priority: doc.priority,
|
||||
estimatedEffort: doc.estimatedEffort,
|
||||
documentType: doc.documentType,
|
||||
sdkStepUrl: doc.sdkStepUrl,
|
||||
blockers: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const gap of gaps) {
|
||||
let actionType: NextAction['actionType'] = 'establish_process'
|
||||
if (gap.gapType === 'documentation') actionType = 'create_document'
|
||||
else if (gap.gapType === 'technical') actionType = 'implement_technical'
|
||||
else if (gap.gapType === 'organizational') actionType = 'organizational_change'
|
||||
|
||||
actions.push({
|
||||
actionType,
|
||||
title: `Gap schließen: ${gap.description}`,
|
||||
description: `Von "${gap.currentState}" zu "${gap.targetState}"`,
|
||||
priority: gap.priority,
|
||||
estimatedEffort: gap.effort,
|
||||
blockers: [],
|
||||
})
|
||||
}
|
||||
|
||||
const priorityOrder: Record<string, number> = { high: 3, medium: 2, low: 1 }
|
||||
actions.sort((a, b) => priorityOrder[b.priority] - priorityOrder[a.priority])
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reasoning (audit trail)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildReasoning(
|
||||
scores: ComplianceScores,
|
||||
triggers: TriggeredHardTrigger[],
|
||||
level: ComplianceDepthLevel,
|
||||
docs: RequiredDocument[],
|
||||
): ScopeReasoning[] {
|
||||
const reasoning: ScopeReasoning[] = []
|
||||
|
||||
reasoning.push({
|
||||
step: 'score_calculation',
|
||||
description: 'Risikobasierte Score-Berechnung aus Profiling-Antworten',
|
||||
factors: [
|
||||
`Risiko-Score: ${scores.risk_score}/10`,
|
||||
`Komplexitäts-Score: ${scores.complexity_score}/10`,
|
||||
`Assurance-Score: ${scores.assurance_need}/10`,
|
||||
`Composite Score: ${scores.composite_score}/10`,
|
||||
],
|
||||
impact: `Score-basiertes Level: ${getLevelFromScore(scores.composite_score)}`,
|
||||
})
|
||||
|
||||
if (triggers.length > 0) {
|
||||
reasoning.push({
|
||||
step: 'hard_trigger_evaluation',
|
||||
description: `${triggers.length} Hard Trigger Rule(s) aktiviert`,
|
||||
factors: triggers.map(
|
||||
(t) => `${t.ruleId}: ${t.description}${t.legalReference ? ` (${t.legalReference})` : ''}`,
|
||||
),
|
||||
impact: `Höchstes Trigger-Level: ${getMaxTriggerLevel(triggers)}`,
|
||||
})
|
||||
}
|
||||
|
||||
reasoning.push({
|
||||
step: 'level_determination',
|
||||
description: 'Finales Compliance-Level durch Maximum aus Score und Triggers',
|
||||
factors: [
|
||||
`Score-Level: ${getLevelFromScore(scores.composite_score)}`,
|
||||
`Trigger-Level: ${getMaxTriggerLevel(triggers)}`,
|
||||
],
|
||||
impact: `Finales Level: ${level}`,
|
||||
})
|
||||
|
||||
const mandatoryDocs = docs.filter((d) => d.requirement === 'mandatory')
|
||||
reasoning.push({
|
||||
step: 'document_scope',
|
||||
description: `Dokumenten-Scope für ${level} bestimmt`,
|
||||
factors: [
|
||||
`${mandatoryDocs.length} Pflichtdokumente`,
|
||||
`${docs.length - mandatoryDocs.length} empfohlene Dokumente`,
|
||||
],
|
||||
impact: `Gesamtaufwand: ~${docs.reduce((sum, d) => sum + d.estimatedEffort, 0)} Stunden`,
|
||||
})
|
||||
|
||||
return reasoning
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
489
admin-compliance/lib/sdk/compliance-scope-profiling-blocks.ts
Normal file
489
admin-compliance/lib/sdk/compliance-scope-profiling-blocks.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import type {
|
||||
ScopeQuestionBlock,
|
||||
ScopeProfilingQuestion,
|
||||
} from './compliance-scope-types'
|
||||
|
||||
/**
|
||||
* IDs of questions that are auto-filled from company profile.
|
||||
* These are no longer shown as interactive questions but still contribute to scoring.
|
||||
*/
|
||||
export const PROFILE_AUTOFILL_QUESTION_IDS = [
|
||||
'org_employee_count',
|
||||
'org_annual_revenue',
|
||||
'org_industry',
|
||||
'org_business_model',
|
||||
'org_has_dsb',
|
||||
'org_cert_target',
|
||||
'data_volume',
|
||||
'prod_type',
|
||||
'prod_webshop',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Block 1: Organisation & Reife
|
||||
*/
|
||||
export const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
|
||||
id: 'organisation',
|
||||
title: 'Kunden & Nutzer',
|
||||
description: 'Informationen zu Ihren Kunden und Nutzern',
|
||||
order: 1,
|
||||
questions: [
|
||||
{
|
||||
id: 'org_customer_count',
|
||||
type: 'single',
|
||||
question: 'Wie viele Kunden/Nutzer betreuen Sie?',
|
||||
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: '<100', label: 'Weniger als 100' },
|
||||
{ value: '100-1000', label: '100 bis 1.000' },
|
||||
{ value: '1000-10000', label: '1.000 bis 10.000' },
|
||||
{ value: '10000-100000', label: '10.000 bis 100.000' },
|
||||
{ value: '100000+', label: 'Mehr als 100.000' },
|
||||
],
|
||||
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Block 2: Daten & Betroffene
|
||||
*/
|
||||
export const BLOCK_2_DATA: ScopeQuestionBlock = {
|
||||
id: 'data',
|
||||
title: 'Datenverarbeitung',
|
||||
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
|
||||
order: 2,
|
||||
questions: [
|
||||
{
|
||||
id: 'data_minors',
|
||||
type: 'boolean',
|
||||
question: 'Verarbeiten Sie Daten von Minderjährigen?',
|
||||
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
|
||||
required: true,
|
||||
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
|
||||
mapsToVVTQuestion: 'data_minors',
|
||||
},
|
||||
{
|
||||
id: 'data_art9',
|
||||
type: 'multi',
|
||||
question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
|
||||
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'gesundheit', label: 'Gesundheitsdaten' },
|
||||
{ value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
|
||||
{ value: 'genetik', label: 'Genetische Daten' },
|
||||
{ value: 'politisch', label: 'Politische Meinungen' },
|
||||
{ value: 'religion', label: 'Religiöse/weltanschauliche Überzeugungen' },
|
||||
{ value: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit' },
|
||||
{ value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
|
||||
{ value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
|
||||
{ value: 'ethnisch', label: 'Ethnische Herkunft' },
|
||||
],
|
||||
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
|
||||
mapsToVVTQuestion: 'data_health',
|
||||
},
|
||||
{
|
||||
id: 'data_hr',
|
||||
type: 'boolean',
|
||||
question: 'Verarbeiten Sie Personaldaten (HR)?',
|
||||
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
|
||||
required: true,
|
||||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||||
mapsToVVTQuestion: 'dept_hr',
|
||||
mapsToLFQuestion: 'data-hr',
|
||||
},
|
||||
{
|
||||
id: 'data_communication',
|
||||
type: 'boolean',
|
||||
question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
|
||||
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
|
||||
required: true,
|
||||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'data_financial',
|
||||
type: 'boolean',
|
||||
question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
|
||||
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
|
||||
required: true,
|
||||
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
|
||||
mapsToVVTQuestion: 'dept_finance',
|
||||
mapsToLFQuestion: 'data-buchhaltung',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Block 3: Verarbeitung & Zweck
|
||||
*/
|
||||
export const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
|
||||
id: 'processing',
|
||||
title: 'Verarbeitung & Zweck',
|
||||
description: 'Wie und wofür werden personenbezogene Daten verarbeitet?',
|
||||
order: 3,
|
||||
questions: [
|
||||
{
|
||||
id: 'proc_tracking',
|
||||
type: 'boolean',
|
||||
question: 'Setzen Sie Tracking oder Profiling ein?',
|
||||
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
|
||||
required: true,
|
||||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'proc_adm_scoring',
|
||||
type: 'boolean',
|
||||
question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
|
||||
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
|
||||
required: true,
|
||||
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
|
||||
},
|
||||
{
|
||||
id: 'proc_ai_usage',
|
||||
type: 'multi',
|
||||
question: 'Setzen Sie KI-Systeme ein?',
|
||||
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'keine', label: 'Keine KI im Einsatz' },
|
||||
{ value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
|
||||
{ value: 'scoring', label: 'Scoring/Risikobewertung' },
|
||||
{ value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
|
||||
{ value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
|
||||
{ value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
|
||||
],
|
||||
scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'proc_data_combination',
|
||||
type: 'boolean',
|
||||
question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
|
||||
helpText: 'Data Matching, Anreicherung aus externen Quellen',
|
||||
required: true,
|
||||
scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'proc_employee_monitoring',
|
||||
type: 'boolean',
|
||||
question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
|
||||
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
|
||||
required: true,
|
||||
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'proc_video_surveillance',
|
||||
type: 'boolean',
|
||||
question: 'Setzen Sie Videoüberwachung ein?',
|
||||
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
|
||||
required: true,
|
||||
scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
|
||||
mapsToVVTQuestion: 'special_video_surveillance',
|
||||
mapsToLFQuestion: 'data-video',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Block 4: Technik/Hosting/Transfers
|
||||
*/
|
||||
export const BLOCK_4_TECH: ScopeQuestionBlock = {
|
||||
id: 'tech',
|
||||
title: 'Hosting & Verarbeitung',
|
||||
description: 'Technische Infrastruktur und Datenübermittlung',
|
||||
order: 4,
|
||||
questions: [
|
||||
{
|
||||
id: 'tech_hosting_location',
|
||||
type: 'single',
|
||||
question: 'Wo werden Ihre Daten primär gehostet?',
|
||||
helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'de', label: 'Deutschland' },
|
||||
{ value: 'eu', label: 'EU (ohne Deutschland)' },
|
||||
{ value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
|
||||
{ value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
|
||||
{ value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
|
||||
],
|
||||
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'tech_subprocessors',
|
||||
type: 'boolean',
|
||||
question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
|
||||
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. – erfordert AVV nach Art. 28 DSGVO',
|
||||
required: true,
|
||||
scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'tech_third_country',
|
||||
type: 'boolean',
|
||||
question: 'Übermitteln Sie Daten in Drittländer?',
|
||||
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
|
||||
required: true,
|
||||
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
|
||||
mapsToVVTQuestion: 'transfer_cloud_us',
|
||||
},
|
||||
{
|
||||
id: 'tech_encryption_rest',
|
||||
type: 'boolean',
|
||||
question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
|
||||
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
|
||||
required: true,
|
||||
scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'tech_encryption_transit',
|
||||
type: 'boolean',
|
||||
question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
|
||||
helpText: 'TLS/SSL für alle Verbindungen',
|
||||
required: true,
|
||||
scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'tech_cloud_providers',
|
||||
type: 'multi',
|
||||
question: 'Welche Cloud-Anbieter nutzen Sie?',
|
||||
helpText: 'Mehrfachauswahl möglich',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 'aws', label: 'Amazon Web Services (AWS)' },
|
||||
{ value: 'azure', label: 'Microsoft Azure' },
|
||||
{ value: 'gcp', label: 'Google Cloud Platform (GCP)' },
|
||||
{ value: 'hetzner', label: 'Hetzner' },
|
||||
{ value: 'ionos', label: 'IONOS' },
|
||||
{ value: 'ovh', label: 'OVH' },
|
||||
{ value: 'andere', label: 'Andere Anbieter' },
|
||||
{ value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
|
||||
],
|
||||
scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Block 5: Rechte & Prozesse
|
||||
*/
|
||||
export const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
|
||||
id: 'processes',
|
||||
title: 'Rechte & Prozesse',
|
||||
description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
|
||||
order: 5,
|
||||
questions: [
|
||||
{
|
||||
id: 'proc_dsar_process',
|
||||
type: 'boolean',
|
||||
question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
|
||||
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. – Art. 15-22 DSGVO',
|
||||
required: true,
|
||||
scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
|
||||
},
|
||||
{
|
||||
id: 'proc_deletion_concept',
|
||||
type: 'boolean',
|
||||
question: 'Haben Sie ein Löschkonzept?',
|
||||
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
|
||||
required: true,
|
||||
scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
|
||||
},
|
||||
{
|
||||
id: 'proc_incident_response',
|
||||
type: 'boolean',
|
||||
question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
|
||||
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
|
||||
required: true,
|
||||
scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
|
||||
},
|
||||
{
|
||||
id: 'proc_regular_audits',
|
||||
type: 'boolean',
|
||||
question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
|
||||
helpText: 'Interne oder externe Prüfungen mindestens jährlich',
|
||||
required: true,
|
||||
scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
|
||||
},
|
||||
{
|
||||
id: 'proc_training',
|
||||
type: 'boolean',
|
||||
question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
|
||||
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
|
||||
required: true,
|
||||
scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Block 6: Produktkontext
|
||||
*/
|
||||
export const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
|
||||
id: 'product',
|
||||
title: 'Website und Services',
|
||||
description: 'Spezifische Merkmale Ihrer Produkte und Services',
|
||||
order: 6,
|
||||
questions: [
|
||||
{
|
||||
id: 'prod_cookies_consent',
|
||||
type: 'boolean',
|
||||
question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
|
||||
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
|
||||
required: true,
|
||||
scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'prod_api_external',
|
||||
type: 'boolean',
|
||||
question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
|
||||
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
|
||||
required: true,
|
||||
scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'prod_data_broker',
|
||||
type: 'boolean',
|
||||
question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
|
||||
helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
|
||||
required: true,
|
||||
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden questions -- removed from UI but still contribute to scoring.
|
||||
* These are auto-filled from the Company Profile.
|
||||
*/
|
||||
export const HIDDEN_SCORING_QUESTIONS: ScopeProfilingQuestion[] = [
|
||||
{
|
||||
id: 'org_employee_count',
|
||||
type: 'number',
|
||||
question: 'Mitarbeiterzahl (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
|
||||
mapsToCompanyProfile: 'employeeCount',
|
||||
},
|
||||
{
|
||||
id: 'org_annual_revenue',
|
||||
type: 'single',
|
||||
question: 'Jahresumsatz (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
|
||||
mapsToCompanyProfile: 'annualRevenue',
|
||||
},
|
||||
{
|
||||
id: 'org_industry',
|
||||
type: 'single',
|
||||
question: 'Branche (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
||||
mapsToCompanyProfile: 'industry',
|
||||
mapsToVVTQuestion: 'org_industry',
|
||||
mapsToLFQuestion: 'org-branche',
|
||||
},
|
||||
{
|
||||
id: 'org_business_model',
|
||||
type: 'single',
|
||||
question: 'Geschäftsmodell (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
|
||||
mapsToCompanyProfile: 'businessModel',
|
||||
mapsToVVTQuestion: 'org_b2b_b2c',
|
||||
mapsToLFQuestion: 'org-geschaeftsmodell',
|
||||
},
|
||||
{
|
||||
id: 'org_has_dsb',
|
||||
type: 'boolean',
|
||||
question: 'DSB vorhanden (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'org_cert_target',
|
||||
type: 'multi',
|
||||
question: 'Zertifizierungen (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
|
||||
},
|
||||
{
|
||||
id: 'data_volume',
|
||||
type: 'single',
|
||||
question: 'Personendatensaetze (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'prod_type',
|
||||
type: 'multi',
|
||||
question: 'Angebotstypen (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
|
||||
},
|
||||
{
|
||||
id: 'prod_webshop',
|
||||
type: 'boolean',
|
||||
question: 'Webshop (aus Profil)',
|
||||
required: false,
|
||||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Block 7: KI-Systeme (portiert aus Company Profile Step 7)
|
||||
*/
|
||||
export const BLOCK_7_AI_SYSTEMS: ScopeQuestionBlock = {
|
||||
id: 'ai_systems',
|
||||
title: 'KI-Systeme',
|
||||
description: 'Erfassung eingesetzter KI-Systeme für EU AI Act und DSGVO-Dokumentation',
|
||||
order: 7,
|
||||
questions: [
|
||||
{
|
||||
id: 'ai_uses_ai',
|
||||
type: 'boolean',
|
||||
question: 'Setzt Ihr Unternehmen KI-Systeme ein?',
|
||||
helpText: 'Chatbots, Empfehlungssysteme, automatisierte Entscheidungen, Copilot, etc.',
|
||||
required: true,
|
||||
scoreWeights: { risk: 8, complexity: 7, assurance: 6 },
|
||||
},
|
||||
{
|
||||
id: 'ai_categories',
|
||||
type: 'multi',
|
||||
question: 'Welche Kategorien von KI-Systemen setzen Sie ein?',
|
||||
helpText: 'Mehrfachauswahl möglich. Wird nur angezeigt, wenn KI im Einsatz ist.',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 'chatbot', label: 'Text-KI / Chatbots (ChatGPT, Claude, Gemini)' },
|
||||
{ value: 'office', label: 'Office / Produktivität (Copilot, Workspace AI)' },
|
||||
{ value: 'code', label: 'Code-Assistenz (GitHub Copilot, Cursor)' },
|
||||
{ value: 'image', label: 'Bildgenerierung (DALL-E, Midjourney, Firefly)' },
|
||||
{ value: 'translation', label: 'Übersetzung / Sprache (DeepL)' },
|
||||
{ value: 'crm', label: 'CRM / Sales KI (Salesforce Einstein, HubSpot AI)' },
|
||||
{ value: 'internal', label: 'Eigene / interne KI-Systeme' },
|
||||
{ value: 'other', label: 'Sonstige KI-Systeme' },
|
||||
],
|
||||
scoreWeights: { risk: 5, complexity: 5, assurance: 5 },
|
||||
},
|
||||
{
|
||||
id: 'ai_personal_data',
|
||||
type: 'boolean',
|
||||
question: 'Werden personenbezogene Daten an KI-Systeme übermittelt?',
|
||||
helpText: 'Z.B. Kundendaten in ChatGPT eingeben, E-Mails mit Copilot verarbeiten',
|
||||
required: false,
|
||||
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'ai_risk_assessment',
|
||||
type: 'single',
|
||||
question: 'Haben Sie eine KI-Risikobewertung nach EU AI Act durchgeführt?',
|
||||
helpText: 'Risikoeinstufung der KI-Systeme (verboten / hochriskant / begrenzt / minimal)',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 'yes', label: 'Ja' },
|
||||
{ value: 'no', label: 'Nein' },
|
||||
{ value: 'not_yet', label: 'Noch nicht' },
|
||||
],
|
||||
scoreWeights: { risk: -5, complexity: 3, assurance: 8 },
|
||||
},
|
||||
],
|
||||
}
|
||||
358
admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts
Normal file
358
admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import type {
|
||||
ScopeQuestionBlockId,
|
||||
ScopeProfilingQuestion,
|
||||
ScopeProfilingAnswer,
|
||||
} from './compliance-scope-types'
|
||||
import type { CompanyProfile } from './types'
|
||||
import {
|
||||
HIDDEN_SCORING_QUESTIONS,
|
||||
} from './compliance-scope-profiling-blocks'
|
||||
import {
|
||||
SCOPE_QUESTION_BLOCKS,
|
||||
} from './compliance-scope-profiling-vvt-blocks'
|
||||
|
||||
/**
|
||||
* Prefill scope answers from CompanyProfile.
|
||||
*/
|
||||
export function prefillFromCompanyProfile(
|
||||
profile: CompanyProfile
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
|
||||
// dpoName -> org_has_dsb (auto-filled, not shown in UI)
|
||||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
||||
answers.push({
|
||||
questionId: 'org_has_dsb',
|
||||
value: true,
|
||||
})
|
||||
}
|
||||
|
||||
// offerings -> prod_type mapping (auto-filled, not shown in UI)
|
||||
if (profile.offerings && profile.offerings.length > 0) {
|
||||
const prodTypes: string[] = []
|
||||
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
|
||||
|
||||
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
|
||||
prodTypes.push('webapp')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
|
||||
) {
|
||||
prodTypes.push('mobile')
|
||||
}
|
||||
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
|
||||
prodTypes.push('saas')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some(
|
||||
(o) => o.includes('onpremise') || o.includes('on-premise')
|
||||
)
|
||||
) {
|
||||
prodTypes.push('onpremise')
|
||||
}
|
||||
if (offeringsLower.some((o) => o.includes('api'))) {
|
||||
prodTypes.push('api')
|
||||
}
|
||||
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
|
||||
prodTypes.push('iot')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some(
|
||||
(o) => o.includes('beratung') || o.includes('consulting')
|
||||
)
|
||||
) {
|
||||
prodTypes.push('beratung')
|
||||
}
|
||||
if (
|
||||
offeringsLower.some(
|
||||
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
|
||||
)
|
||||
) {
|
||||
prodTypes.push('handel')
|
||||
}
|
||||
|
||||
if (prodTypes.length > 0) {
|
||||
answers.push({
|
||||
questionId: 'prod_type',
|
||||
value: prodTypes,
|
||||
})
|
||||
}
|
||||
|
||||
// webshop auto-fill
|
||||
if (offeringsLower.some((o) => o.includes('webshop') || o.includes('shop'))) {
|
||||
answers.push({
|
||||
questionId: 'prod_webshop',
|
||||
value: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auto-filled scoring values for questions removed from UI.
|
||||
*/
|
||||
export function getAutoFilledScoringAnswers(
|
||||
profile: CompanyProfile
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
|
||||
if (profile.employeeCount != null) {
|
||||
answers.push({ questionId: 'org_employee_count', value: profile.employeeCount })
|
||||
}
|
||||
if (profile.annualRevenue) {
|
||||
answers.push({ questionId: 'org_annual_revenue', value: profile.annualRevenue })
|
||||
}
|
||||
if (profile.industry && profile.industry.length > 0) {
|
||||
answers.push({ questionId: 'org_industry', value: profile.industry.join(', ') })
|
||||
}
|
||||
if (profile.businessModel) {
|
||||
answers.push({ questionId: 'org_business_model', value: profile.businessModel })
|
||||
}
|
||||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
||||
answers.push({ questionId: 'org_has_dsb', value: true })
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Get profile info summary for display in "Aus Profil" info boxes.
|
||||
*/
|
||||
export function getProfileInfoForBlock(
|
||||
profile: CompanyProfile,
|
||||
blockId: ScopeQuestionBlockId
|
||||
): { label: string; value: string }[] {
|
||||
const items: { label: string; value: string }[] = []
|
||||
|
||||
if (blockId === 'organisation') {
|
||||
if (profile.industry && profile.industry.length > 0) items.push({ label: 'Branche', value: profile.industry.join(', ') })
|
||||
if (profile.employeeCount) items.push({ label: 'Mitarbeiter', value: profile.employeeCount })
|
||||
if (profile.annualRevenue) items.push({ label: 'Umsatz', value: profile.annualRevenue })
|
||||
if (profile.businessModel) items.push({ label: 'Geschäftsmodell', value: profile.businessModel })
|
||||
if (profile.dpoName) items.push({ label: 'DSB', value: profile.dpoName })
|
||||
}
|
||||
|
||||
if (blockId === 'product') {
|
||||
if (profile.offerings && profile.offerings.length > 0) {
|
||||
items.push({ label: 'Angebote', value: profile.offerings.join(', ') })
|
||||
}
|
||||
const hasWebshop = profile.offerings?.some(o => o.toLowerCase().includes('webshop') || o.toLowerCase().includes('shop'))
|
||||
if (hasWebshop) items.push({ label: 'Webshop', value: 'Ja' })
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill scope answers from VVT profiling answers
|
||||
*/
|
||||
export function prefillFromVVTAnswers(
|
||||
vvtAnswers: Record<string, unknown>
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
const reverseMap: Record<string, string> = {}
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
for (const q of block.questions) {
|
||||
if (q.mapsToVVTQuestion) {
|
||||
reverseMap[q.mapsToVVTQuestion] = q.id
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
|
||||
const scopeQuestionId = reverseMap[vvtQuestionId]
|
||||
if (scopeQuestionId) {
|
||||
answers.push({ questionId: scopeQuestionId, value: vvtValue })
|
||||
}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill scope answers from Loeschfristen profiling answers
|
||||
*/
|
||||
export function prefillFromLoeschfristenAnswers(
|
||||
lfAnswers: Array<{ questionId: string; value: unknown }>
|
||||
): ScopeProfilingAnswer[] {
|
||||
const answers: ScopeProfilingAnswer[] = []
|
||||
const reverseMap: Record<string, string> = {}
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
for (const q of block.questions) {
|
||||
if (q.mapsToLFQuestion) {
|
||||
reverseMap[q.mapsToLFQuestion] = q.id
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const lfAnswer of lfAnswers) {
|
||||
const scopeQuestionId = reverseMap[lfAnswer.questionId]
|
||||
if (scopeQuestionId) {
|
||||
answers.push({ questionId: scopeQuestionId, value: lfAnswer.value })
|
||||
}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scope answers in VVT format
|
||||
*/
|
||||
export function exportToVVTAnswers(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Record<string, unknown> {
|
||||
const vvtAnswers: Record<string, unknown> = {}
|
||||
for (const answer of scopeAnswers) {
|
||||
let question: ScopeProfilingQuestion | undefined
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
question = block.questions.find((q) => q.id === answer.questionId)
|
||||
if (question) break
|
||||
}
|
||||
if (question?.mapsToVVTQuestion) {
|
||||
vvtAnswers[question.mapsToVVTQuestion] = answer.value
|
||||
}
|
||||
}
|
||||
return vvtAnswers
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scope answers in Loeschfristen format
|
||||
*/
|
||||
export function exportToLoeschfristenAnswers(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Array<{ questionId: string; value: unknown }> {
|
||||
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
|
||||
for (const answer of scopeAnswers) {
|
||||
let question: ScopeProfilingQuestion | undefined
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
question = block.questions.find((q) => q.id === answer.questionId)
|
||||
if (question) break
|
||||
}
|
||||
if (question?.mapsToLFQuestion) {
|
||||
lfAnswers.push({ questionId: question.mapsToLFQuestion, value: answer.value })
|
||||
}
|
||||
}
|
||||
return lfAnswers
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scope answers for TOM generator
|
||||
*/
|
||||
export function exportToTOMProfile(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Record<string, unknown> {
|
||||
const tomProfile: Record<string, unknown> = {}
|
||||
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
|
||||
|
||||
tomProfile.industry = getVal('org_industry')
|
||||
tomProfile.employeeCount = getVal('org_employee_count')
|
||||
tomProfile.hasDataMinors = getVal('data_minors')
|
||||
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
|
||||
? (getVal('data_art9') as string[]).length > 0
|
||||
: false
|
||||
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
|
||||
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
|
||||
? !(getVal('proc_ai_usage') as string[]).includes('keine')
|
||||
: false
|
||||
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
|
||||
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
|
||||
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
|
||||
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
|
||||
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
|
||||
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
|
||||
tomProfile.hasTraining = getVal('proc_training')
|
||||
|
||||
return tomProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is complete (all required questions answered)
|
||||
*/
|
||||
export function isBlockComplete(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
blockId: ScopeQuestionBlockId
|
||||
): boolean {
|
||||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
||||
if (!block) return false
|
||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||||
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress for a specific block (0-100)
|
||||
*/
|
||||
export function getBlockProgress(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
blockId: ScopeQuestionBlockId
|
||||
): number {
|
||||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
||||
if (!block) return 0
|
||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||||
if (requiredQuestions.length === 0) return 100
|
||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||||
const answeredCount = requiredQuestions.filter((q) =>
|
||||
answeredQuestionIds.has(q.id)
|
||||
).length
|
||||
return Math.round((answeredCount / requiredQuestions.length) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total progress across all blocks (0-100)
|
||||
*/
|
||||
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
|
||||
let totalRequired = 0
|
||||
let totalAnswered = 0
|
||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||||
totalRequired += requiredQuestions.length
|
||||
totalAnswered += requiredQuestions.filter((q) =>
|
||||
answeredQuestionIds.has(q.id)
|
||||
).length
|
||||
}
|
||||
if (totalRequired === 0) return 100
|
||||
return Math.round((totalAnswered / totalRequired) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get answer value for a specific question
|
||||
*/
|
||||
export function getAnswerValue(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
questionId: string
|
||||
): unknown {
|
||||
const answer = answers.find((a) => a.questionId === questionId)
|
||||
return answer?.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all questions as a flat array (including hidden auto-filled questions)
|
||||
*/
|
||||
export function getAllQuestions(): ScopeProfilingQuestion[] {
|
||||
return [
|
||||
...SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions),
|
||||
...HIDDEN_SCORING_QUESTIONS,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unanswered required questions, optionally filtered by block.
|
||||
*/
|
||||
export function getUnansweredRequiredQuestions(
|
||||
answers: ScopeProfilingAnswer[],
|
||||
blockId?: ScopeQuestionBlockId
|
||||
): { blockId: ScopeQuestionBlockId; blockTitle: string; question: ScopeProfilingQuestion }[] {
|
||||
const answeredIds = new Set(answers.map((a) => a.questionId))
|
||||
const blocks = blockId
|
||||
? SCOPE_QUESTION_BLOCKS.filter((b) => b.id === blockId)
|
||||
: SCOPE_QUESTION_BLOCKS
|
||||
|
||||
const result: { blockId: ScopeQuestionBlockId; blockTitle: string; question: ScopeProfilingQuestion }[] = []
|
||||
for (const block of blocks) {
|
||||
for (const q of block.questions) {
|
||||
if (q.required && !answeredIds.has(q.id)) {
|
||||
result.push({ blockId: block.id, blockTitle: block.title, question: q })
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import type {
|
||||
ScopeQuestionBlock,
|
||||
} from './compliance-scope-types'
|
||||
import { DEPARTMENT_DATA_CATEGORIES } from './vvt-profiling'
|
||||
import {
|
||||
BLOCK_1_ORGANISATION,
|
||||
BLOCK_2_DATA,
|
||||
BLOCK_3_PROCESSING,
|
||||
BLOCK_4_TECH,
|
||||
BLOCK_5_PROCESSES,
|
||||
BLOCK_6_PRODUCT,
|
||||
BLOCK_7_AI_SYSTEMS,
|
||||
} from './compliance-scope-profiling-blocks'
|
||||
|
||||
/**
|
||||
* Block 8: Verarbeitungstätigkeiten (portiert aus Company Profile Step 6)
|
||||
*/
|
||||
const BLOCK_8_VVT: ScopeQuestionBlock = {
|
||||
id: 'vvt',
|
||||
title: 'Verarbeitungstätigkeiten',
|
||||
description: 'Übersicht der Datenverarbeitungen nach Art. 30 DSGVO',
|
||||
order: 8,
|
||||
questions: [
|
||||
{
|
||||
id: 'vvt_departments',
|
||||
type: 'multi',
|
||||
question: 'In welchen Abteilungen werden personenbezogene Daten verarbeitet?',
|
||||
helpText: 'Wählen Sie alle Abteilungen, in denen Verarbeitungstätigkeiten stattfinden',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'personal', label: 'Personal / HR' },
|
||||
{ value: 'finanzen', label: 'Finanzen / Buchhaltung' },
|
||||
{ value: 'vertrieb', label: 'Vertrieb / Sales' },
|
||||
{ value: 'marketing', label: 'Marketing' },
|
||||
{ value: 'it', label: 'IT / Administration' },
|
||||
{ value: 'recht', label: 'Recht / Compliance' },
|
||||
{ value: 'kundenservice', label: 'Kundenservice / Support' },
|
||||
{ value: 'produktion', label: 'Produktion / Fertigung' },
|
||||
{ value: 'logistik', label: 'Logistik / Versand' },
|
||||
{ value: 'einkauf', label: 'Einkauf / Beschaffung' },
|
||||
{ value: 'facility', label: 'Facility Management' },
|
||||
],
|
||||
scoreWeights: { risk: 10, complexity: 10, assurance: 8 },
|
||||
},
|
||||
{
|
||||
id: 'vvt_data_categories',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien werden verarbeitet?',
|
||||
helpText: 'Wählen Sie alle zutreffenden Kategorien personenbezogener Daten',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'stammdaten', label: 'Stammdaten (Name, Geburtsdatum)' },
|
||||
{ value: 'kontaktdaten', label: 'Kontaktdaten (E-Mail, Telefon, Adresse)' },
|
||||
{ value: 'vertragsdaten', label: 'Vertragsdaten' },
|
||||
{ value: 'zahlungsdaten', label: 'Zahlungs-/Bankdaten' },
|
||||
{ value: 'beschaeftigtendaten', label: 'Beschäftigtendaten (Gehalt, Arbeitszeiten)' },
|
||||
{ value: 'kommunikation', label: 'Kommunikationsdaten (E-Mail, Chat)' },
|
||||
{ value: 'nutzungsdaten', label: 'Nutzungs-/Logdaten (IP, Klicks)' },
|
||||
{ value: 'standortdaten', label: 'Standortdaten' },
|
||||
{ value: 'bilddaten', label: 'Bild-/Videodaten' },
|
||||
{ value: 'bewerberdaten', label: 'Bewerberdaten' },
|
||||
],
|
||||
scoreWeights: { risk: 8, complexity: 7, assurance: 7 },
|
||||
},
|
||||
{
|
||||
id: 'vvt_special_categories',
|
||||
type: 'boolean',
|
||||
question: 'Verarbeiten Sie besondere Kategorien (Art. 9 DSGVO) in Ihren Tätigkeiten?',
|
||||
helpText: 'Gesundheit, Biometrie, Religion, Gewerkschaft — über die bereits in Block 2 erfassten hinaus',
|
||||
required: true,
|
||||
scoreWeights: { risk: 10, complexity: 5, assurance: 8 },
|
||||
},
|
||||
{
|
||||
id: 'vvt_has_vvt',
|
||||
type: 'boolean',
|
||||
question: 'Haben Sie bereits ein Verarbeitungsverzeichnis (VVT)?',
|
||||
helpText: 'Dokumentation aller Verarbeitungstätigkeiten nach Art. 30 DSGVO',
|
||||
required: true,
|
||||
scoreWeights: { risk: -5, complexity: 3, assurance: 8 },
|
||||
},
|
||||
{
|
||||
id: 'vvt_external_processors',
|
||||
type: 'boolean',
|
||||
question: 'Setzen Sie externe Dienstleister als Auftragsverarbeiter ein?',
|
||||
helpText: 'Lohnbüro, Hosting-Provider, Cloud-Dienste, externe IT etc.',
|
||||
required: true,
|
||||
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Block 9: Datenkategorien pro Abteilung
|
||||
* Generiert Fragen dynamisch aus DEPARTMENT_DATA_CATEGORIES
|
||||
*/
|
||||
const BLOCK_9_DATENKATEGORIEN: ScopeQuestionBlock = {
|
||||
id: 'datenkategorien_detail',
|
||||
title: 'Datenkategorien pro Abteilung',
|
||||
description: 'Detaillierte Erfassung der Datenkategorien je Abteilung — basierend auf Ihrer Abteilungswahl in Block 8',
|
||||
order: 9,
|
||||
questions: [
|
||||
{
|
||||
id: 'dk_dept_hr',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihre Personalabteilung?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer den HR-Bereich',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_hr.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||||
mapsToVVTQuestion: 'dept_hr_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_recruiting',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihr Recruiting?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer das Bewerbermanagement',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_recruiting.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
|
||||
mapsToVVTQuestion: 'dept_recruiting_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_finance',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihre Finanzabteilung?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Finanzen & Buchhaltung',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_finance.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||||
mapsToVVTQuestion: 'dept_finance_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_sales',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihr Vertrieb?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Vertrieb & CRM',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_sales.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 5, complexity: 4, assurance: 4 },
|
||||
mapsToVVTQuestion: 'dept_sales_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_marketing',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihr Marketing?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Marketing',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_marketing.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
|
||||
mapsToVVTQuestion: 'dept_marketing_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_support',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihr Kundenservice?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Support',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_support.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
|
||||
mapsToVVTQuestion: 'dept_support_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_it',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihre IT-Abteilung?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer IT / Administration',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_it.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
||||
mapsToVVTQuestion: 'dept_it_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_recht',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihre Rechtsabteilung?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Recht / Compliance',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_recht.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 6, complexity: 4, assurance: 6 },
|
||||
mapsToVVTQuestion: 'dept_recht_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_produktion',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihre Produktion?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Produktion / Fertigung',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_produktion.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||||
mapsToVVTQuestion: 'dept_produktion_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_logistik',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihre Logistik?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Logistik / Versand',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_logistik.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
|
||||
mapsToVVTQuestion: 'dept_logistik_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_einkauf',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihr Einkauf?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Einkauf / Beschaffung',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_einkauf.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 4, complexity: 3, assurance: 4 },
|
||||
mapsToVVTQuestion: 'dept_einkauf_categories',
|
||||
},
|
||||
{
|
||||
id: 'dk_dept_facility',
|
||||
type: 'multi',
|
||||
question: 'Welche Datenkategorien verarbeitet Ihr Facility Management?',
|
||||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Facility Management',
|
||||
required: false,
|
||||
options: DEPARTMENT_DATA_CATEGORIES.dept_facility.categories.map(c => ({
|
||||
value: c.id,
|
||||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||||
})),
|
||||
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
|
||||
mapsToVVTQuestion: 'dept_facility_categories',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* All question blocks in order
|
||||
*/
|
||||
export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
|
||||
BLOCK_1_ORGANISATION,
|
||||
BLOCK_2_DATA,
|
||||
BLOCK_3_PROCESSING,
|
||||
BLOCK_4_TECH,
|
||||
BLOCK_5_PROCESSES,
|
||||
BLOCK_6_PRODUCT,
|
||||
BLOCK_7_AI_SYSTEMS,
|
||||
BLOCK_8_VVT,
|
||||
BLOCK_9_DATENKATEGORIEN,
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
823
admin-compliance/lib/sdk/compliance-scope-triggers.ts
Normal file
823
admin-compliance/lib/sdk/compliance-scope-triggers.ts
Normal file
@@ -0,0 +1,823 @@
|
||||
/**
|
||||
* 50 Hard Trigger Rules — data table.
|
||||
*
|
||||
* This file legitimately exceeds 500 LOC because it is a pure data
|
||||
* definition with no logic. Splitting it further would hurt readability.
|
||||
*/
|
||||
import type { HardTriggerRule } from './compliance-scope-types'
|
||||
|
||||
// ============================================================================
|
||||
// 50 HARD TRIGGER RULES
|
||||
// ============================================================================
|
||||
|
||||
export const HARD_TRIGGER_RULES: HardTriggerRule[] = [
|
||||
// ========== A: Art. 9 Besondere Kategorien (9 rules) ==========
|
||||
{
|
||||
id: 'HT-A01',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'gesundheit',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung von Gesundheitsdaten',
|
||||
},
|
||||
{
|
||||
id: 'HT-A02',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'biometrie',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung biometrischer Daten zur eindeutigen Identifizierung',
|
||||
},
|
||||
{
|
||||
id: 'HT-A03',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'genetik',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung genetischer Daten',
|
||||
},
|
||||
{
|
||||
id: 'HT-A04',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'politisch',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung politischer Meinungen',
|
||||
},
|
||||
{
|
||||
id: 'HT-A05',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'religion',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung religiöser oder weltanschaulicher Überzeugungen',
|
||||
},
|
||||
{
|
||||
id: 'HT-A06',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'gewerkschaft',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung von Gewerkschaftszugehörigkeit',
|
||||
},
|
||||
{
|
||||
id: 'HT-A07',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'sexualleben',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung von Daten zum Sexualleben oder zur sexuellen Orientierung',
|
||||
},
|
||||
{
|
||||
id: 'HT-A08',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'strafrechtlich',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 10 DSGVO',
|
||||
description: 'Verarbeitung strafrechtlicher Verurteilungen',
|
||||
},
|
||||
{
|
||||
id: 'HT-A09',
|
||||
category: 'art9',
|
||||
questionId: 'data_art9',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'ethnisch',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 Abs. 1 DSGVO',
|
||||
description: 'Verarbeitung der rassischen oder ethnischen Herkunft',
|
||||
},
|
||||
|
||||
// ========== B: Vulnerable Gruppen (3 rules) ==========
|
||||
{
|
||||
id: 'HT-B01',
|
||||
category: 'vulnerable',
|
||||
questionId: 'data_minors',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'DSE'],
|
||||
legalReference: 'Art. 8 DSGVO',
|
||||
description: 'Verarbeitung von Daten Minderjähriger',
|
||||
},
|
||||
{
|
||||
id: 'HT-B02',
|
||||
category: 'vulnerable',
|
||||
questionId: 'data_minors',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'DSE'],
|
||||
legalReference: 'Art. 8 + Art. 9 DSGVO',
|
||||
description: 'Verarbeitung besonderer Kategorien von Daten Minderjähriger',
|
||||
combineWithArt9: true,
|
||||
},
|
||||
{
|
||||
id: 'HT-B03',
|
||||
category: 'vulnerable',
|
||||
questionId: 'data_minors',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'AI_ACT_DOKU'],
|
||||
legalReference: 'Art. 8 DSGVO + AI Act',
|
||||
description: 'KI-gestützte Verarbeitung von Daten Minderjähriger',
|
||||
combineWithAI: true,
|
||||
},
|
||||
|
||||
// ========== C: ADM/KI (6 rules) ==========
|
||||
{
|
||||
id: 'HT-C01',
|
||||
category: 'adm',
|
||||
questionId: 'proc_adm_scoring',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 22 DSGVO',
|
||||
description: 'Automatisierte Einzelentscheidung mit Rechtswirkung oder erheblicher Beeinträchtigung',
|
||||
},
|
||||
{
|
||||
id: 'HT-C02',
|
||||
category: 'adm',
|
||||
questionId: 'proc_ai_usage',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'autonom',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'AI_ACT_DOKU'],
|
||||
legalReference: 'Art. 22 DSGVO + AI Act',
|
||||
description: 'Autonome KI-Systeme mit Entscheidungsbefugnis',
|
||||
},
|
||||
{
|
||||
id: 'HT-C03',
|
||||
category: 'adm',
|
||||
questionId: 'proc_ai_usage',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'scoring',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM'],
|
||||
legalReference: 'Art. 22 DSGVO',
|
||||
description: 'KI-gestütztes Scoring',
|
||||
},
|
||||
{
|
||||
id: 'HT-C04',
|
||||
category: 'adm',
|
||||
questionId: 'proc_ai_usage',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'profiling',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 22 DSGVO',
|
||||
description: 'KI-gestütztes Profiling mit erheblicher Wirkung',
|
||||
},
|
||||
{
|
||||
id: 'HT-C05',
|
||||
category: 'adm',
|
||||
questionId: 'proc_ai_usage',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'generativ',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'AI_ACT_DOKU'],
|
||||
legalReference: 'AI Act',
|
||||
description: 'Generative KI-Systeme',
|
||||
},
|
||||
{
|
||||
id: 'HT-C06',
|
||||
category: 'adm',
|
||||
questionId: 'proc_ai_usage',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'chatbot',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'AI_ACT_DOKU'],
|
||||
legalReference: 'AI Act',
|
||||
description: 'Chatbots mit Personendatenverarbeitung',
|
||||
},
|
||||
|
||||
// ========== D: Überwachung (5 rules) ==========
|
||||
{
|
||||
id: 'HT-D01',
|
||||
category: 'surveillance',
|
||||
questionId: 'proc_video_surveillance',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSE'],
|
||||
legalReference: 'Art. 6 DSGVO',
|
||||
description: 'Videoüberwachung',
|
||||
},
|
||||
{
|
||||
id: 'HT-D02',
|
||||
category: 'surveillance',
|
||||
questionId: 'proc_employee_monitoring',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 88 DSGVO + BetrVG',
|
||||
description: 'Mitarbeiterüberwachung',
|
||||
},
|
||||
{
|
||||
id: 'HT-D03',
|
||||
category: 'surveillance',
|
||||
questionId: 'proc_tracking',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNGEN'],
|
||||
legalReference: 'Art. 6 DSGVO + ePrivacy',
|
||||
description: 'Online-Tracking',
|
||||
},
|
||||
{
|
||||
id: 'HT-D04',
|
||||
category: 'surveillance',
|
||||
questionId: 'proc_video_surveillance',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 35 Abs. 3 DSGVO',
|
||||
description: 'Videoüberwachung kombiniert mit Mitarbeitermonitoring',
|
||||
combineWithEmployeeMonitoring: true,
|
||||
},
|
||||
{
|
||||
id: 'HT-D05',
|
||||
category: 'surveillance',
|
||||
questionId: 'proc_video_surveillance',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 35 Abs. 3 DSGVO',
|
||||
description: 'Videoüberwachung kombiniert mit automatisierter Bewertung',
|
||||
combineWithADM: true,
|
||||
},
|
||||
|
||||
// ========== E: Drittland (5 rules) ==========
|
||||
{
|
||||
id: 'HT-E01',
|
||||
category: 'third_country',
|
||||
questionId: 'tech_third_country',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TRANSFER_DOKU'],
|
||||
legalReference: 'Art. 44 ff. DSGVO',
|
||||
description: 'Datenübermittlung in Drittland',
|
||||
},
|
||||
{
|
||||
id: 'HT-E02',
|
||||
category: 'third_country',
|
||||
questionId: 'tech_hosting_location',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: 'drittland',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'TRANSFER_DOKU'],
|
||||
legalReference: 'Art. 44 ff. DSGVO',
|
||||
description: 'Hosting in Drittland',
|
||||
},
|
||||
{
|
||||
id: 'HT-E03',
|
||||
category: 'third_country',
|
||||
questionId: 'tech_hosting_location',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: 'us_adequacy',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TRANSFER_DOKU'],
|
||||
legalReference: 'Art. 45 DSGVO',
|
||||
description: 'Hosting in USA mit Angemessenheitsbeschluss',
|
||||
},
|
||||
{
|
||||
id: 'HT-E04',
|
||||
category: 'third_country',
|
||||
questionId: 'tech_third_country',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'TRANSFER_DOKU', 'DSFA'],
|
||||
legalReference: 'Art. 44 ff. + Art. 9 DSGVO',
|
||||
description: 'Drittlandtransfer besonderer Kategorien',
|
||||
combineWithArt9: true,
|
||||
},
|
||||
{
|
||||
id: 'HT-E05',
|
||||
category: 'third_country',
|
||||
questionId: 'tech_third_country',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'TRANSFER_DOKU', 'DSFA'],
|
||||
legalReference: 'Art. 44 ff. + Art. 8 DSGVO',
|
||||
description: 'Drittlandtransfer von Daten Minderjähriger',
|
||||
combineWithMinors: true,
|
||||
},
|
||||
|
||||
// ========== F: Zertifizierung (5 rules) ==========
|
||||
{
|
||||
id: 'HT-F01',
|
||||
category: 'certification',
|
||||
questionId: 'org_cert_target',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'ISO27001',
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST'],
|
||||
legalReference: 'ISO/IEC 27001',
|
||||
description: 'Angestrebte ISO 27001 Zertifizierung',
|
||||
},
|
||||
{
|
||||
id: 'HT-F02',
|
||||
category: 'certification',
|
||||
questionId: 'org_cert_target',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'ISO27701',
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM', 'VVT', 'AUDIT_CHECKLIST'],
|
||||
legalReference: 'ISO/IEC 27701',
|
||||
description: 'Angestrebte ISO 27701 Zertifizierung',
|
||||
},
|
||||
{
|
||||
id: 'HT-F03',
|
||||
category: 'certification',
|
||||
questionId: 'org_cert_target',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'SOC2',
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST'],
|
||||
legalReference: 'SOC 2 Type II',
|
||||
description: 'Angestrebte SOC 2 Zertifizierung',
|
||||
},
|
||||
{
|
||||
id: 'HT-F04',
|
||||
category: 'certification',
|
||||
questionId: 'org_cert_target',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'TISAX',
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST', 'VENDOR_MANAGEMENT'],
|
||||
legalReference: 'TISAX',
|
||||
description: 'Angestrebte TISAX Zertifizierung',
|
||||
},
|
||||
{
|
||||
id: 'HT-F05',
|
||||
category: 'certification',
|
||||
questionId: 'org_cert_target',
|
||||
condition: 'CONTAINS',
|
||||
conditionValue: 'BSI-Grundschutz',
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM', 'AUDIT_CHECKLIST'],
|
||||
legalReference: 'BSI IT-Grundschutz',
|
||||
description: 'Angestrebte BSI-Grundschutz Zertifizierung',
|
||||
},
|
||||
|
||||
// ========== G: Volumen/Skala (5 rules) ==========
|
||||
{
|
||||
id: 'HT-G01',
|
||||
category: 'scale',
|
||||
questionId: 'data_volume',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: '>1000000',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'LOESCHKONZEPT'],
|
||||
legalReference: 'Art. 35 Abs. 3 lit. b DSGVO',
|
||||
description: 'Umfangreiche Verarbeitung personenbezogener Daten (>1 Mio. Datensätze)',
|
||||
},
|
||||
{
|
||||
id: 'HT-G02',
|
||||
category: 'scale',
|
||||
questionId: 'data_volume',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: '100000-1000000',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM'],
|
||||
legalReference: 'Art. 35 Abs. 3 lit. b DSGVO',
|
||||
description: 'Großvolumige Datenverarbeitung (100k-1M Datensätze)',
|
||||
},
|
||||
{
|
||||
id: 'HT-G03',
|
||||
category: 'scale',
|
||||
questionId: 'org_customer_count',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: '100000+',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSR_PROZESS'],
|
||||
legalReference: 'Art. 15-22 DSGVO',
|
||||
description: 'Großer Kundenstamm (>100k) mit hoher Betroffenenanzahl',
|
||||
},
|
||||
{
|
||||
id: 'HT-G04',
|
||||
category: 'scale',
|
||||
questionId: 'org_employee_count',
|
||||
condition: 'GREATER_THAN',
|
||||
conditionValue: 249,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'LOESCHKONZEPT', 'NOTFALLPLAN'],
|
||||
legalReference: 'Art. 37 DSGVO',
|
||||
description: 'Große Organisation (>250 Mitarbeiter) mit erhöhten Compliance-Anforderungen',
|
||||
},
|
||||
{
|
||||
id: 'HT-G05',
|
||||
category: 'scale',
|
||||
questionId: 'org_employee_count',
|
||||
condition: 'GREATER_THAN',
|
||||
conditionValue: 999,
|
||||
minimumLevel: 'L4',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'LOESCHKONZEPT'],
|
||||
legalReference: 'Art. 35 + Art. 37 DSGVO',
|
||||
description: 'Sehr große Organisation (>1000 Mitarbeiter) mit Art. 9 Daten',
|
||||
combineWithArt9: true,
|
||||
},
|
||||
|
||||
// ========== H: Produkt/Business (7 rules) ==========
|
||||
{
|
||||
id: 'HT-H01a',
|
||||
category: 'product',
|
||||
questionId: 'prod_webshop',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
excludeWhen: { questionId: 'org_business_model', value: 'B2B' },
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['DSE', 'AGB', 'COOKIE_BANNER', 'EINWILLIGUNGEN',
|
||||
'WIDERRUFSBELEHRUNG', 'PREISANGABEN', 'FERNABSATZ_INFO', 'STREITBEILEGUNG'],
|
||||
legalReference: 'Art. 6 DSGVO + Fernabsatzrecht + PAngV + VSBG',
|
||||
description: 'E-Commerce / Webshop (B2C) — Verbraucherschutzpflichten',
|
||||
},
|
||||
{
|
||||
id: 'HT-H01b',
|
||||
category: 'product',
|
||||
questionId: 'prod_webshop',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
requireWhen: { questionId: 'org_business_model', value: 'B2B' },
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['DSE', 'AGB', 'COOKIE_BANNER'],
|
||||
legalReference: 'Art. 6 DSGVO + eCommerce',
|
||||
description: 'E-Commerce / Webshop (B2B) — Basis-Pflichten',
|
||||
},
|
||||
{
|
||||
id: 'HT-H02',
|
||||
category: 'product',
|
||||
questionId: 'prod_data_broker',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNGEN'],
|
||||
legalReference: 'Art. 35 Abs. 3 DSGVO',
|
||||
description: 'Datenhandel oder Datenmakler-Tätigkeit',
|
||||
},
|
||||
{
|
||||
id: 'HT-H03',
|
||||
category: 'product',
|
||||
questionId: 'prod_api_external',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM', 'AVV'],
|
||||
legalReference: 'Art. 28 DSGVO',
|
||||
description: 'Externe API mit Datenweitergabe',
|
||||
},
|
||||
{
|
||||
id: 'HT-H04',
|
||||
category: 'product',
|
||||
questionId: 'org_business_model',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: 'b2c',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['DSE', 'COOKIE_BANNER', 'EINWILLIGUNGEN'],
|
||||
legalReference: 'Art. 6 DSGVO',
|
||||
description: 'B2C-Geschäftsmodell mit Endkundenkontakt',
|
||||
},
|
||||
{
|
||||
id: 'HT-H05',
|
||||
category: 'product',
|
||||
questionId: 'org_industry',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: 'finance',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM'],
|
||||
legalReference: 'Art. 6 DSGVO + Finanzaufsicht',
|
||||
description: 'Finanzbranche mit erhöhten regulatorischen Anforderungen',
|
||||
},
|
||||
{
|
||||
id: 'HT-H06',
|
||||
category: 'product',
|
||||
questionId: 'org_industry',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: 'healthcare',
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: true,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSFA'],
|
||||
legalReference: 'Art. 9 DSGVO + Gesundheitsrecht',
|
||||
description: 'Gesundheitsbranche mit sensiblen Daten',
|
||||
},
|
||||
{
|
||||
id: 'HT-H07',
|
||||
category: 'product',
|
||||
questionId: 'org_industry',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: 'public',
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM', 'DSR_PROZESS'],
|
||||
legalReference: 'Art. 6 Abs. 1 lit. e DSGVO',
|
||||
description: 'Öffentlicher Sektor',
|
||||
},
|
||||
|
||||
// ========== I: Prozessreife - Gap Flags (5 rules) ==========
|
||||
{
|
||||
id: 'HT-I01',
|
||||
category: 'process_maturity',
|
||||
questionId: 'proc_dsar_process',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: false,
|
||||
minimumLevel: 'L1',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'Art. 15-22 DSGVO',
|
||||
description: 'Fehlender Prozess für Betroffenenrechte',
|
||||
},
|
||||
{
|
||||
id: 'HT-I02',
|
||||
category: 'process_maturity',
|
||||
questionId: 'proc_deletion_concept',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: false,
|
||||
minimumLevel: 'L1',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'Art. 17 DSGVO',
|
||||
description: 'Fehlendes Löschkonzept',
|
||||
},
|
||||
{
|
||||
id: 'HT-I03',
|
||||
category: 'process_maturity',
|
||||
questionId: 'proc_incident_response',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: false,
|
||||
minimumLevel: 'L1',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'Art. 33 DSGVO',
|
||||
description: 'Fehlender Incident-Response-Prozess',
|
||||
},
|
||||
{
|
||||
id: 'HT-I04',
|
||||
category: 'process_maturity',
|
||||
questionId: 'proc_regular_audits',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: false,
|
||||
minimumLevel: 'L1',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'Art. 24 DSGVO',
|
||||
description: 'Fehlende regelmäßige Audits',
|
||||
},
|
||||
{
|
||||
id: 'HT-I05',
|
||||
category: 'process_maturity',
|
||||
questionId: 'comp_training',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: false,
|
||||
minimumLevel: 'L1',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'Art. 39 Abs. 1 lit. b DSGVO',
|
||||
description: 'Fehlende Schulungen zum Datenschutz',
|
||||
},
|
||||
|
||||
// ========== J: IACE — AI Act Produkt-Triggers (3 rules) ==========
|
||||
{
|
||||
id: 'HT-J01',
|
||||
category: 'iace_ai_act_product',
|
||||
questionId: 'machineBuilder.containsAI',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM'],
|
||||
legalReference: 'EU AI Act Annex I + EU Maschinenverordnung 2023/1230',
|
||||
description: 'KI mit Sicherheitsfunktion in Maschine → AI Act High-Risk',
|
||||
combineWithMachineBuilder: { field: 'hasSafetyFunction', value: true },
|
||||
riskWeight: 9,
|
||||
},
|
||||
{
|
||||
id: 'HT-J02',
|
||||
category: 'iace_ai_act_product',
|
||||
questionId: 'machineBuilder.containsAI',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM'],
|
||||
legalReference: 'EU AI Act + EU Maschinenverordnung 2023/1230',
|
||||
description: 'Autonome KI in Maschine → AI Act + Maschinenverordnung',
|
||||
combineWithMachineBuilder: { field: 'autonomousBehavior', value: true },
|
||||
riskWeight: 8,
|
||||
},
|
||||
{
|
||||
id: 'HT-J03',
|
||||
category: 'iace_ai_act_product',
|
||||
questionId: 'machineBuilder.hasSafetyFunction',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['VVT', 'TOM'],
|
||||
legalReference: 'EU AI Act Annex III',
|
||||
description: 'KI-Bildverarbeitung mit Sicherheitsbezug',
|
||||
combineWithMachineBuilder: { field: 'aiIntegrationType', includes: 'vision' },
|
||||
riskWeight: 8,
|
||||
},
|
||||
|
||||
// ========== K: IACE — CRA Triggers (3 rules) ==========
|
||||
{
|
||||
id: 'HT-K01',
|
||||
category: 'iace_cra',
|
||||
questionId: 'machineBuilder.isNetworked',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM'],
|
||||
legalReference: 'EU Cyber Resilience Act (CRA)',
|
||||
description: 'Vernetztes Produkt → Cyber Resilience Act',
|
||||
riskWeight: 6,
|
||||
},
|
||||
{
|
||||
id: 'HT-K02',
|
||||
category: 'iace_cra',
|
||||
questionId: 'machineBuilder.hasRemoteAccess',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM'],
|
||||
legalReference: 'CRA + NIS2 Art. 21',
|
||||
description: 'Remote-Zugriff → CRA + NIS2 Supply Chain',
|
||||
riskWeight: 7,
|
||||
},
|
||||
{
|
||||
id: 'HT-K03',
|
||||
category: 'iace_cra',
|
||||
questionId: 'machineBuilder.hasOTAUpdates',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM'],
|
||||
legalReference: 'CRA Art. 10 - Patch Management',
|
||||
description: 'OTA-Updates → CRA Patch Management Pflicht',
|
||||
riskWeight: 7,
|
||||
},
|
||||
|
||||
// ========== L: IACE — NIS2 indirekt (2 rules) ==========
|
||||
{
|
||||
id: 'HT-L01',
|
||||
category: 'iace_nis2_indirect',
|
||||
questionId: 'machineBuilder.criticalSectorClients',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM'],
|
||||
legalReference: 'NIS2 Art. 21 - Supply Chain',
|
||||
description: 'Lieferant an KRITIS → NIS2 Supply Chain Anforderungen',
|
||||
riskWeight: 7,
|
||||
},
|
||||
{
|
||||
id: 'HT-L02',
|
||||
category: 'iace_nis2_indirect',
|
||||
questionId: 'machineBuilder.oemClients',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'NIS2 + EU Maschinenverordnung',
|
||||
description: 'OEM-Zulieferer → Compliance-Nachweispflicht',
|
||||
riskWeight: 5,
|
||||
},
|
||||
|
||||
// ========== M: IACE — Maschinenverordnung Triggers (4 rules) ==========
|
||||
{
|
||||
id: 'HT-M01',
|
||||
category: 'iace_machinery_regulation',
|
||||
questionId: 'machineBuilder.containsSoftware',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM'],
|
||||
legalReference: 'EU Maschinenverordnung 2023/1230 Anhang III',
|
||||
description: 'Software als Sicherheitskomponente → Maschinenverordnung',
|
||||
combineWithMachineBuilder: { field: 'hasSafetyFunction', value: true },
|
||||
riskWeight: 9,
|
||||
},
|
||||
{
|
||||
id: 'HT-M02',
|
||||
category: 'iace_machinery_regulation',
|
||||
questionId: 'machineBuilder.ceMarkingRequired',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'EU Maschinenverordnung 2023/1230',
|
||||
description: 'CE-Kennzeichnung erforderlich',
|
||||
riskWeight: 6,
|
||||
},
|
||||
{
|
||||
id: 'HT-M03',
|
||||
category: 'iace_machinery_regulation',
|
||||
questionId: 'machineBuilder.ceMarkingRequired',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L3',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: [],
|
||||
legalReference: 'EU Maschinenverordnung 2023/1230 Art. 10',
|
||||
description: 'CE ohne bestehende Risikobeurteilung → Dringend!',
|
||||
combineWithMachineBuilder: { field: 'hasRiskAssessment', value: false },
|
||||
riskWeight: 9,
|
||||
},
|
||||
{
|
||||
id: 'HT-M04',
|
||||
category: 'iace_machinery_regulation',
|
||||
questionId: 'machineBuilder.containsFirmware',
|
||||
condition: 'EQUALS',
|
||||
conditionValue: true,
|
||||
minimumLevel: 'L2',
|
||||
requiresDSFA: false,
|
||||
mandatoryDocuments: ['TOM'],
|
||||
legalReference: 'EU Maschinenverordnung + CRA',
|
||||
description: 'Firmware mit Remote-Update → Change Management Pflicht',
|
||||
combineWithMachineBuilder: { field: 'hasOTAUpdates', value: true },
|
||||
riskWeight: 7,
|
||||
},
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
83
admin-compliance/lib/sdk/compliance-scope-types/constants.ts
Normal file
83
admin-compliance/lib/sdk/compliance-scope-types/constants.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Constants
|
||||
*
|
||||
* Labels, Beschreibungen und Farben für Compliance-Levels und Dokumenttypen.
|
||||
*/
|
||||
|
||||
import type { ComplianceDepthLevel } from './core-levels'
|
||||
import type { ScopeDocumentType } from './documents'
|
||||
|
||||
/**
|
||||
* Deutsche Bezeichnungen für Compliance-Levels
|
||||
*/
|
||||
export const DEPTH_LEVEL_LABELS: Record<ComplianceDepthLevel, string> = {
|
||||
L1: 'Lean Startup',
|
||||
L2: 'KMU Standard',
|
||||
L3: 'Erweitert',
|
||||
L4: 'Zertifizierungsbereit',
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaillierte Beschreibungen der Compliance-Levels
|
||||
*/
|
||||
export const DEPTH_LEVEL_DESCRIPTIONS: Record<ComplianceDepthLevel, string> = {
|
||||
L1: 'Minimalansatz für kleine Organisationen und Startups. Fokus auf gesetzliche Pflichten mit pragmatischen Lösungen.',
|
||||
L2: 'Standard-Compliance für mittelständische Unternehmen. Ausgewogenes Verhältnis zwischen Aufwand und Compliance-Qualität.',
|
||||
L3: 'Erweiterte Compliance für größere oder risikoreichere Organisationen. Detaillierte Dokumentation und Prozesse.',
|
||||
L4: 'Vollständige Compliance für Zertifizierungen und höchste Anforderungen. Audit-ready Dokumentation.',
|
||||
};
|
||||
|
||||
/**
|
||||
* Farben für Compliance-Levels (Tailwind-kompatibel)
|
||||
*/
|
||||
export const DEPTH_LEVEL_COLORS: Record<ComplianceDepthLevel, { bg: string; border: string; badge: string; text: string }> = {
|
||||
L1: { bg: 'bg-green-50', border: 'border-green-300', badge: 'bg-green-100', text: 'text-green-800' },
|
||||
L2: { bg: 'bg-blue-50', border: 'border-blue-300', badge: 'bg-blue-100', text: 'text-blue-800' },
|
||||
L3: { bg: 'bg-amber-50', border: 'border-amber-300', badge: 'bg-amber-100', text: 'text-amber-800' },
|
||||
L4: { bg: 'bg-red-50', border: 'border-red-300', badge: 'bg-red-100', text: 'text-red-800' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Deutsche Bezeichnungen für alle Dokumenttypen
|
||||
*/
|
||||
export const DOCUMENT_TYPE_LABELS: Record<ScopeDocumentType, string> = {
|
||||
vvt: 'Verzeichnis von Verarbeitungstätigkeiten (VVT)',
|
||||
lf: 'Löschfristenkonzept',
|
||||
tom: 'Technische und organisatorische Maßnahmen (TOM)',
|
||||
av_vertrag: 'Auftragsverarbeitungsvertrag (AVV)',
|
||||
dsi: 'Datenschutz-Informationen (Privacy Policy)',
|
||||
betroffenenrechte: 'Betroffenenrechte-Prozess',
|
||||
dsfa: 'Datenschutz-Folgenabschätzung (DSFA)',
|
||||
daten_transfer: 'Drittlandtransfer-Dokumentation',
|
||||
datenpannen: 'Datenpannen-Prozess',
|
||||
einwilligung: 'Einwilligungsmanagement',
|
||||
vertragsmanagement: 'Vertragsmanagement-Prozess',
|
||||
schulung: 'Mitarbeiterschulung',
|
||||
audit_log: 'Audit & Logging Konzept',
|
||||
risikoanalyse: 'Risikoanalyse',
|
||||
notfallplan: 'Notfall- & Krisenplan',
|
||||
zertifizierung: 'Zertifizierungsvorbereitung',
|
||||
datenschutzmanagement: 'Datenschutzmanagement-System (DSMS)',
|
||||
iace_ce_assessment: 'CE-Risikobeurteilung SW/FW/KI (IACE)',
|
||||
widerrufsbelehrung: 'Widerrufsbelehrung (§ 312g BGB)',
|
||||
preisangaben: 'Preisangaben (PAngV)',
|
||||
fernabsatz_info: 'Informationspflichten Fernabsatz (§ 312d BGB)',
|
||||
streitbeilegung: 'Streitbeilegungshinweis (VSBG § 36)',
|
||||
produktsicherheit: 'Produktsicherheitsdokumentation (GPSR)',
|
||||
ai_act_doku: 'AI Act Technische Dokumentation (Art. 11)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Status-Labels für Scope-Zustand
|
||||
*/
|
||||
export const SCOPE_STATUS_LABELS = {
|
||||
NOT_STARTED: 'Nicht begonnen',
|
||||
IN_PROGRESS: 'In Bearbeitung',
|
||||
COMPLETE: 'Abgeschlossen',
|
||||
NEEDS_UPDATE: 'Aktualisierung erforderlich',
|
||||
};
|
||||
|
||||
/**
|
||||
* LocalStorage Key für Scope State
|
||||
*/
|
||||
export const STORAGE_KEY = 'bp_compliance_scope';
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Core Level Types
|
||||
*
|
||||
* Definiert die grundlegenden Compliance-Tiefenstufen und Score-Typen.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Compliance-Tiefenstufen
|
||||
* - L1: Lean Startup - Minimalansatz für kleine Organisationen
|
||||
* - L2: KMU Standard - Standard-Compliance für mittelständische Unternehmen
|
||||
* - L3: Erweitert - Erweiterte Compliance für größere/risikoreichere Organisationen
|
||||
* - L4: Zertifizierungsbereit - Vollständige Compliance für Zertifizierungen
|
||||
*/
|
||||
export type ComplianceDepthLevel = 'L1' | 'L2' | 'L3' | 'L4';
|
||||
|
||||
/**
|
||||
* Compliance-Scores zur Bestimmung der optimalen Tiefe
|
||||
* Alle Werte zwischen 0-100
|
||||
*/
|
||||
export interface ComplianceScores {
|
||||
/** Risiko-Score (0-100): Höhere Werte = höheres Risiko */
|
||||
risk_score: number;
|
||||
/** Komplexitäts-Score (0-100): Höhere Werte = komplexere Verarbeitung */
|
||||
complexity_score: number;
|
||||
/** Assurance-Bedarf (0-100): Höhere Werte = höherer Nachweis-/Zertifizierungsbedarf */
|
||||
assurance_need: number;
|
||||
/** Zusammengesetzter Score (0-100): Gewichtete Kombination aller Scores */
|
||||
composite_score: number;
|
||||
}
|
||||
111
admin-compliance/lib/sdk/compliance-scope-types/decisions.ts
Normal file
111
admin-compliance/lib/sdk/compliance-scope-types/decisions.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Decision & Output Types
|
||||
*
|
||||
* Definiert die finale Scope-Entscheidung und zugehörige Ausgabetypen.
|
||||
*/
|
||||
|
||||
import type { ComplianceDepthLevel, ComplianceScores } from './core-levels'
|
||||
import type { TriggeredHardTrigger } from './hard-triggers'
|
||||
import type { RequiredDocument, ScopeDocumentType } from './documents'
|
||||
|
||||
/**
|
||||
* Die finale Scope-Entscheidung mit allen Details
|
||||
*/
|
||||
export interface ScopeDecision {
|
||||
/** Eindeutige ID dieser Entscheidung */
|
||||
id: string;
|
||||
/** Bestimmtes Compliance-Level */
|
||||
determinedLevel: ComplianceDepthLevel;
|
||||
/** Berechnete Scores */
|
||||
scores: ComplianceScores;
|
||||
/** Getriggerte Hard Trigger */
|
||||
triggeredHardTriggers: TriggeredHardTrigger[];
|
||||
/** Erforderliche Dokumente mit Details */
|
||||
requiredDocuments: RequiredDocument[];
|
||||
/** Identifizierte Risiko-Flags */
|
||||
riskFlags: RiskFlag[];
|
||||
/** Identifizierte Lücken */
|
||||
gaps: ScopeGap[];
|
||||
/** Empfohlene nächste Schritte */
|
||||
nextActions: NextAction[];
|
||||
/** Begründung der Entscheidung */
|
||||
reasoning: ScopeReasoning[];
|
||||
/** Zeitstempel Erstellung */
|
||||
createdAt: string;
|
||||
/** Zeitstempel letzte Änderung */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Risiko-Flag
|
||||
*/
|
||||
export interface RiskFlag {
|
||||
/** Schweregrad */
|
||||
severity: string;
|
||||
/** Kategorie */
|
||||
category: string;
|
||||
/** Beschreibung */
|
||||
message: string;
|
||||
/** Rechtsgrundlage */
|
||||
legalReference?: string;
|
||||
/** Empfehlung zur Behebung */
|
||||
recommendation: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifizierte Lücke in der Compliance
|
||||
*/
|
||||
export interface ScopeGap {
|
||||
/** Gap-Typ */
|
||||
gapType: string;
|
||||
/** Schweregrad */
|
||||
severity: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
/** Erforderlich für Level */
|
||||
requiredFor: ComplianceDepthLevel;
|
||||
/** Aktueller Zustand */
|
||||
currentState: string;
|
||||
/** Zielzustand */
|
||||
targetState: string;
|
||||
/** Aufwand in Stunden */
|
||||
effort: number;
|
||||
/** Priorität */
|
||||
priority: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nächster empfohlener Schritt
|
||||
*/
|
||||
export interface NextAction {
|
||||
/** Aktionstyp */
|
||||
actionType: 'create_document' | 'establish_process' | 'implement_technical' | 'organizational_change';
|
||||
/** Titel */
|
||||
title: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
/** Priorität */
|
||||
priority: string;
|
||||
/** Geschätzter Aufwand in Stunden */
|
||||
estimatedEffort: number;
|
||||
/** Dokumenttyp (optional) */
|
||||
documentType?: ScopeDocumentType;
|
||||
/** Link zum SDK-Schritt */
|
||||
sdkStepUrl?: string;
|
||||
/** Blocker */
|
||||
blockers: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Begründungsschritt für die Entscheidung
|
||||
*/
|
||||
export interface ScopeReasoning {
|
||||
/** Schritt-Nummer/ID */
|
||||
step: string;
|
||||
/** Kurzbeschreibung */
|
||||
description: string;
|
||||
/** Faktoren */
|
||||
factors: string[];
|
||||
/** Auswirkung */
|
||||
impact: string;
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Document Scope Matrix (Core Documents)
|
||||
*
|
||||
* Anforderungen pro Level fuer Kern-DSGVO-Dokumente:
|
||||
* vvt, lf, tom, av_vertrag, dsi, betroffenenrechte, dsfa,
|
||||
* daten_transfer, datenpannen, einwilligung, vertragsmanagement.
|
||||
*/
|
||||
|
||||
import type { ScopeDocumentType } from './documents'
|
||||
import type { DocumentScopeRequirement } from './documents'
|
||||
|
||||
/**
|
||||
* Scope-Matrix fuer Kern-DSGVO-Dokumente
|
||||
*/
|
||||
export const DOCUMENT_SCOPE_MATRIX_CORE: Partial<Record<ScopeDocumentType, DocumentScopeRequirement>> = {
|
||||
vvt: {
|
||||
L1: {
|
||||
required: true,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Liste aller Verarbeitungstätigkeiten',
|
||||
'Grundlegende Angaben zu Zweck und Rechtsgrundlage',
|
||||
'Kategorien betroffener Personen und Daten',
|
||||
'Einfache Tabellenform ausreichend',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierte Beschreibung der Verarbeitungszwecke',
|
||||
'Empfängerkategorien',
|
||||
'Speicherfristen',
|
||||
'TOM-Referenzen',
|
||||
'Strukturiertes Format',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Vollständige Rechtsgrundlagen mit Begründung',
|
||||
'Detaillierte Datenkategorien',
|
||||
'Verknüpfung mit DSFA wo relevant',
|
||||
'Versionierung und Änderungshistorie',
|
||||
'Freigabeprozess dokumentiert',
|
||||
],
|
||||
estimatedEffort: '8-16 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständige Nachweiskette für alle Angaben',
|
||||
'Integration mit Risikobewertung',
|
||||
'Regelmäßige Review-Zyklen dokumentiert',
|
||||
'Audit-Trail für alle Änderungen',
|
||||
'Compliance-Nachweise für jede Verarbeitung',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden',
|
||||
},
|
||||
},
|
||||
lf: {
|
||||
L1: {
|
||||
required: true,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegende Löschfristen für Hauptdatenkategorien',
|
||||
'Einfache Tabelle oder Liste',
|
||||
'Bezug auf gesetzliche Aufbewahrungsfristen',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierte Löschfristen pro Verarbeitungstätigkeit',
|
||||
'Begründung der Fristen',
|
||||
'Technischer Löschprozess beschrieben',
|
||||
'Verantwortlichkeiten festgelegt',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Ausnahmen und Sonderfälle dokumentiert',
|
||||
'Automatisierte Löschprozesse beschrieben',
|
||||
'Nachweis regelmäßiger Löschungen',
|
||||
'Eskalationsprozess bei Problemen',
|
||||
],
|
||||
estimatedEffort: '6-10 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständiger Audit-Trail aller Löschvorgänge',
|
||||
'Regelmäßige Audits dokumentiert',
|
||||
'Compliance-Nachweise für alle Löschfristen',
|
||||
'Integration mit Backup-Konzept',
|
||||
],
|
||||
estimatedEffort: '10-16 Stunden',
|
||||
},
|
||||
},
|
||||
tom: {
|
||||
L1: {
|
||||
required: true,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegende technische Maßnahmen aufgelistet',
|
||||
'Organisatorische Grundmaßnahmen',
|
||||
'Einfache Checkliste oder Tabelle',
|
||||
],
|
||||
estimatedEffort: '2-3 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierte Beschreibung aller TOM',
|
||||
'Zuordnung zu Art. 32 DSGVO Kategorien',
|
||||
'Verantwortlichkeiten und Umsetzungsstatus',
|
||||
'Einfache Wirksamkeitsbewertung',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Risikobewertung für jede Maßnahme',
|
||||
'Nachweis der Umsetzung',
|
||||
'Regelmäßige Überprüfungszyklen',
|
||||
'Verbesserungsmaßnahmen dokumentiert',
|
||||
'Verknüpfung mit VVT',
|
||||
],
|
||||
estimatedEffort: '8-12 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständige Wirksamkeitsnachweise',
|
||||
'Externe Audits dokumentiert',
|
||||
'Compliance-Matrix zu Standards (ISO 27001, etc.)',
|
||||
'Kontinuierliches Monitoring nachgewiesen',
|
||||
],
|
||||
estimatedEffort: '12-20 Stunden',
|
||||
},
|
||||
},
|
||||
av_vertrag: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Standard-AVV-Vorlage verwenden',
|
||||
'Grundlegende Angaben zu Auftragsverarbeiter',
|
||||
'Wesentliche Pflichten aufgeführt',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden pro Vertrag',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierte Beschreibung der Verarbeitung',
|
||||
'TOM des Auftragsverarbeiters geprüft',
|
||||
'Unterschriebene Verträge vollständig',
|
||||
'Register aller AVV geführt',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden pro Vertrag',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Risikobewertung für jeden Auftragsverarbeiter',
|
||||
'Regelmäßige Überprüfungen dokumentiert',
|
||||
'Sub-Auftragsverarbeiter erfasst',
|
||||
'Audit-Rechte vereinbart und dokumentiert',
|
||||
],
|
||||
estimatedEffort: '4-6 Stunden pro Vertrag',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Regelmäßige Audits durchgeführt und dokumentiert',
|
||||
'Compliance-Nachweise vom Auftragsverarbeiter',
|
||||
'Vollständiges Vertragsmanagement-System',
|
||||
'Eskalations- und Kündigungsprozesse dokumentiert',
|
||||
],
|
||||
estimatedEffort: '6-10 Stunden pro Vertrag',
|
||||
},
|
||||
},
|
||||
dsi: {
|
||||
L1: {
|
||||
required: true,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Datenschutzerklärung auf Website',
|
||||
'Pflichtangaben nach Art. 13/14 DSGVO',
|
||||
'Verständliche Sprache',
|
||||
'Kontaktdaten DSB/Verantwortlicher',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierte Beschreibung aller Verarbeitungen',
|
||||
'Rechtsgrundlagen erklärt',
|
||||
'Informationen zu Betroffenenrechten',
|
||||
'Cookie-/Tracking-Informationen',
|
||||
'Regelmäßige Aktualisierung',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Mehrsprachige Versionen wo erforderlich',
|
||||
'Layered Notices (mehrstufige Informationen)',
|
||||
'Spezifische Informationen für verschiedene Verarbeitungen',
|
||||
'Versionierung und Änderungshistorie',
|
||||
'Consent Management Integration',
|
||||
],
|
||||
estimatedEffort: '8-12 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständige Nachweiskette für alle Informationen',
|
||||
'Audit-Trail für Änderungen',
|
||||
'Compliance mit internationalen Standards',
|
||||
'Regelmäßige rechtliche Reviews dokumentiert',
|
||||
],
|
||||
estimatedEffort: '12-16 Stunden',
|
||||
},
|
||||
},
|
||||
betroffenenrechte: {
|
||||
L1: {
|
||||
required: true,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Prozess für Auskunftsanfragen definiert',
|
||||
'Kontaktmöglichkeit bereitgestellt',
|
||||
'Grundlegende Fristen bekannt',
|
||||
'Einfaches Formular oder E-Mail-Vorlage',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Prozesse für alle Betroffenenrechte (Auskunft, Löschung, Berichtigung, etc.)',
|
||||
'Verantwortlichkeiten festgelegt',
|
||||
'Standardvorlagen für Antworten',
|
||||
'Tracking von Anfragen',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Detaillierte Prozessbeschreibungen',
|
||||
'Eskalationsprozesse bei komplexen Fällen',
|
||||
'Schulung der Mitarbeiter dokumentiert',
|
||||
'Audit-Trail aller Anfragen',
|
||||
'Nachweis der Fristeneinhaltung',
|
||||
],
|
||||
estimatedEffort: '6-10 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständiges Ticket-/Case-Management-System',
|
||||
'Regelmäßige Audits der Prozesse',
|
||||
'Compliance-Kennzahlen und Reporting',
|
||||
'Integration mit allen relevanten Systemen',
|
||||
],
|
||||
estimatedEffort: '10-16 Stunden',
|
||||
},
|
||||
},
|
||||
dsfa: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht erforderlich',
|
||||
detailItems: ['Nur bei Hard Trigger erforderlich'],
|
||||
estimatedEffort: 'N/A',
|
||||
},
|
||||
L2: {
|
||||
required: false,
|
||||
depth: 'Bei Bedarf',
|
||||
detailItems: [
|
||||
'DSFA-Schwellwertanalyse durchführen',
|
||||
'Bei Erforderlichkeit: Basis-DSFA',
|
||||
'Risiken identifiziert und bewertet',
|
||||
'Maßnahmen zur Risikominimierung',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden pro DSFA',
|
||||
},
|
||||
L3: {
|
||||
required: false,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Detaillierte Risikobewertung',
|
||||
'Konsultation der Betroffenen wo sinnvoll',
|
||||
'Dokumentation der Entscheidungsprozesse',
|
||||
'Regelmäßige Überprüfung',
|
||||
],
|
||||
estimatedEffort: '8-16 Stunden pro DSFA',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollständig',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Strukturierter DSFA-Prozess etabliert',
|
||||
'Vorabkonsultation der Aufsichtsbehörde wo erforderlich',
|
||||
'Vollständige Dokumentation aller Schritte',
|
||||
'Integration in Projektmanagement',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden pro DSFA',
|
||||
},
|
||||
},
|
||||
daten_transfer: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Liste aller Drittlandtransfers',
|
||||
'Grundlegende Rechtsgrundlage identifiziert',
|
||||
'Standard-Vertragsklauseln wo nötig',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierte Dokumentation aller Transfers',
|
||||
'Angemessenheitsbeschlüsse oder geeignete Garantien',
|
||||
'Informationen an Betroffene bereitgestellt',
|
||||
'Register geführt',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Transfer Impact Assessment (TIA) durchgeführt',
|
||||
'Zusätzliche Schutzmaßnahmen dokumentiert',
|
||||
'Regelmäßige Überprüfung der Rechtsgrundlagen',
|
||||
'Risikobewertung für jedes Zielland',
|
||||
],
|
||||
estimatedEffort: '6-12 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständige TIA-Dokumentation',
|
||||
'Regelmäßige Reviews dokumentiert',
|
||||
'Rechtliche Expertise nachgewiesen',
|
||||
'Compliance-Nachweise für alle Transfers',
|
||||
],
|
||||
estimatedEffort: '12-20 Stunden',
|
||||
},
|
||||
},
|
||||
datenpannen: {
|
||||
L1: {
|
||||
required: true,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegender Prozess für Datenpannen',
|
||||
'Kontakt zur Aufsichtsbehörde bekannt',
|
||||
'Verantwortlichkeiten grob definiert',
|
||||
'Einfache Checkliste',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Detaillierter Incident-Response-Plan',
|
||||
'Bewertungskriterien für Meldepflicht',
|
||||
'Vorlagen für Meldungen (Behörde & Betroffene)',
|
||||
'Dokumentationspflichten klar definiert',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Incident-Management-System etabliert',
|
||||
'Regelmäßige Übungen durchgeführt',
|
||||
'Eskalationsprozesse dokumentiert',
|
||||
'Post-Incident-Review-Prozess',
|
||||
'Lessons Learned dokumentiert',
|
||||
],
|
||||
estimatedEffort: '6-10 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Vollständiges Breach-Log geführt',
|
||||
'Integration mit IT-Security-Incident-Response',
|
||||
'Regelmäßige Audits des Prozesses',
|
||||
'Compliance-Nachweise für alle Vorfälle',
|
||||
],
|
||||
estimatedEffort: '10-16 Stunden',
|
||||
},
|
||||
},
|
||||
einwilligung: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Einwilligungsformulare DSGVO-konform',
|
||||
'Opt-in statt Opt-out',
|
||||
'Widerrufsmöglichkeit bereitgestellt',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Granulare Einwilligungen',
|
||||
'Nachweisbarkeit der Einwilligung',
|
||||
'Dokumentation des Einwilligungsprozesses',
|
||||
'Regelmäßige Überprüfung',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Consent-Management-System implementiert',
|
||||
'Vollständiger Audit-Trail',
|
||||
'A/B-Testing dokumentiert',
|
||||
'Integration mit allen Datenverarbeitungen',
|
||||
'Regelmäßige Revalidierung',
|
||||
],
|
||||
estimatedEffort: '6-12 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Enterprise Consent Management Platform',
|
||||
'Vollständige Nachweiskette für alle Einwilligungen',
|
||||
'Compliance-Dashboard',
|
||||
'Regelmäßige externe Audits',
|
||||
],
|
||||
estimatedEffort: '12-20 Stunden',
|
||||
},
|
||||
},
|
||||
vertragsmanagement: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Einfaches Register wichtiger Verträge',
|
||||
'Ablage datenschutzrelevanter Verträge',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Vollständiges Vertragsregister',
|
||||
'Datenschutzklauseln in Standardverträgen',
|
||||
'Überprüfungsprozess für neue Verträge',
|
||||
'Ablaufdaten und Kündigungsfristen getrackt',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden Setup',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Vertragsmanagement-System implementiert',
|
||||
'Automatische Erinnerungen für Reviews',
|
||||
'Risikobewertung für Vertragspartner',
|
||||
'Compliance-Checks vor Vertragsabschluss',
|
||||
],
|
||||
estimatedEffort: '6-12 Stunden Setup',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Enterprise Contract Management System',
|
||||
'Vollständiger Audit-Trail',
|
||||
'Integration mit Procurement',
|
||||
'Regelmäßige Compliance-Audits',
|
||||
],
|
||||
estimatedEffort: '12-20 Stunden Setup',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Document Scope Matrix (Extended Documents)
|
||||
*
|
||||
* Anforderungen pro Level fuer erweiterte Dokumente:
|
||||
* schulung, audit_log, risikoanalyse, notfallplan, zertifizierung,
|
||||
* datenschutzmanagement, iace_ce_assessment, widerrufsbelehrung,
|
||||
* preisangaben, fernabsatz_info, streitbeilegung, produktsicherheit,
|
||||
* ai_act_doku.
|
||||
*/
|
||||
|
||||
import type { ScopeDocumentType } from './documents'
|
||||
import type { DocumentScopeRequirement } from './documents'
|
||||
|
||||
/**
|
||||
* Scope-Matrix fuer erweiterte Dokumente
|
||||
*/
|
||||
export const DOCUMENT_SCOPE_MATRIX_EXTENDED: Partial<Record<ScopeDocumentType, DocumentScopeRequirement>> = {
|
||||
schulung: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegende Datenschutz-Awareness',
|
||||
'Informationsblatt für Mitarbeiter',
|
||||
'Kontaktperson benannt',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden Vorbereitung',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Jährliche Datenschutzschulung',
|
||||
'Schulungsunterlagen erstellt',
|
||||
'Teilnahme dokumentiert',
|
||||
'Rollenspezifische Inhalte',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden Vorbereitung',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'E-Learning-Plattform oder strukturiertes Schulungsprogramm',
|
||||
'Wissenstests durchgeführt',
|
||||
'Auffrischungsschulungen',
|
||||
'Spezialschulungen für Schlüsselpersonal',
|
||||
'Schulungsplan erstellt',
|
||||
],
|
||||
estimatedEffort: '8-16 Stunden Vorbereitung',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Umfassendes Schulungsprogramm',
|
||||
'Externe Schulungen wo erforderlich',
|
||||
'Zertifizierungen für Schlüsselpersonal',
|
||||
'Vollständige Dokumentation aller Schulungen',
|
||||
'Wirksamkeitsmessung',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden Vorbereitung',
|
||||
},
|
||||
},
|
||||
audit_log: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegendes Logging aktiviert',
|
||||
'Zugriffsprotokolle für kritische Systeme',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: false,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Strukturiertes Logging-Konzept',
|
||||
'Aufbewahrungsfristen definiert',
|
||||
'Zugriffskontrolle auf Logs',
|
||||
'Regelmäßige Überprüfung',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Zentralisiertes Logging-System',
|
||||
'Automatische Alerts bei Anomalien',
|
||||
'Audit-Trail für alle datenschutzrelevanten Vorgänge',
|
||||
'Compliance-Reporting',
|
||||
],
|
||||
estimatedEffort: '8-16 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Enterprise SIEM-System',
|
||||
'Vollständige Nachvollziehbarkeit aller Zugriffe',
|
||||
'Regelmäßige Log-Audits dokumentiert',
|
||||
'Integration mit Incident Response',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden',
|
||||
},
|
||||
},
|
||||
risikoanalyse: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegende Risikoidentifikation',
|
||||
'Einfache Bewertung nach Eintrittswahrscheinlichkeit und Auswirkung',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: false,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Strukturierte Risikoanalyse',
|
||||
'Risikomatrix erstellt',
|
||||
'Maßnahmen zur Risikominimierung definiert',
|
||||
'Jährliche Überprüfung',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Umfassende Risikoanalyse nach Standard-Framework',
|
||||
'Integration mit VVT und DSFA',
|
||||
'Risikomanagement-Prozess etabliert',
|
||||
'Regelmäßige Reviews',
|
||||
'Risiko-Dashboard',
|
||||
],
|
||||
estimatedEffort: '8-16 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Enterprise Risk Management System',
|
||||
'Vollständige Integration mit ISMS',
|
||||
'Kontinuierliche Risikoüberwachung',
|
||||
'Regelmäßige externe Assessments',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden',
|
||||
},
|
||||
},
|
||||
notfallplan: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegende Notfallkontakte definiert',
|
||||
'Einfacher Backup-Prozess',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: false,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L1-Anforderungen',
|
||||
'Notfall- und Krisenplan erstellt',
|
||||
'Business Continuity Grundlagen',
|
||||
'Backup und Recovery dokumentiert',
|
||||
'Verantwortlichkeiten festgelegt',
|
||||
],
|
||||
estimatedEffort: '3-6 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Detaillierter Business Continuity Plan',
|
||||
'Disaster Recovery Plan',
|
||||
'Regelmäßige Tests durchgeführt',
|
||||
'Eskalationsprozesse dokumentiert',
|
||||
'Externe Kommunikation geplant',
|
||||
],
|
||||
estimatedEffort: '6-12 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'ISO 22301 konformes BCMS',
|
||||
'Regelmäßige Übungen und Audits',
|
||||
'Vollständige Dokumentation',
|
||||
'Integration mit IT-Disaster-Recovery',
|
||||
],
|
||||
estimatedEffort: '12-20 Stunden',
|
||||
},
|
||||
},
|
||||
zertifizierung: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht relevant',
|
||||
detailItems: ['Keine Zertifizierung erforderlich'],
|
||||
estimatedEffort: 'N/A',
|
||||
},
|
||||
L2: {
|
||||
required: false,
|
||||
depth: 'Nicht relevant',
|
||||
detailItems: ['Keine Zertifizierung erforderlich'],
|
||||
estimatedEffort: 'N/A',
|
||||
},
|
||||
L3: {
|
||||
required: false,
|
||||
depth: 'Optional',
|
||||
detailItems: [
|
||||
'Evaluierung möglicher Zertifizierungen',
|
||||
'Gap-Analyse durchgeführt',
|
||||
'Entscheidung für/gegen Zertifizierung dokumentiert',
|
||||
],
|
||||
estimatedEffort: '4-8 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollständig',
|
||||
detailItems: [
|
||||
'Zertifizierungsvorbereitung (ISO 27001, ISO 27701, etc.)',
|
||||
'Gap-Analyse abgeschlossen',
|
||||
'Maßnahmenplan erstellt',
|
||||
'Interne Audits durchgeführt',
|
||||
'Dokumentation audit-ready',
|
||||
'Zertifizierungsstelle ausgewählt',
|
||||
],
|
||||
estimatedEffort: '40-80 Stunden',
|
||||
},
|
||||
},
|
||||
datenschutzmanagement: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht erforderlich',
|
||||
detailItems: ['Kein formales DSMS notwendig'],
|
||||
estimatedEffort: 'N/A',
|
||||
},
|
||||
L2: {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [
|
||||
'Grundlegendes Datenschutzmanagement',
|
||||
'Verantwortlichkeiten definiert',
|
||||
'Regelmäßige Reviews geplant',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Strukturiertes DSMS etabliert',
|
||||
'Datenschutz-Policy erstellt',
|
||||
'Regelmäßige Management-Reviews',
|
||||
'KPIs für Datenschutz definiert',
|
||||
'Verbesserungsprozess etabliert',
|
||||
],
|
||||
estimatedEffort: '8-16 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollständig',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'ISO 27701 oder vergleichbares DSMS',
|
||||
'Integration mit ISMS',
|
||||
'Vollständige Dokumentation aller Prozesse',
|
||||
'Regelmäßige interne und externe Audits',
|
||||
'Kontinuierliche Verbesserung nachgewiesen',
|
||||
],
|
||||
estimatedEffort: '24-40 Stunden',
|
||||
},
|
||||
},
|
||||
iace_ce_assessment: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Minimal',
|
||||
detailItems: [
|
||||
'Regulatorischer Quick-Check fuer SW/FW/KI',
|
||||
'Grundlegende Identifikation relevanter Vorschriften',
|
||||
],
|
||||
estimatedEffort: '2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'CE-Risikobeurteilung fuer SW/FW-Komponenten',
|
||||
'Hazard Log mit S×E×P Bewertung',
|
||||
'CRA-Konformitaetspruefung',
|
||||
'Grundlegende Massnahmendokumentation',
|
||||
],
|
||||
estimatedEffort: '8 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Detailliert',
|
||||
detailItems: [
|
||||
'Alle L2-Anforderungen',
|
||||
'Vollstaendige CE-Akte inkl. KI-Dossier',
|
||||
'AI Act High-Risk Konformitaetsbewertung',
|
||||
'Maschinenverordnung Anhang III Nachweis',
|
||||
'Verifikationsplan mit Akzeptanzkriterien',
|
||||
'Evidence-Management fuer Testnachweise',
|
||||
],
|
||||
estimatedEffort: '16 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Alle L3-Anforderungen',
|
||||
'Zertifizierungsfertige CE-Dokumentation',
|
||||
'Benannte-Stelle-tauglicher Nachweis',
|
||||
'Revisionssichere Audit Trails',
|
||||
'Post-Market Monitoring Plan',
|
||||
'Continuous Compliance Framework',
|
||||
],
|
||||
estimatedEffort: '24 Stunden',
|
||||
},
|
||||
},
|
||||
widerrufsbelehrung: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht relevant',
|
||||
detailItems: ['Nur bei B2C-Fernabsatz erforderlich'],
|
||||
estimatedEffort: '0',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Muster-Widerrufsbelehrung nach EGBGB Anlage 1',
|
||||
'Muster-Widerrufsformular nach EGBGB Anlage 2',
|
||||
'Integration in Bestellprozess',
|
||||
'14-Tage Widerrufsfrist korrekt dargestellt',
|
||||
],
|
||||
estimatedEffort: '2-4 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Erweitert',
|
||||
detailItems: [
|
||||
'Wie L2 + digitale Inhalte (§ 356 Abs. 5 BGB)',
|
||||
'Ausnahmen dokumentiert (§ 312g Abs. 2 BGB)',
|
||||
],
|
||||
estimatedEffort: '4-6 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollstaendig',
|
||||
detailItems: [
|
||||
'Wie L3 + automatisierte Pruefung',
|
||||
'Mehrsprachig bei EU-Verkauf',
|
||||
],
|
||||
estimatedEffort: '6-8 Stunden',
|
||||
},
|
||||
},
|
||||
preisangaben: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht relevant',
|
||||
detailItems: ['Nur bei B2C-Preisauszeichnung erforderlich'],
|
||||
estimatedEffort: '0',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Gesamtpreisangabe inkl. MwSt (§ 1 PAngV)',
|
||||
'Grundpreisangabe bei Mengenware (§ 4 PAngV)',
|
||||
'Versandkosten deutlich angegeben',
|
||||
],
|
||||
estimatedEffort: '2-3 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Erweitert',
|
||||
detailItems: [
|
||||
'Wie L2 + Preishistorie bei Rabattaktionen (Omnibus-RL)',
|
||||
'Streichpreise korrekt dargestellt',
|
||||
],
|
||||
estimatedEffort: '3-5 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollstaendig',
|
||||
detailItems: [
|
||||
'Wie L3 + automatisierte Pruefung',
|
||||
'Mehrwaehrungsunterstuetzung',
|
||||
],
|
||||
estimatedEffort: '5-8 Stunden',
|
||||
},
|
||||
},
|
||||
fernabsatz_info: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht relevant',
|
||||
detailItems: ['Nur bei Fernabsatzvertraegen erforderlich'],
|
||||
estimatedEffort: '0',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Pflichtinformationen nach § 312d BGB i.V.m. Art. 246a EGBGB',
|
||||
'Wesentliche Eigenschaften der Ware/Dienstleistung',
|
||||
'Identitaet und Anschrift des Unternehmers',
|
||||
'Zahlungs-, Liefer- und Leistungsbedingungen',
|
||||
],
|
||||
estimatedEffort: '3-5 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Erweitert',
|
||||
detailItems: [
|
||||
'Wie L2 + Informationen zu digitalen Inhalten/Diensten',
|
||||
'Funktionalitaet und Interoperabilitaet (§ 327 BGB)',
|
||||
],
|
||||
estimatedEffort: '5-8 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollstaendig',
|
||||
detailItems: [
|
||||
'Wie L3 + mehrsprachige Informationspflichten',
|
||||
'Automatisierte Vollstaendigkeitspruefung',
|
||||
],
|
||||
estimatedEffort: '8-12 Stunden',
|
||||
},
|
||||
},
|
||||
streitbeilegung: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Nicht relevant',
|
||||
detailItems: ['Nur bei B2C-Handel erforderlich'],
|
||||
estimatedEffort: '0',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Hinweis auf OS-Plattform der EU-Kommission (Art. 14 ODR-VO)',
|
||||
'Erklaerung zur Teilnahmebereitschaft an Streitbeilegung (§ 36 VSBG)',
|
||||
'Link zur OS-Plattform im Impressum/AGB',
|
||||
],
|
||||
estimatedEffort: '1-2 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Erweitert',
|
||||
detailItems: [
|
||||
'Wie L2 + Benennung zustaendiger Verbraucherschlichtungsstelle',
|
||||
'Prozess fuer Streitbeilegungsanfragen dokumentiert',
|
||||
],
|
||||
estimatedEffort: '2-3 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollstaendig',
|
||||
detailItems: [
|
||||
'Wie L3 + Eskalationsprozess dokumentiert',
|
||||
'Regelmaessige Auswertung von Beschwerden',
|
||||
],
|
||||
estimatedEffort: '3-4 Stunden',
|
||||
},
|
||||
},
|
||||
produktsicherheit: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Minimal',
|
||||
detailItems: ['Grundlegende Produktkennzeichnung pruefen'],
|
||||
estimatedEffort: '1 Stunde',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Produktsicherheitsbewertung nach GPSR (EU 2023/988)',
|
||||
'CE-Kennzeichnung und Konformitaetserklaerung',
|
||||
'Wirtschaftsakteur-Angaben auf Produkt/Verpackung',
|
||||
'Technische Dokumentation fuer Marktaufsicht',
|
||||
],
|
||||
estimatedEffort: '8-12 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Erweitert',
|
||||
detailItems: [
|
||||
'Wie L2 + Risikoanalyse fuer alle Produktvarianten',
|
||||
'Rueckrufplan und Marktbeobachtungspflichten',
|
||||
'Supply-Chain-Dokumentation',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Vollstaendig',
|
||||
detailItems: [
|
||||
'Wie L3 + vollstaendige GPSR-Konformitaetsakte',
|
||||
'Post-Market-Surveillance System',
|
||||
'Audit-Trail fuer alle Sicherheitsbewertungen',
|
||||
],
|
||||
estimatedEffort: '24-40 Stunden',
|
||||
},
|
||||
},
|
||||
ai_act_doku: {
|
||||
L1: {
|
||||
required: false,
|
||||
depth: 'Minimal',
|
||||
detailItems: ['KI-Risikokategorisierung (Art. 6 AI Act)'],
|
||||
estimatedEffort: '2 Stunden',
|
||||
},
|
||||
L2: {
|
||||
required: true,
|
||||
depth: 'Standard',
|
||||
detailItems: [
|
||||
'Technische Dokumentation nach Art. 11 AI Act',
|
||||
'Transparenzpflichten (Art. 52 AI Act)',
|
||||
'Risikomanagement-Grundlagen (Art. 9 AI Act)',
|
||||
'Menschliche Aufsicht dokumentiert (Art. 14 AI Act)',
|
||||
],
|
||||
estimatedEffort: '8-12 Stunden',
|
||||
},
|
||||
L3: {
|
||||
required: true,
|
||||
depth: 'Erweitert',
|
||||
detailItems: [
|
||||
'Wie L2 + Datenqualitaetsmanagement (Art. 10 AI Act)',
|
||||
'Genauigkeits- und Robustheitstests (Art. 15 AI Act)',
|
||||
'Vollstaendige Konformitaetsbewertung fuer Hochrisiko-KI',
|
||||
],
|
||||
estimatedEffort: '16-24 Stunden',
|
||||
},
|
||||
L4: {
|
||||
required: true,
|
||||
depth: 'Audit-Ready',
|
||||
detailItems: [
|
||||
'Wie L3 + Zertifizierungsfertige AI Act Dokumentation',
|
||||
'EU-Datenbank-Registrierung (Art. 60 AI Act)',
|
||||
'Post-Market Monitoring fuer KI-Systeme',
|
||||
'Continuous Compliance Framework fuer KI',
|
||||
],
|
||||
estimatedEffort: '24-40 Stunden',
|
||||
},
|
||||
},
|
||||
};
|
||||
84
admin-compliance/lib/sdk/compliance-scope-types/documents.ts
Normal file
84
admin-compliance/lib/sdk/compliance-scope-types/documents.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Document Types
|
||||
*
|
||||
* Definiert Dokumenttypen und deren Anforderungen pro Compliance-Level.
|
||||
*/
|
||||
|
||||
import type { ComplianceDepthLevel } from './core-levels'
|
||||
|
||||
/**
|
||||
* Alle verfügbaren Dokumenttypen im SDK
|
||||
*/
|
||||
export type ScopeDocumentType =
|
||||
| 'vvt' // Verzeichnis von Verarbeitungstätigkeiten
|
||||
| 'lf' // Löschfristenkonzept
|
||||
| 'tom' // Technische und organisatorische Maßnahmen
|
||||
| 'av_vertrag' // Auftragsverarbeitungsvertrag
|
||||
| 'dsi' // Datenschutz-Informationen (Privacy Policy)
|
||||
| 'betroffenenrechte' // Betroffenenrechte-Prozess
|
||||
| 'dsfa' // Datenschutz-Folgenabschätzung
|
||||
| 'daten_transfer' // Drittlandtransfer-Dokumentation
|
||||
| 'datenpannen' // Datenpannen-Prozess
|
||||
| 'einwilligung' // Einwilligungsmanagement
|
||||
| 'vertragsmanagement' // Vertragsmanagement-Prozess
|
||||
| 'schulung' // Mitarbeiterschulung
|
||||
| 'audit_log' // Audit & Logging Konzept
|
||||
| 'risikoanalyse' // Risikoanalyse
|
||||
| 'notfallplan' // Notfall- & Krisenplan
|
||||
| 'zertifizierung' // Zertifizierungsvorbereitung
|
||||
| 'datenschutzmanagement' // Datenschutzmanagement-System (DSMS)
|
||||
| 'iace_ce_assessment' // CE-Risikobeurteilung SW/FW/KI (IACE)
|
||||
| 'widerrufsbelehrung' // Widerrufsbelehrung (§ 312g BGB)
|
||||
| 'preisangaben' // Preisangaben (PAngV)
|
||||
| 'fernabsatz_info' // Informationspflichten Fernabsatz (§ 312d BGB)
|
||||
| 'streitbeilegung' // Streitbeilegungshinweis (VSBG § 36)
|
||||
| 'produktsicherheit' // Produktsicherheit (GPSR EU 2023/988)
|
||||
| 'ai_act_doku'; // AI Act Technische Dokumentation (Art. 11)
|
||||
|
||||
/**
|
||||
* Erforderliches Dokument mit Detailtiefe
|
||||
*/
|
||||
export interface RequiredDocument {
|
||||
/** Dokumenttyp */
|
||||
documentType: ScopeDocumentType;
|
||||
/** Anzeigename */
|
||||
label: string;
|
||||
/** Pflicht oder empfohlen */
|
||||
requirement: 'mandatory' | 'recommended';
|
||||
/** Priorität */
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
/** Geschätzter Aufwand in Stunden */
|
||||
estimatedEffort: number;
|
||||
/** Von welchen Triggern/Regeln gefordert */
|
||||
triggeredBy: string[];
|
||||
/** Link zum SDK-Schritt */
|
||||
sdkStepUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anforderungen an ein Dokument pro Level
|
||||
*/
|
||||
export interface DocumentDepthRequirement {
|
||||
/** Ist auf diesem Level erforderlich? */
|
||||
required: boolean;
|
||||
/** Tiefenbezeichnung */
|
||||
depth: string;
|
||||
/** Konkrete Anforderungen */
|
||||
detailItems: string[];
|
||||
/** Geschätzter Aufwand */
|
||||
estimatedEffort: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige Scope-Anforderungen für ein Dokument
|
||||
*/
|
||||
export interface DocumentScopeRequirement {
|
||||
/** L1 Anforderungen */
|
||||
L1: DocumentDepthRequirement;
|
||||
/** L2 Anforderungen */
|
||||
L2: DocumentDepthRequirement;
|
||||
/** L3 Anforderungen */
|
||||
L3: DocumentDepthRequirement;
|
||||
/** L4 Anforderungen */
|
||||
L4: DocumentDepthRequirement;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Hard Trigger Types
|
||||
*
|
||||
* Definiert Typen für regelbasierte Mindest-Compliance-Level-Erzwingung.
|
||||
*/
|
||||
|
||||
import type { ComplianceDepthLevel } from './core-levels'
|
||||
|
||||
/**
|
||||
* Bedingungsoperatoren für Hard Trigger
|
||||
*/
|
||||
export type HardTriggerOperator =
|
||||
| 'EQUALS' // Exakte Übereinstimmung
|
||||
| 'CONTAINS' // Enthält (für Arrays/Strings)
|
||||
| 'IN' // Ist in Liste enthalten
|
||||
| 'GREATER_THAN' // Größer als (numerisch)
|
||||
| 'NOT_EQUALS'; // Ungleich
|
||||
|
||||
/**
|
||||
* Hard Trigger Regel - erzwingt Mindest-Compliance-Level
|
||||
*/
|
||||
export interface HardTriggerRule {
|
||||
/** Eindeutige ID der Regel */
|
||||
id: string;
|
||||
/** Kategorie der Regel */
|
||||
category: string;
|
||||
/** Frage-ID, die geprüft wird */
|
||||
questionId: string;
|
||||
/** Bedingungsoperator */
|
||||
condition: HardTriggerOperator;
|
||||
/** Wert, der geprüft wird */
|
||||
conditionValue: unknown;
|
||||
/** Minimal erforderliches Level */
|
||||
minimumLevel: ComplianceDepthLevel;
|
||||
/** DSFA erforderlich? */
|
||||
requiresDSFA: boolean;
|
||||
/** Pflichtdokumente bei Trigger */
|
||||
mandatoryDocuments: string[];
|
||||
/** Rechtsgrundlage */
|
||||
legalReference: string;
|
||||
/** Detaillierte Beschreibung */
|
||||
description: string;
|
||||
/** Kombiniert mit Art. 9 Daten? */
|
||||
combineWithArt9?: boolean;
|
||||
/** Kombiniert mit Minderjährigen-Daten? */
|
||||
combineWithMinors?: boolean;
|
||||
/** Kombiniert mit KI-Nutzung? */
|
||||
combineWithAI?: boolean;
|
||||
/** Kombiniert mit Mitarbeiterüberwachung? */
|
||||
combineWithEmployeeMonitoring?: boolean;
|
||||
/** Kombiniert mit automatisierter Entscheidungsfindung? */
|
||||
combineWithADM?: boolean;
|
||||
/** Regel feuert NICHT wenn diese Bedingung zutrifft */
|
||||
excludeWhen?: { questionId: string; value: string | string[] };
|
||||
/** Regel feuert NUR wenn diese Bedingung zutrifft */
|
||||
requireWhen?: { questionId: string; value: string | string[] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Getriggerter Hard Trigger mit Kontext
|
||||
*/
|
||||
export interface TriggeredHardTrigger {
|
||||
/** Regel-ID */
|
||||
ruleId: string;
|
||||
/** Kategorie */
|
||||
category: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
/** Rechtsgrundlage */
|
||||
legalReference?: string;
|
||||
/** Mindest-Level */
|
||||
minimumLevel: ComplianceDepthLevel;
|
||||
/** DSFA erforderlich? */
|
||||
requiresDSFA: boolean;
|
||||
/** Pflichtdokumente */
|
||||
mandatoryDocuments: string[];
|
||||
}
|
||||
10
admin-compliance/lib/sdk/compliance-scope-types/index.ts
Normal file
10
admin-compliance/lib/sdk/compliance-scope-types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Barrel re-export — split from the monolithic compliance-scope-types.ts
|
||||
export * from './core-levels';
|
||||
export * from './constants';
|
||||
export * from './questions';
|
||||
export * from './hard-triggers';
|
||||
export * from './documents';
|
||||
export * from './decisions';
|
||||
export * from './document-scope-matrix-core';
|
||||
export * from './document-scope-matrix-extended';
|
||||
export * from './state';
|
||||
77
admin-compliance/lib/sdk/compliance-scope-types/questions.ts
Normal file
77
admin-compliance/lib/sdk/compliance-scope-types/questions.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Question & Profiling Types
|
||||
*
|
||||
* Definiert Typen für das Scope-Profiling-Fragebogensystem.
|
||||
*/
|
||||
|
||||
/**
|
||||
* IDs der Fragenblöcke für das Scope-Profiling
|
||||
*/
|
||||
export type ScopeQuestionBlockId =
|
||||
| 'organisation' // Organisation & Reife
|
||||
| 'data' // Daten & Betroffene
|
||||
| 'processing' // Verarbeitung & Zweck
|
||||
| 'tech' // Technik & Hosting
|
||||
| 'processes' // Rechte & Prozesse
|
||||
| 'product' // Produktkontext
|
||||
| 'ai_systems' // KI-Systeme (aus Profil portiert)
|
||||
| 'vvt' // Verarbeitungstaetigkeiten (aus Profil portiert)
|
||||
| 'datenkategorien_detail'; // Datenkategorien pro Abteilung (Block 9)
|
||||
|
||||
/**
|
||||
* Eine einzelne Frage im Scope-Profiling
|
||||
*/
|
||||
export interface ScopeProfilingQuestion {
|
||||
/** Eindeutige ID der Frage */
|
||||
id: string;
|
||||
/** Fragetext */
|
||||
question: string;
|
||||
/** Optional: Hilfetext/Erklärung */
|
||||
helpText?: string;
|
||||
/** Antworttyp */
|
||||
type: 'single' | 'multi' | 'boolean' | 'number' | 'text';
|
||||
/** Antwortoptionen (für single/multi) */
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
/** Ist die Frage erforderlich? */
|
||||
required: boolean;
|
||||
/** Gewichtung für Score-Berechnung */
|
||||
scoreWeights?: {
|
||||
risk?: number; // Einfluss auf Risiko-Score
|
||||
complexity?: number; // Einfluss auf Komplexitäts-Score
|
||||
assurance?: number; // Einfluss auf Assurance-Bedarf
|
||||
};
|
||||
/** Mapping zu Firmenprofil-Feldern */
|
||||
mapsToCompanyProfile?: string;
|
||||
/** Mapping zu VVT-Fragen */
|
||||
mapsToVVTQuestion?: string;
|
||||
/** Mapping zu LF-Fragen */
|
||||
mapsToLFQuestion?: string;
|
||||
/** Mapping zu TOM-Profil */
|
||||
mapsToTOMProfile?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Antwort auf eine Profiling-Frage
|
||||
*/
|
||||
export interface ScopeProfilingAnswer {
|
||||
/** ID der beantworteten Frage */
|
||||
questionId: string;
|
||||
/** Antwortwert (Typ abhängig von Fragentyp) */
|
||||
value: string | string[] | boolean | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Block von zusammengehörigen Fragen
|
||||
*/
|
||||
export interface ScopeQuestionBlock {
|
||||
/** Block-ID */
|
||||
id: ScopeQuestionBlockId;
|
||||
/** Block-Titel */
|
||||
title: string;
|
||||
/** Block-Beschreibung */
|
||||
description: string;
|
||||
/** Reihenfolge des Blocks */
|
||||
order: number;
|
||||
/** Fragen in diesem Block */
|
||||
questions: ScopeProfilingQuestion[];
|
||||
}
|
||||
22
admin-compliance/lib/sdk/compliance-scope-types/state.ts
Normal file
22
admin-compliance/lib/sdk/compliance-scope-types/state.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Compliance Scope Engine - State Management Types
|
||||
*
|
||||
* Definiert den Gesamtzustand des Compliance Scope.
|
||||
*/
|
||||
|
||||
import type { ScopeProfilingAnswer } from './questions'
|
||||
import type { ScopeDecision } from './decisions'
|
||||
|
||||
/**
|
||||
* Gesamter Zustand des Compliance Scope
|
||||
*/
|
||||
export interface ComplianceScopeState {
|
||||
/** Alle gegebenen Antworten */
|
||||
answers: ScopeProfilingAnswer[];
|
||||
/** Aktuelle Entscheidung (null wenn noch nicht berechnet) */
|
||||
decision: ScopeDecision | null;
|
||||
/** Zeitpunkt der letzten Evaluierung */
|
||||
lastEvaluatedAt: string | null;
|
||||
/** Sind alle Pflichtfragen beantwortet? */
|
||||
isComplete: boolean;
|
||||
}
|
||||
17
admin-compliance/lib/sdk/context-hooks.ts
Normal file
17
admin-compliance/lib/sdk/context-hooks.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useContext } from 'react'
|
||||
import { SDKContextValue } from './context-types'
|
||||
import { SDKContext } from './context-provider'
|
||||
|
||||
// =============================================================================
|
||||
// HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useSDK(): SDKContextValue {
|
||||
const context = useContext(SDKContext)
|
||||
if (!context) {
|
||||
throw new Error('useSDK must be used within SDKProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
67
admin-compliance/lib/sdk/context-projects.ts
Normal file
67
admin-compliance/lib/sdk/context-projects.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import { SDKApiClient, getSDKApiClient } from './api-client'
|
||||
import { CustomerType, ProjectInfo } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// PROJECT MANAGEMENT HELPERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ensures an API client is available. If the ref is null and backend sync is
|
||||
* enabled, lazily initialises one. Returns the client or throws.
|
||||
*/
|
||||
export function ensureApiClient(
|
||||
apiClientRef: React.MutableRefObject<SDKApiClient | null>,
|
||||
enableBackendSync: boolean,
|
||||
tenantId: string,
|
||||
projectId?: string
|
||||
): SDKApiClient {
|
||||
if (!apiClientRef.current && enableBackendSync) {
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
}
|
||||
if (!apiClientRef.current) {
|
||||
throw new Error('Backend sync not enabled')
|
||||
}
|
||||
return apiClientRef.current
|
||||
}
|
||||
|
||||
export async function createProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
name: string,
|
||||
customerType: CustomerType,
|
||||
copyFromProjectId?: string
|
||||
): Promise<ProjectInfo> {
|
||||
return apiClient.createProject({
|
||||
name,
|
||||
customer_type: customerType,
|
||||
copy_from_project_id: copyFromProjectId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function listProjectsApi(
|
||||
apiClient: SDKApiClient
|
||||
): Promise<ProjectInfo[]> {
|
||||
const result = await apiClient.listProjects()
|
||||
return result.projects
|
||||
}
|
||||
|
||||
export async function archiveProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
archiveId: string
|
||||
): Promise<void> {
|
||||
await apiClient.archiveProject(archiveId)
|
||||
}
|
||||
|
||||
export async function restoreProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
restoreId: string
|
||||
): Promise<ProjectInfo> {
|
||||
return apiClient.restoreProject(restoreId)
|
||||
}
|
||||
|
||||
export async function permanentlyDeleteProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
deleteId: string
|
||||
): Promise<void> {
|
||||
await apiClient.permanentlyDeleteProject(deleteId)
|
||||
}
|
||||
495
admin-compliance/lib/sdk/context-provider.tsx
Normal file
495
admin-compliance/lib/sdk/context-provider.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import {
|
||||
SDKState,
|
||||
CheckpointStatus,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
CustomerType,
|
||||
CompanyProfile,
|
||||
ImportedDocument,
|
||||
GapAnalysis,
|
||||
SDKPackageId,
|
||||
ProjectInfo,
|
||||
getStepById,
|
||||
getStepByUrl,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
getCompletionPercentage,
|
||||
getPhaseCompletionPercentage,
|
||||
getPackageCompletionPercentage,
|
||||
} from './types'
|
||||
import { exportToPDF, exportToZIP } from './export'
|
||||
import { SDKApiClient, getSDKApiClient } from './api-client'
|
||||
import { StateSyncManager, SyncState } from './sync'
|
||||
import { generateDemoState } from './demo-data'
|
||||
import { SDKContextValue, initialState, SDK_STORAGE_KEY } from './context-types'
|
||||
import { sdkReducer } from './context-reducer'
|
||||
import { validateCheckpointLocally } from './context-validators'
|
||||
import {
|
||||
ensureApiClient,
|
||||
createProjectApi,
|
||||
listProjectsApi,
|
||||
archiveProjectApi,
|
||||
restoreProjectApi,
|
||||
permanentlyDeleteProjectApi,
|
||||
} from './context-projects'
|
||||
import {
|
||||
buildSyncCallbacks,
|
||||
loadInitialState,
|
||||
initSyncInfra,
|
||||
cleanupSyncInfra,
|
||||
} from './context-sync-helpers'
|
||||
|
||||
export const SDKContext = createContext<SDKContextValue | null>(null)
|
||||
|
||||
interface SDKProviderProps {
|
||||
children: React.ReactNode
|
||||
tenantId?: string
|
||||
userId?: string
|
||||
projectId?: string
|
||||
enableBackendSync?: boolean
|
||||
}
|
||||
|
||||
export function SDKProvider({
|
||||
children,
|
||||
tenantId = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
|
||||
userId = 'default',
|
||||
projectId,
|
||||
enableBackendSync = false,
|
||||
}: SDKProviderProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [state, dispatch] = useReducer(sdkReducer, {
|
||||
...initialState,
|
||||
tenantId,
|
||||
userId,
|
||||
projectId: projectId || '',
|
||||
})
|
||||
const [isCommandBarOpen, setCommandBarOpen] = React.useState(false)
|
||||
const [isInitialized, setIsInitialized] = React.useState(false)
|
||||
const [syncState, setSyncState] = React.useState<SyncState>({
|
||||
status: 'idle',
|
||||
lastSyncedAt: null,
|
||||
localVersion: 0,
|
||||
serverVersion: 0,
|
||||
pendingChanges: 0,
|
||||
error: null,
|
||||
})
|
||||
const [isOnline, setIsOnline] = React.useState(true)
|
||||
|
||||
// Refs for sync manager and API client
|
||||
const apiClientRef = useRef<SDKApiClient | null>(null)
|
||||
const syncManagerRef = useRef<StateSyncManager | null>(null)
|
||||
const stateRef = useRef(state)
|
||||
stateRef.current = state
|
||||
|
||||
// Initialize API client and sync manager
|
||||
useEffect(() => {
|
||||
const callbacks = buildSyncCallbacks(setSyncState, setIsOnline, dispatch, stateRef)
|
||||
initSyncInfra(enableBackendSync, tenantId, projectId, apiClientRef, syncManagerRef, callbacks)
|
||||
return () => cleanupSyncInfra(enableBackendSync, syncManagerRef, apiClientRef)
|
||||
}, [enableBackendSync, tenantId, projectId])
|
||||
|
||||
// Sync current step with URL
|
||||
useEffect(() => {
|
||||
if (pathname) {
|
||||
const step = getStepByUrl(pathname)
|
||||
if (step && step.id !== state.currentStep) {
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: step.id })
|
||||
}
|
||||
}
|
||||
}, [pathname, state.currentStep])
|
||||
|
||||
// Storage key — per tenant+project
|
||||
const storageKey = projectId
|
||||
? `${SDK_STORAGE_KEY}-${tenantId}-${projectId}`
|
||||
: `${SDK_STORAGE_KEY}-${tenantId}`
|
||||
|
||||
// Load state on mount (localStorage first, then server)
|
||||
useEffect(() => {
|
||||
loadInitialState({
|
||||
storageKey,
|
||||
enableBackendSync,
|
||||
projectId,
|
||||
syncManager: syncManagerRef.current,
|
||||
apiClient: apiClientRef.current,
|
||||
dispatch,
|
||||
})
|
||||
.catch(error => console.error('Failed to load SDK state:', error))
|
||||
.finally(() => setIsInitialized(true))
|
||||
}, [tenantId, projectId, enableBackendSync, storageKey])
|
||||
|
||||
// Auto-save to localStorage and sync to server
|
||||
useEffect(() => {
|
||||
if (!isInitialized || !state.preferences.autoSave) return
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
try {
|
||||
// Save to localStorage (per tenant+project)
|
||||
localStorage.setItem(storageKey, JSON.stringify(state))
|
||||
|
||||
// Sync to server if backend sync is enabled
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
syncManagerRef.current.queueSync(state)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save SDK state:', error)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(saveTimeout)
|
||||
}, [state, tenantId, projectId, isInitialized, enableBackendSync, storageKey])
|
||||
|
||||
// Keyboard shortcut for Command Bar
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setCommandBarOpen(prev => !prev)
|
||||
}
|
||||
if (e.key === 'Escape' && isCommandBarOpen) {
|
||||
setCommandBarOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isCommandBarOpen])
|
||||
|
||||
// Navigation
|
||||
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
|
||||
|
||||
const goToStep = useCallback(
|
||||
(stepId: string) => {
|
||||
const step = getStepById(stepId)
|
||||
if (step) {
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
||||
const url = projectId ? `${step.url}?project=${projectId}` : step.url
|
||||
router.push(url)
|
||||
}
|
||||
},
|
||||
[router, projectId]
|
||||
)
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
const nextStep = getNextStep(state.currentStep, state)
|
||||
if (nextStep) {
|
||||
goToStep(nextStep.id)
|
||||
}
|
||||
}, [state, goToStep])
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
const prevStep = getPreviousStep(state.currentStep, state)
|
||||
if (prevStep) {
|
||||
goToStep(prevStep.id)
|
||||
}
|
||||
}, [state, goToStep])
|
||||
|
||||
const canGoNext = useMemo(() => {
|
||||
return getNextStep(state.currentStep, state) !== undefined
|
||||
}, [state])
|
||||
|
||||
const canGoPrevious = useMemo(() => {
|
||||
return getPreviousStep(state.currentStep, state) !== undefined
|
||||
}, [state])
|
||||
|
||||
// Progress
|
||||
const completionPercentage = useMemo(() => getCompletionPercentage(state), [state])
|
||||
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
|
||||
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
|
||||
|
||||
// Package Completion
|
||||
const packageCompletion = useMemo(() => {
|
||||
const completion: Record<SDKPackageId, number> = {
|
||||
'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'),
|
||||
'analyse': getPackageCompletionPercentage(state, 'analyse'),
|
||||
'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'),
|
||||
'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'),
|
||||
'betrieb': getPackageCompletionPercentage(state, 'betrieb'),
|
||||
}
|
||||
return completion
|
||||
}, [state])
|
||||
|
||||
// Simple dispatch callbacks
|
||||
const setCustomerType = useCallback((type: CustomerType) => dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type }), [])
|
||||
const setCompanyProfile = useCallback((profile: CompanyProfile) => dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile }), [])
|
||||
const updateCompanyProfile = useCallback((updates: Partial<CompanyProfile>) => dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates }), [])
|
||||
const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope }), [])
|
||||
const updateComplianceScope = useCallback((updates: Partial<import('./compliance-scope-types').ComplianceScopeState>) => dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates }), [])
|
||||
const addImportedDocument = useCallback((doc: ImportedDocument) => dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc }), [])
|
||||
const setGapAnalysis = useCallback((analysis: GapAnalysis) => dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis }), [])
|
||||
|
||||
// Checkpoints
|
||||
const validateCheckpoint = useCallback(
|
||||
async (checkpointId: string): Promise<CheckpointStatus> => {
|
||||
// Try backend validation if available
|
||||
if (enableBackendSync && apiClientRef.current) {
|
||||
try {
|
||||
const result = await apiClientRef.current.validateCheckpoint(checkpointId, state)
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId: result.checkpointId,
|
||||
passed: result.passed,
|
||||
validatedAt: new Date(result.validatedAt),
|
||||
validatedBy: result.validatedBy,
|
||||
errors: result.errors,
|
||||
warnings: result.warnings,
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
} catch {
|
||||
// Fall back to local validation
|
||||
}
|
||||
}
|
||||
|
||||
// Local validation
|
||||
const status = validateCheckpointLocally(checkpointId, state)
|
||||
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
},
|
||||
[state, enableBackendSync]
|
||||
)
|
||||
|
||||
const overrideCheckpoint = useCallback(async (checkpointId: string, reason: string): Promise<void> => {
|
||||
const existing = state.checkpoints[checkpointId]
|
||||
const overridden: CheckpointStatus = {
|
||||
...existing, checkpointId, passed: true, overrideReason: reason,
|
||||
overriddenBy: state.userId, overriddenAt: new Date(),
|
||||
errors: [], warnings: existing?.warnings || [],
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overridden } })
|
||||
}, [state.checkpoints, state.userId])
|
||||
|
||||
const getCheckpointStatus = useCallback(
|
||||
(checkpointId: string) => state.checkpoints[checkpointId],
|
||||
[state.checkpoints]
|
||||
)
|
||||
|
||||
const updateUseCase = useCallback((id: string, data: Partial<UseCaseAssessment>) => dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } }), [])
|
||||
const addRisk = useCallback((risk: Risk) => dispatch({ type: 'ADD_RISK', payload: risk }), [])
|
||||
const updateControl = useCallback((id: string, data: Partial<Control>) => dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } }), [])
|
||||
const loadDemoData = useCallback((demoState: Partial<SDKState>) => dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState }), [])
|
||||
|
||||
// Seed demo data via API (stores like real customer data)
|
||||
const seedDemoData = useCallback(async (): Promise<{ success: boolean; message: string }> => {
|
||||
try {
|
||||
// Generate demo state
|
||||
const demoState = generateDemoState(tenantId, userId) as SDKState
|
||||
|
||||
// Save via API (same path as real customer data)
|
||||
if (enableBackendSync && apiClientRef.current) {
|
||||
await apiClientRef.current.saveState(demoState)
|
||||
}
|
||||
|
||||
// Also save to localStorage for immediate availability
|
||||
localStorage.setItem(storageKey, JSON.stringify(demoState))
|
||||
|
||||
// Update local state
|
||||
dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState })
|
||||
|
||||
return { success: true, message: `Demo-Daten erfolgreich geladen für Tenant ${tenantId}` }
|
||||
} catch (error) {
|
||||
console.error('Failed to seed demo data:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten',
|
||||
}
|
||||
}
|
||||
}, [tenantId, userId, enableBackendSync, storageKey])
|
||||
|
||||
// Clear demo data
|
||||
const clearDemoData = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
// Delete from API
|
||||
if (enableBackendSync && apiClientRef.current) {
|
||||
await apiClientRef.current.deleteState()
|
||||
}
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem(storageKey)
|
||||
|
||||
// Reset local state
|
||||
dispatch({ type: 'RESET_STATE' })
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to clear demo data:', error)
|
||||
return false
|
||||
}
|
||||
}, [storageKey, enableBackendSync])
|
||||
|
||||
// Check if demo data is loaded (has use cases with demo- prefix)
|
||||
const isDemoDataLoaded = useMemo(() => {
|
||||
return state.useCases.length > 0 && state.useCases.some(uc => uc.id.startsWith('demo-'))
|
||||
}, [state.useCases])
|
||||
|
||||
// Persistence
|
||||
const saveState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(state))
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forcSync(state)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save SDK state:', error)
|
||||
throw error
|
||||
}
|
||||
}, [state, storageKey, enableBackendSync])
|
||||
|
||||
const loadState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to localStorage
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
dispatch({ type: 'SET_STATE', payload: parsed })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load SDK state:', error)
|
||||
throw error
|
||||
}
|
||||
}, [storageKey, enableBackendSync])
|
||||
|
||||
// Force sync to server
|
||||
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forcSync(state)
|
||||
}
|
||||
}, [state, enableBackendSync])
|
||||
|
||||
// Project Management
|
||||
const createProject = useCallback(
|
||||
async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise<ProjectInfo> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
return createProjectApi(client, name, customerType, copyFromProjectId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
const listProjectsFn = useCallback(async (): Promise<ProjectInfo[]> => {
|
||||
if (!apiClientRef.current && enableBackendSync) {
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
}
|
||||
if (!apiClientRef.current) {
|
||||
return []
|
||||
}
|
||||
return listProjectsApi(apiClientRef.current)
|
||||
}, [enableBackendSync, tenantId, projectId])
|
||||
|
||||
const switchProject = useCallback(
|
||||
(newProjectId: string) => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('project', newProjectId)
|
||||
router.push(`/sdk?${params.toString()}`)
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
const archiveProjectFn = useCallback(
|
||||
async (archiveId: string): Promise<void> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
await archiveProjectApi(client, archiveId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
const restoreProjectFn = useCallback(
|
||||
async (restoreId: string): Promise<ProjectInfo> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
return restoreProjectApi(client, restoreId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
const permanentlyDeleteProjectFn = useCallback(
|
||||
async (deleteId: string): Promise<void> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
await permanentlyDeleteProjectApi(client, deleteId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
// Export
|
||||
const exportState = useCallback(
|
||||
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return new Blob([JSON.stringify(state, null, 2)], {
|
||||
type: 'application/json',
|
||||
})
|
||||
|
||||
case 'pdf':
|
||||
return exportToPDF(state)
|
||||
|
||||
case 'zip':
|
||||
return exportToZIP(state)
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown export format: ${format}`)
|
||||
}
|
||||
},
|
||||
[state]
|
||||
)
|
||||
|
||||
const value: SDKContextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
packageCompletion,
|
||||
setCustomerType,
|
||||
setCompanyProfile,
|
||||
updateCompanyProfile,
|
||||
setComplianceScope,
|
||||
updateComplianceScope,
|
||||
addImportedDocument,
|
||||
setGapAnalysis,
|
||||
validateCheckpoint,
|
||||
overrideCheckpoint,
|
||||
getCheckpointStatus,
|
||||
updateUseCase,
|
||||
addRisk,
|
||||
updateControl,
|
||||
saveState,
|
||||
loadState,
|
||||
loadDemoData,
|
||||
seedDemoData,
|
||||
clearDemoData,
|
||||
isDemoDataLoaded,
|
||||
syncState,
|
||||
forceSyncToServer,
|
||||
isOnline,
|
||||
exportState,
|
||||
isCommandBarOpen,
|
||||
setCommandBarOpen,
|
||||
projectId,
|
||||
createProject,
|
||||
listProjects: listProjectsFn,
|
||||
switchProject,
|
||||
archiveProject: archiveProjectFn,
|
||||
restoreProject: restoreProjectFn,
|
||||
permanentlyDeleteProject: permanentlyDeleteProjectFn,
|
||||
}
|
||||
|
||||
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>
|
||||
}
|
||||
353
admin-compliance/lib/sdk/context-reducer.ts
Normal file
353
admin-compliance/lib/sdk/context-reducer.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
SDKState,
|
||||
getStepById,
|
||||
} from './types'
|
||||
import { ExtendedSDKAction, initialState } from './context-types'
|
||||
|
||||
// =============================================================================
|
||||
// REDUCER
|
||||
// =============================================================================
|
||||
|
||||
export function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState {
|
||||
const updateState = (updates: Partial<SDKState>): SDKState => ({
|
||||
...state,
|
||||
...updates,
|
||||
lastModified: new Date(),
|
||||
})
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_STATE':
|
||||
return updateState(action.payload)
|
||||
|
||||
case 'LOAD_DEMO_DATA':
|
||||
// Load demo data while preserving user preferences
|
||||
return {
|
||||
...initialState,
|
||||
...action.payload,
|
||||
tenantId: state.tenantId,
|
||||
userId: state.userId,
|
||||
preferences: state.preferences,
|
||||
lastModified: new Date(),
|
||||
}
|
||||
|
||||
case 'SET_CURRENT_STEP': {
|
||||
const step = getStepById(action.payload)
|
||||
return updateState({
|
||||
currentStep: action.payload,
|
||||
currentPhase: step?.phase || state.currentPhase,
|
||||
})
|
||||
}
|
||||
|
||||
case 'COMPLETE_STEP':
|
||||
if (state.completedSteps.includes(action.payload)) {
|
||||
return state
|
||||
}
|
||||
return updateState({
|
||||
completedSteps: [...state.completedSteps, action.payload],
|
||||
})
|
||||
|
||||
case 'SET_CHECKPOINT_STATUS':
|
||||
return updateState({
|
||||
checkpoints: {
|
||||
...state.checkpoints,
|
||||
[action.payload.id]: action.payload.status,
|
||||
},
|
||||
})
|
||||
|
||||
case 'SET_CUSTOMER_TYPE':
|
||||
return updateState({ customerType: action.payload })
|
||||
|
||||
case 'SET_COMPANY_PROFILE':
|
||||
return updateState({ companyProfile: action.payload })
|
||||
|
||||
case 'UPDATE_COMPANY_PROFILE':
|
||||
return updateState({
|
||||
companyProfile: state.companyProfile
|
||||
? { ...state.companyProfile, ...action.payload }
|
||||
: null,
|
||||
})
|
||||
|
||||
case 'SET_COMPLIANCE_SCOPE':
|
||||
return updateState({ complianceScope: action.payload })
|
||||
|
||||
case 'UPDATE_COMPLIANCE_SCOPE':
|
||||
return updateState({
|
||||
complianceScope: state.complianceScope
|
||||
? { ...state.complianceScope, ...action.payload }
|
||||
: null,
|
||||
})
|
||||
|
||||
case 'ADD_IMPORTED_DOCUMENT':
|
||||
return updateState({
|
||||
importedDocuments: [...state.importedDocuments, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_IMPORTED_DOCUMENT':
|
||||
return updateState({
|
||||
importedDocuments: state.importedDocuments.map(doc =>
|
||||
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
|
||||
),
|
||||
})
|
||||
|
||||
case 'DELETE_IMPORTED_DOCUMENT':
|
||||
return updateState({
|
||||
importedDocuments: state.importedDocuments.filter(doc => doc.id !== action.payload),
|
||||
})
|
||||
|
||||
case 'SET_GAP_ANALYSIS':
|
||||
return updateState({ gapAnalysis: action.payload })
|
||||
|
||||
case 'ADD_USE_CASE':
|
||||
return updateState({
|
||||
useCases: [...state.useCases, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_USE_CASE':
|
||||
return updateState({
|
||||
useCases: state.useCases.map(uc =>
|
||||
uc.id === action.payload.id ? { ...uc, ...action.payload.data } : uc
|
||||
),
|
||||
})
|
||||
|
||||
case 'DELETE_USE_CASE':
|
||||
return updateState({
|
||||
useCases: state.useCases.filter(uc => uc.id !== action.payload),
|
||||
activeUseCase: state.activeUseCase === action.payload ? null : state.activeUseCase,
|
||||
})
|
||||
|
||||
case 'SET_ACTIVE_USE_CASE':
|
||||
return updateState({ activeUseCase: action.payload })
|
||||
|
||||
case 'SET_SCREENING':
|
||||
return updateState({ screening: action.payload })
|
||||
|
||||
case 'ADD_MODULE':
|
||||
return updateState({
|
||||
modules: [...state.modules, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_MODULE':
|
||||
return updateState({
|
||||
modules: state.modules.map(m =>
|
||||
m.id === action.payload.id ? { ...m, ...action.payload.data } : m
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_REQUIREMENT':
|
||||
return updateState({
|
||||
requirements: [...state.requirements, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_REQUIREMENT':
|
||||
return updateState({
|
||||
requirements: state.requirements.map(r =>
|
||||
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_CONTROL':
|
||||
return updateState({
|
||||
controls: [...state.controls, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_CONTROL':
|
||||
return updateState({
|
||||
controls: state.controls.map(c =>
|
||||
c.id === action.payload.id ? { ...c, ...action.payload.data } : c
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_EVIDENCE':
|
||||
return updateState({
|
||||
evidence: [...state.evidence, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_EVIDENCE':
|
||||
return updateState({
|
||||
evidence: state.evidence.map(e =>
|
||||
e.id === action.payload.id ? { ...e, ...action.payload.data } : e
|
||||
),
|
||||
})
|
||||
|
||||
case 'DELETE_EVIDENCE':
|
||||
return updateState({
|
||||
evidence: state.evidence.filter(e => e.id !== action.payload),
|
||||
})
|
||||
|
||||
case 'ADD_RISK':
|
||||
return updateState({
|
||||
risks: [...state.risks, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_RISK':
|
||||
return updateState({
|
||||
risks: state.risks.map(r =>
|
||||
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
|
||||
),
|
||||
})
|
||||
|
||||
case 'DELETE_RISK':
|
||||
return updateState({
|
||||
risks: state.risks.filter(r => r.id !== action.payload),
|
||||
})
|
||||
|
||||
case 'SET_AI_ACT_RESULT':
|
||||
return updateState({ aiActClassification: action.payload })
|
||||
|
||||
case 'ADD_OBLIGATION':
|
||||
return updateState({
|
||||
obligations: [...state.obligations, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_OBLIGATION':
|
||||
return updateState({
|
||||
obligations: state.obligations.map(o =>
|
||||
o.id === action.payload.id ? { ...o, ...action.payload.data } : o
|
||||
),
|
||||
})
|
||||
|
||||
case 'SET_DSFA':
|
||||
return updateState({ dsfa: action.payload })
|
||||
|
||||
case 'ADD_TOM':
|
||||
return updateState({
|
||||
toms: [...state.toms, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_TOM':
|
||||
return updateState({
|
||||
toms: state.toms.map(t =>
|
||||
t.id === action.payload.id ? { ...t, ...action.payload.data } : t
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_RETENTION_POLICY':
|
||||
return updateState({
|
||||
retentionPolicies: [...state.retentionPolicies, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_RETENTION_POLICY':
|
||||
return updateState({
|
||||
retentionPolicies: state.retentionPolicies.map(p =>
|
||||
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_PROCESSING_ACTIVITY':
|
||||
return updateState({
|
||||
vvt: [...state.vvt, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_PROCESSING_ACTIVITY':
|
||||
return updateState({
|
||||
vvt: state.vvt.map(p =>
|
||||
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_DOCUMENT':
|
||||
return updateState({
|
||||
documents: [...state.documents, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_DOCUMENT':
|
||||
return updateState({
|
||||
documents: state.documents.map(d =>
|
||||
d.id === action.payload.id ? { ...d, ...action.payload.data } : d
|
||||
),
|
||||
})
|
||||
|
||||
case 'SET_COOKIE_BANNER':
|
||||
return updateState({ cookieBanner: action.payload })
|
||||
|
||||
case 'SET_DSR_CONFIG':
|
||||
return updateState({ dsrConfig: action.payload })
|
||||
|
||||
case 'ADD_ESCALATION_WORKFLOW':
|
||||
return updateState({
|
||||
escalationWorkflows: [...state.escalationWorkflows, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_ESCALATION_WORKFLOW':
|
||||
return updateState({
|
||||
escalationWorkflows: state.escalationWorkflows.map(w =>
|
||||
w.id === action.payload.id ? { ...w, ...action.payload.data } : w
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_SECURITY_ISSUE':
|
||||
return updateState({
|
||||
securityIssues: [...state.securityIssues, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_SECURITY_ISSUE':
|
||||
return updateState({
|
||||
securityIssues: state.securityIssues.map(i =>
|
||||
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_BACKLOG_ITEM':
|
||||
return updateState({
|
||||
securityBacklog: [...state.securityBacklog, action.payload],
|
||||
})
|
||||
|
||||
case 'UPDATE_BACKLOG_ITEM':
|
||||
return updateState({
|
||||
securityBacklog: state.securityBacklog.map(i =>
|
||||
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
|
||||
),
|
||||
})
|
||||
|
||||
case 'ADD_COMMAND_HISTORY':
|
||||
return updateState({
|
||||
commandBarHistory: [action.payload, ...state.commandBarHistory].slice(0, 50),
|
||||
})
|
||||
|
||||
case 'SET_PREFERENCES':
|
||||
return updateState({
|
||||
preferences: { ...state.preferences, ...action.payload },
|
||||
})
|
||||
|
||||
case 'ADD_CUSTOM_CATALOG_ENTRY': {
|
||||
const entry = action.payload
|
||||
const existing = state.customCatalogs[entry.catalogId] || []
|
||||
return updateState({
|
||||
customCatalogs: {
|
||||
...state.customCatalogs,
|
||||
[entry.catalogId]: [...existing, entry],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'UPDATE_CUSTOM_CATALOG_ENTRY': {
|
||||
const { catalogId, entryId, data } = action.payload
|
||||
const entries = state.customCatalogs[catalogId] || []
|
||||
return updateState({
|
||||
customCatalogs: {
|
||||
...state.customCatalogs,
|
||||
[catalogId]: entries.map(e =>
|
||||
e.id === entryId ? { ...e, data: { ...e.data, ...data }, updatedAt: new Date().toISOString() } : e
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'DELETE_CUSTOM_CATALOG_ENTRY': {
|
||||
const { catalogId, entryId } = action.payload
|
||||
const items = state.customCatalogs[catalogId] || []
|
||||
return updateState({
|
||||
customCatalogs: {
|
||||
...state.customCatalogs,
|
||||
[catalogId]: items.filter(e => e.id !== entryId),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'RESET_STATE':
|
||||
return { ...initialState, lastModified: new Date() }
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
145
admin-compliance/lib/sdk/context-sync-helpers.ts
Normal file
145
admin-compliance/lib/sdk/context-sync-helpers.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import React from 'react'
|
||||
import { SDKState } from './types'
|
||||
import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client'
|
||||
import { StateSyncManager, createStateSyncManager, SyncState, SyncCallbacks } from './sync'
|
||||
import { ExtendedSDKAction } from './context-types'
|
||||
|
||||
// =============================================================================
|
||||
// SYNC CALLBACK BUILDER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Builds the SyncCallbacks object used by the StateSyncManager.
|
||||
* Keeps the provider component cleaner by extracting this factory.
|
||||
*/
|
||||
export function buildSyncCallbacks(
|
||||
setSyncState: React.Dispatch<React.SetStateAction<SyncState>>,
|
||||
setIsOnline: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
dispatch: React.Dispatch<ExtendedSDKAction>,
|
||||
stateRef: React.MutableRefObject<SDKState>
|
||||
): SyncCallbacks {
|
||||
return {
|
||||
onSyncStart: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
||||
},
|
||||
onSyncComplete: (syncedState) => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'idle',
|
||||
lastSyncedAt: new Date(),
|
||||
pendingChanges: 0,
|
||||
}))
|
||||
if (syncedState.lastModified > stateRef.current.lastModified) {
|
||||
dispatch({ type: 'SET_STATE', payload: syncedState })
|
||||
}
|
||||
},
|
||||
onSyncError: (error) => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
}))
|
||||
},
|
||||
onConflict: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'conflict' }))
|
||||
},
|
||||
onOffline: () => {
|
||||
setIsOnline(false)
|
||||
setSyncState(prev => ({ ...prev, status: 'offline' }))
|
||||
},
|
||||
onOnline: () => {
|
||||
setIsOnline(true)
|
||||
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE LOADER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Loads SDK state from localStorage and optionally from the server,
|
||||
* dispatching SET_STATE as appropriate.
|
||||
*/
|
||||
export async function loadInitialState(params: {
|
||||
storageKey: string
|
||||
enableBackendSync: boolean
|
||||
projectId?: string
|
||||
syncManager: StateSyncManager | null
|
||||
apiClient: SDKApiClient | null
|
||||
dispatch: React.Dispatch<ExtendedSDKAction>
|
||||
}): Promise<void> {
|
||||
const { storageKey, enableBackendSync, projectId, syncManager, apiClient, dispatch } = params
|
||||
|
||||
// First, try loading from localStorage
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
if (parsed.lastModified) {
|
||||
parsed.lastModified = new Date(parsed.lastModified)
|
||||
}
|
||||
dispatch({ type: 'SET_STATE', payload: parsed })
|
||||
}
|
||||
|
||||
// Then, try loading from server if backend sync is enabled
|
||||
if (enableBackendSync && syncManager) {
|
||||
const serverState = await syncManager.loadFromServer()
|
||||
if (serverState) {
|
||||
const localTime = stored ? new Date(JSON.parse(stored).lastModified).getTime() : 0
|
||||
const serverTime = new Date(serverState.lastModified).getTime()
|
||||
if (serverTime > localTime) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load project metadata (name, status, etc.) from backend
|
||||
if (enableBackendSync && projectId && apiClient) {
|
||||
try {
|
||||
const info = await apiClient.getProject(projectId)
|
||||
dispatch({ type: 'SET_STATE', payload: { projectInfo: info } })
|
||||
} catch (err) {
|
||||
console.warn('Failed to load project info:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INIT / CLEANUP HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function initSyncInfra(
|
||||
enableBackendSync: boolean,
|
||||
tenantId: string,
|
||||
projectId: string | undefined,
|
||||
apiClientRef: React.MutableRefObject<SDKApiClient | null>,
|
||||
syncManagerRef: React.MutableRefObject<StateSyncManager | null>,
|
||||
callbacks: SyncCallbacks
|
||||
): void {
|
||||
if (!enableBackendSync || typeof window === 'undefined') return
|
||||
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
syncManagerRef.current = createStateSyncManager(
|
||||
apiClientRef.current,
|
||||
tenantId,
|
||||
{ debounceMs: 2000, maxRetries: 3 },
|
||||
callbacks,
|
||||
projectId
|
||||
)
|
||||
}
|
||||
|
||||
export function cleanupSyncInfra(
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: React.MutableRefObject<StateSyncManager | null>,
|
||||
apiClientRef: React.MutableRefObject<SDKApiClient | null>
|
||||
): void {
|
||||
if (syncManagerRef.current) {
|
||||
syncManagerRef.current.destroy()
|
||||
syncManagerRef.current = null
|
||||
}
|
||||
if (enableBackendSync) {
|
||||
resetSDKApiClient()
|
||||
apiClientRef.current = null
|
||||
}
|
||||
}
|
||||
203
admin-compliance/lib/sdk/context-types.ts
Normal file
203
admin-compliance/lib/sdk/context-types.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
SDKState,
|
||||
SDKAction,
|
||||
SDKStep,
|
||||
CheckpointStatus,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
UserPreferences,
|
||||
CustomerType,
|
||||
CompanyProfile,
|
||||
ImportedDocument,
|
||||
GapAnalysis,
|
||||
SDKPackageId,
|
||||
ProjectInfo,
|
||||
} from './types'
|
||||
import { SyncState } from './sync'
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE
|
||||
// =============================================================================
|
||||
|
||||
const initialPreferences: UserPreferences = {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true, // Standard: Paralleles Arbeiten erlaubt
|
||||
}
|
||||
|
||||
export const initialState: SDKState = {
|
||||
// Metadata
|
||||
version: '1.0.0',
|
||||
projectVersion: 1,
|
||||
lastModified: new Date(),
|
||||
|
||||
// Tenant & User
|
||||
tenantId: '',
|
||||
userId: '',
|
||||
subscription: 'PROFESSIONAL',
|
||||
|
||||
// Project Context
|
||||
projectId: '',
|
||||
projectInfo: null,
|
||||
|
||||
// Customer Type
|
||||
customerType: null,
|
||||
|
||||
// Company Profile
|
||||
companyProfile: null,
|
||||
|
||||
// Compliance Scope
|
||||
complianceScope: null,
|
||||
|
||||
// Source Policy
|
||||
sourcePolicy: null,
|
||||
|
||||
// Progress
|
||||
currentPhase: 1,
|
||||
currentStep: 'company-profile',
|
||||
completedSteps: [],
|
||||
checkpoints: {},
|
||||
|
||||
// Imported Documents (for existing customers)
|
||||
importedDocuments: [],
|
||||
gapAnalysis: null,
|
||||
|
||||
// Phase 1 Data
|
||||
useCases: [],
|
||||
activeUseCase: null,
|
||||
screening: null,
|
||||
modules: [],
|
||||
requirements: [],
|
||||
controls: [],
|
||||
evidence: [],
|
||||
checklist: [],
|
||||
risks: [],
|
||||
|
||||
// Phase 2 Data
|
||||
aiActClassification: null,
|
||||
obligations: [],
|
||||
dsfa: null,
|
||||
toms: [],
|
||||
retentionPolicies: [],
|
||||
vvt: [],
|
||||
documents: [],
|
||||
cookieBanner: null,
|
||||
consents: [],
|
||||
dsrConfig: null,
|
||||
escalationWorkflows: [],
|
||||
|
||||
// IACE (Industrial AI Compliance Engine)
|
||||
iaceProjects: [],
|
||||
|
||||
// RAG Corpus Versioning
|
||||
ragCorpusStatus: null,
|
||||
|
||||
// Security
|
||||
sbom: null,
|
||||
securityIssues: [],
|
||||
securityBacklog: [],
|
||||
|
||||
// Catalog Manager
|
||||
customCatalogs: {},
|
||||
|
||||
// UI State
|
||||
commandBarHistory: [],
|
||||
recentSearches: [],
|
||||
preferences: initialPreferences,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXTENDED ACTION TYPES
|
||||
// =============================================================================
|
||||
|
||||
// Extended action type to include demo data loading
|
||||
export type ExtendedSDKAction =
|
||||
| SDKAction
|
||||
| { type: 'LOAD_DEMO_DATA'; payload: Partial<SDKState> }
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface SDKContextValue {
|
||||
state: SDKState
|
||||
dispatch: React.Dispatch<ExtendedSDKAction>
|
||||
|
||||
// Navigation
|
||||
currentStep: SDKStep | undefined
|
||||
goToStep: (stepId: string) => void
|
||||
goToNextStep: () => void
|
||||
goToPreviousStep: () => void
|
||||
canGoNext: boolean
|
||||
canGoPrevious: boolean
|
||||
|
||||
// Progress
|
||||
completionPercentage: number
|
||||
phase1Completion: number
|
||||
phase2Completion: number
|
||||
packageCompletion: Record<SDKPackageId, number>
|
||||
|
||||
// Customer Type
|
||||
setCustomerType: (type: CustomerType) => void
|
||||
|
||||
// Company Profile
|
||||
setCompanyProfile: (profile: CompanyProfile) => void
|
||||
updateCompanyProfile: (updates: Partial<CompanyProfile>) => void
|
||||
|
||||
// Compliance Scope
|
||||
setComplianceScope: (scope: import('./compliance-scope-types').ComplianceScopeState) => void
|
||||
updateComplianceScope: (updates: Partial<import('./compliance-scope-types').ComplianceScopeState>) => void
|
||||
|
||||
// Import (for existing customers)
|
||||
addImportedDocument: (doc: ImportedDocument) => void
|
||||
setGapAnalysis: (analysis: GapAnalysis) => void
|
||||
|
||||
// Checkpoints
|
||||
validateCheckpoint: (checkpointId: string) => Promise<CheckpointStatus>
|
||||
overrideCheckpoint: (checkpointId: string, reason: string) => Promise<void>
|
||||
getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined
|
||||
|
||||
// State Updates
|
||||
updateUseCase: (id: string, data: Partial<UseCaseAssessment>) => void
|
||||
addRisk: (risk: Risk) => void
|
||||
updateControl: (id: string, data: Partial<Control>) => void
|
||||
|
||||
// Persistence
|
||||
saveState: () => Promise<void>
|
||||
loadState: () => Promise<void>
|
||||
|
||||
// Demo Data
|
||||
loadDemoData: (demoState: Partial<SDKState>) => void
|
||||
seedDemoData: () => Promise<{ success: boolean; message: string }>
|
||||
clearDemoData: () => Promise<boolean>
|
||||
isDemoDataLoaded: boolean
|
||||
|
||||
// Sync
|
||||
syncState: SyncState
|
||||
forceSyncToServer: () => Promise<void>
|
||||
isOnline: boolean
|
||||
|
||||
// Export
|
||||
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
|
||||
|
||||
// Command Bar
|
||||
isCommandBarOpen: boolean
|
||||
setCommandBarOpen: (open: boolean) => void
|
||||
|
||||
// Project Management
|
||||
projectId: string | undefined
|
||||
createProject: (name: string, customerType: CustomerType, copyFromProjectId?: string) => Promise<ProjectInfo>
|
||||
listProjects: () => Promise<ProjectInfo[]>
|
||||
switchProject: (projectId: string) => void
|
||||
archiveProject: (projectId: string) => Promise<void>
|
||||
restoreProject: (projectId: string) => Promise<ProjectInfo>
|
||||
permanentlyDeleteProject: (projectId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const SDK_STORAGE_KEY = 'ai-compliance-sdk-state'
|
||||
94
admin-compliance/lib/sdk/context-validators.ts
Normal file
94
admin-compliance/lib/sdk/context-validators.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// LOCAL CHECKPOINT VALIDATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Performs local (client-side) checkpoint validation against the current SDK state.
|
||||
* Returns a CheckpointStatus with errors/warnings populated.
|
||||
*/
|
||||
export function validateCheckpointLocally(
|
||||
checkpointId: string,
|
||||
state: SDKState
|
||||
): CheckpointStatus {
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId,
|
||||
passed: true,
|
||||
validatedAt: new Date(),
|
||||
validatedBy: 'SYSTEM',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
|
||||
switch (checkpointId) {
|
||||
case 'CP-PROF':
|
||||
if (!state.companyProfile || !state.companyProfile.isComplete) {
|
||||
status.passed = false
|
||||
status.errors.push({
|
||||
ruleId: 'prof-complete',
|
||||
field: 'companyProfile',
|
||||
message: 'Unternehmensprofil muss vollständig ausgefüllt werden',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-UC':
|
||||
if (state.useCases.length === 0) {
|
||||
status.passed = false
|
||||
status.errors.push({
|
||||
ruleId: 'uc-min-count',
|
||||
field: 'useCases',
|
||||
message: 'Mindestens ein Anwendungsfall muss erstellt werden',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-SCAN':
|
||||
if (!state.screening || state.screening.status !== 'COMPLETED') {
|
||||
status.passed = false
|
||||
status.errors.push({
|
||||
ruleId: 'scan-complete',
|
||||
field: 'screening',
|
||||
message: 'Security Scan muss abgeschlossen sein',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-MOD':
|
||||
if (state.modules.length === 0) {
|
||||
status.passed = false
|
||||
status.errors.push({
|
||||
ruleId: 'mod-min-count',
|
||||
field: 'modules',
|
||||
message: 'Mindestens ein Modul muss zugewiesen werden',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-RISK': {
|
||||
const criticalRisks = state.risks.filter(
|
||||
r => r.severity === 'CRITICAL' || r.severity === 'HIGH'
|
||||
)
|
||||
const unmitigatedRisks = criticalRisks.filter(
|
||||
r => r.mitigation.length === 0
|
||||
)
|
||||
if (unmitigatedRisks.length > 0) {
|
||||
status.passed = false
|
||||
status.errors.push({
|
||||
ruleId: 'critical-risks-mitigated',
|
||||
field: 'risks',
|
||||
message: `${unmitigatedRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Datapoint Helpers — Generation Functions
|
||||
*
|
||||
* Functions that generate DSGVO-compliant text blocks from data points
|
||||
* for the document generator.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
RetentionPeriod,
|
||||
RiskLevel,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
RISK_LEVEL_STYLING,
|
||||
LocalizedText,
|
||||
SupportedLanguage
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export type Language = SupportedLanguage
|
||||
|
||||
export interface DataPointPlaceholders {
|
||||
'[DATENPUNKTE_COUNT]': string
|
||||
'[DATENPUNKTE_LIST]': string
|
||||
'[DATENPUNKTE_TABLE]': string
|
||||
'[VERARBEITUNGSZWECKE]': string
|
||||
'[RECHTSGRUNDLAGEN]': string
|
||||
'[SPEICHERFRISTEN]': string
|
||||
'[EMPFAENGER]': string
|
||||
'[BESONDERE_KATEGORIEN]': string
|
||||
'[DRITTLAND_TRANSFERS]': string
|
||||
'[RISIKO_ZUSAMMENFASSUNG]': string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getText(text: LocalizedText, lang: Language): string {
|
||||
return text[lang] || text.de
|
||||
}
|
||||
|
||||
export function groupByRetention(
|
||||
dataPoints: DataPoint[]
|
||||
): Record<RetentionPeriod, DataPoint[]> {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
const key = dp.retentionPeriod
|
||||
if (!acc[key]) acc[key] = []
|
||||
acc[key].push(dp)
|
||||
return acc
|
||||
}, {} as Record<RetentionPeriod, DataPoint[]>)
|
||||
}
|
||||
|
||||
export function groupByCategory(
|
||||
dataPoints: DataPoint[]
|
||||
): Record<DataPointCategory, DataPoint[]> {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
const key = dp.category
|
||||
if (!acc[key]) acc[key] = []
|
||||
acc[key].push(dp)
|
||||
return acc
|
||||
}, {} as Record<DataPointCategory, DataPoint[]>)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function generateDataPointsTable(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
if (dataPoints.length === 0) {
|
||||
return lang === 'de' ? '*Keine Datenpunkte ausgewaehlt.*' : '*No data points selected.*'
|
||||
}
|
||||
|
||||
const header = lang === 'de'
|
||||
? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
|
||||
: '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
|
||||
const separator = '|------------|-----------|-------|-----------------|---------------|'
|
||||
|
||||
const rows = dataPoints.map(dp => {
|
||||
const category = CATEGORY_METADATA[dp.category]
|
||||
const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
|
||||
const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
|
||||
|
||||
const name = getText(dp.name, lang)
|
||||
const categoryName = getText(category.name, lang)
|
||||
const purpose = getText(dp.purpose, lang)
|
||||
const legalBasisName = getText(legalBasis.name, lang)
|
||||
const retentionLabel = getText(retention.label, lang)
|
||||
|
||||
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
|
||||
|
||||
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
|
||||
}).join('\n')
|
||||
|
||||
return `${header}\n${separator}\n${rows}`
|
||||
}
|
||||
|
||||
export function generateSpecialCategorySection(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const special = dataPoints.filter(dp => dp.isSpecialCategory)
|
||||
if (special.length === 0) return ''
|
||||
|
||||
if (lang === 'de') {
|
||||
const items = special.map(dp =>
|
||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
||||
).join('\n')
|
||||
|
||||
return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
|
||||
|
||||
Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
|
||||
|
||||
${items}
|
||||
|
||||
Die Verarbeitung erfolgt auf Grundlage Ihrer ausdruecklichen Einwilligung gemaess Art. 9 Abs. 2 lit. a DSGVO. Sie koennen Ihre Einwilligung jederzeit mit Wirkung fuer die Zukunft widerrufen.`
|
||||
} else {
|
||||
const items = special.map(dp =>
|
||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
||||
).join('\n')
|
||||
|
||||
return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
|
||||
|
||||
We process the following special categories of personal data:
|
||||
|
||||
${items}
|
||||
|
||||
Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePurposesList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const purposes = new Set<string>()
|
||||
dataPoints.forEach(dp => purposes.add(getText(dp.purpose, lang)))
|
||||
return [...purposes].join(', ')
|
||||
}
|
||||
|
||||
export function generateLegalBasisList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const bases = new Set<LegalBasis>()
|
||||
dataPoints.forEach(dp => bases.add(dp.legalBasis))
|
||||
|
||||
return [...bases].map(basis => {
|
||||
const info = LEGAL_BASIS_INFO[basis]
|
||||
return `${info.article} (${getText(info.name, lang)})`
|
||||
}).join(', ')
|
||||
}
|
||||
|
||||
export function generateRetentionList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const grouped = groupByRetention(dataPoints)
|
||||
const entries: string[] = []
|
||||
|
||||
for (const [period, points] of Object.entries(grouped)) {
|
||||
const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
|
||||
const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
|
||||
entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
|
||||
}
|
||||
|
||||
return entries.join('; ')
|
||||
}
|
||||
|
||||
export function generateRecipientsList(dataPoints: DataPoint[]): string {
|
||||
const recipients = new Set<string>()
|
||||
dataPoints.forEach(dp => {
|
||||
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
|
||||
})
|
||||
if (recipients.size === 0) return ''
|
||||
return [...recipients].join(', ')
|
||||
}
|
||||
|
||||
export function generateThirdCountrySection(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
|
||||
|
||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
||||
dp.thirdPartyRecipients?.some(r =>
|
||||
thirdCountryIndicators.some(indicator =>
|
||||
r.toLowerCase().includes(indicator.toLowerCase())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (thirdCountryPoints.length === 0) return ''
|
||||
|
||||
const recipients = new Set<string>()
|
||||
thirdCountryPoints.forEach(dp => {
|
||||
dp.thirdPartyRecipients?.forEach(r => {
|
||||
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
|
||||
recipients.add(r)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (lang === 'de') {
|
||||
return `## Uebermittlung in Drittlaender
|
||||
|
||||
Wir uebermitteln personenbezogene Daten an folgende Empfaenger in Drittlaendern (ausserhalb der EU/des EWR):
|
||||
|
||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
||||
|
||||
Die Uebermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
|
||||
} else {
|
||||
return `## Transfers to Third Countries
|
||||
|
||||
We transfer personal data to the following recipients in third countries (outside the EU/EEA):
|
||||
|
||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
||||
|
||||
The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRiskSummary(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const riskCounts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||
dataPoints.forEach(dp => riskCounts[dp.riskLevel]++)
|
||||
|
||||
const parts = Object.entries(riskCounts)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([level, count]) => {
|
||||
const styling = RISK_LEVEL_STYLING[level as RiskLevel]
|
||||
return `${count} ${getText(styling.label, lang).toLowerCase()}`
|
||||
})
|
||||
|
||||
return parts.join(', ')
|
||||
}
|
||||
|
||||
export function generateAllPlaceholders(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): DataPointPlaceholders {
|
||||
return {
|
||||
'[DATENPUNKTE_COUNT]': String(dataPoints.length),
|
||||
'[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
|
||||
'[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
|
||||
'[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
|
||||
'[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
|
||||
'[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
|
||||
'[EMPFAENGER]': generateRecipientsList(dataPoints),
|
||||
'[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
|
||||
'[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
|
||||
'[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
|
||||
}
|
||||
}
|
||||
@@ -1,548 +1,37 @@
|
||||
/**
|
||||
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
|
||||
* in den Dokumentengenerator.
|
||||
* Datapoint Helpers — barrel re-export
|
||||
*
|
||||
* Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf
|
||||
* den vom Benutzer ausgewählten Datenpunkten.
|
||||
* Split into:
|
||||
* - datapoint-generators.ts (text generation functions)
|
||||
* - datapoint-validators.ts (document validation checks)
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
RetentionPeriod,
|
||||
RiskLevel,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
RISK_LEVEL_STYLING,
|
||||
LocalizedText,
|
||||
SupportedLanguage
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Sprach-Option für alle Helper-Funktionen
|
||||
*/
|
||||
export type Language = SupportedLanguage
|
||||
|
||||
/**
|
||||
* Generierte Platzhalter-Map für den Dokumentengenerator
|
||||
*/
|
||||
export interface DataPointPlaceholders {
|
||||
'[DATENPUNKTE_COUNT]': string
|
||||
'[DATENPUNKTE_LIST]': string
|
||||
'[DATENPUNKTE_TABLE]': string
|
||||
'[VERARBEITUNGSZWECKE]': string
|
||||
'[RECHTSGRUNDLAGEN]': string
|
||||
'[SPEICHERFRISTEN]': string
|
||||
'[EMPFAENGER]': string
|
||||
'[BESONDERE_KATEGORIEN]': string
|
||||
'[DRITTLAND_TRANSFERS]': string
|
||||
'[RISIKO_ZUSAMMENFASSUNG]': string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Extrahiert Text aus LocalizedText basierend auf Sprache
|
||||
*/
|
||||
function getText(text: LocalizedText, lang: Language): string {
|
||||
return text[lang] || text.de
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine Markdown-Tabelle der Datenpunkte
|
||||
*
|
||||
* @param dataPoints - Liste der ausgewählten Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Markdown-Tabelle als String
|
||||
*/
|
||||
export function generateDataPointsTable(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
if (dataPoints.length === 0) {
|
||||
return lang === 'de'
|
||||
? '*Keine Datenpunkte ausgewählt.*'
|
||||
: '*No data points selected.*'
|
||||
}
|
||||
|
||||
const header = lang === 'de'
|
||||
? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
|
||||
: '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
|
||||
const separator = '|------------|-----------|-------|-----------------|---------------|'
|
||||
|
||||
const rows = dataPoints.map(dp => {
|
||||
const category = CATEGORY_METADATA[dp.category]
|
||||
const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
|
||||
const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
|
||||
|
||||
const name = getText(dp.name, lang)
|
||||
const categoryName = getText(category.name, lang)
|
||||
const purpose = getText(dp.purpose, lang)
|
||||
const legalBasisName = getText(legalBasis.name, lang)
|
||||
const retentionLabel = getText(retention.label, lang)
|
||||
|
||||
// Truncate long texts for table readability
|
||||
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
|
||||
|
||||
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
|
||||
}).join('\n')
|
||||
|
||||
return `${header}\n${separator}\n${rows}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Speicherfrist
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @returns Record mit Speicherfrist als Key und Datenpunkten als Value
|
||||
*/
|
||||
export function groupByRetention(
|
||||
dataPoints: DataPoint[]
|
||||
): Record<RetentionPeriod, DataPoint[]> {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
const key = dp.retentionPeriod
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(dp)
|
||||
return acc
|
||||
}, {} as Record<RetentionPeriod, DataPoint[]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Kategorie
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @returns Record mit Kategorie als Key und Datenpunkten als Value
|
||||
*/
|
||||
export function groupByCategory(
|
||||
dataPoints: DataPoint[]
|
||||
): Record<DataPointCategory, DataPoint[]> {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
const key = dp.category
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(dp)
|
||||
return acc
|
||||
}, {} as Record<DataPointCategory, DataPoint[]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO)
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten)
|
||||
*/
|
||||
export function generateSpecialCategorySection(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const special = dataPoints.filter(dp => dp.isSpecialCategory)
|
||||
|
||||
if (special.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (lang === 'de') {
|
||||
const items = special.map(dp =>
|
||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
||||
).join('\n')
|
||||
|
||||
return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
|
||||
|
||||
Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
|
||||
|
||||
${items}
|
||||
|
||||
Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.`
|
||||
} else {
|
||||
const items = special.map(dp =>
|
||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
||||
).join('\n')
|
||||
|
||||
return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
|
||||
|
||||
We process the following special categories of personal data:
|
||||
|
||||
${items}
|
||||
|
||||
Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller eindeutigen Verarbeitungszwecke
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Kommaseparierte Liste der Zwecke
|
||||
*/
|
||||
export function generatePurposesList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const purposes = new Set<string>()
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
purposes.add(getText(dp.purpose, lang))
|
||||
})
|
||||
|
||||
return [...purposes].join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller verwendeten Rechtsgrundlagen
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Formatierte Liste der Rechtsgrundlagen
|
||||
*/
|
||||
export function generateLegalBasisList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const bases = new Set<LegalBasis>()
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
bases.add(dp.legalBasis)
|
||||
})
|
||||
|
||||
return [...bases].map(basis => {
|
||||
const info = LEGAL_BASIS_INFO[basis]
|
||||
return `${info.article} (${getText(info.name, lang)})`
|
||||
}).join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller Speicherfristen gruppiert
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien
|
||||
*/
|
||||
export function generateRetentionList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const grouped = groupByRetention(dataPoints)
|
||||
const entries: string[] = []
|
||||
|
||||
for (const [period, points] of Object.entries(grouped)) {
|
||||
const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
|
||||
const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
|
||||
|
||||
entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
|
||||
}
|
||||
|
||||
return entries.join('; ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller Empfänger/Drittparteien
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @returns Kommaseparierte Liste der Empfänger
|
||||
*/
|
||||
export function generateRecipientsList(dataPoints: DataPoint[]): string {
|
||||
const recipients = new Set<string>()
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
|
||||
})
|
||||
|
||||
if (recipients.size === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return [...recipients].join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Abschnitt für Drittland-Übermittlungen
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Markdown-Abschnitt als String
|
||||
*/
|
||||
export function generateThirdCountrySection(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
// Note: We assume dataPoints have been filtered for thirdCountryTransfer
|
||||
// The actual flag would need to be added to the DataPoint interface
|
||||
// For now, we check if any thirdPartyRecipients suggest third country
|
||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
|
||||
|
||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
||||
dp.thirdPartyRecipients?.some(r =>
|
||||
thirdCountryIndicators.some(indicator =>
|
||||
r.toLowerCase().includes(indicator.toLowerCase())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (thirdCountryPoints.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const recipients = new Set<string>()
|
||||
thirdCountryPoints.forEach(dp => {
|
||||
dp.thirdPartyRecipients?.forEach(r => {
|
||||
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
|
||||
recipients.add(r)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (lang === 'de') {
|
||||
return `## Übermittlung in Drittländer
|
||||
|
||||
Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR):
|
||||
|
||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
||||
|
||||
Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
|
||||
} else {
|
||||
return `## Transfers to Third Countries
|
||||
|
||||
We transfer personal data to the following recipients in third countries (outside the EU/EEA):
|
||||
|
||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
||||
|
||||
The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Risiko-Zusammenfassung
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Formatierte Risiko-Zusammenfassung
|
||||
*/
|
||||
export function generateRiskSummary(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const riskCounts: Record<RiskLevel, number> = {
|
||||
LOW: 0,
|
||||
MEDIUM: 0,
|
||||
HIGH: 0
|
||||
}
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
riskCounts[dp.riskLevel]++
|
||||
})
|
||||
|
||||
const parts = Object.entries(riskCounts)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([level, count]) => {
|
||||
const styling = RISK_LEVEL_STYLING[level as RiskLevel]
|
||||
return `${count} ${getText(styling.label, lang).toLowerCase()}`
|
||||
})
|
||||
|
||||
return parts.join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert alle Platzhalter für den Dokumentengenerator
|
||||
*
|
||||
* @param dataPoints - Liste der ausgewählten Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Objekt mit allen Platzhaltern
|
||||
*/
|
||||
export function generateAllPlaceholders(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): DataPointPlaceholders {
|
||||
return {
|
||||
'[DATENPUNKTE_COUNT]': String(dataPoints.length),
|
||||
'[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
|
||||
'[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
|
||||
'[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
|
||||
'[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
|
||||
'[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
|
||||
'[EMPFAENGER]': generateRecipientsList(dataPoints),
|
||||
'[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
|
||||
'[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
|
||||
'[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION HELPERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Validierungswarnung für den Dokumentengenerator
|
||||
*/
|
||||
export interface ValidationWarning {
|
||||
type: 'error' | 'warning' | 'info'
|
||||
code: string
|
||||
message: string
|
||||
suggestion: string
|
||||
affectedDataPoints?: DataPoint[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns ValidationWarning oder null
|
||||
*/
|
||||
export function checkSpecialCategoriesWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
|
||||
|
||||
if (specialCategories.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSection = lang === 'de'
|
||||
? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
|
||||
: documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
|
||||
|
||||
if (!hasSection) {
|
||||
return {
|
||||
type: 'error',
|
||||
code: 'MISSING_ART9_SECTION',
|
||||
message: lang === 'de'
|
||||
? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.`
|
||||
: `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
|
||||
: 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
|
||||
affectedDataPoints: specialCategories
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns ValidationWarning oder null
|
||||
*/
|
||||
export function checkThirdCountryWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
|
||||
|
||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
||||
dp.thirdPartyRecipients?.some(r =>
|
||||
thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
|
||||
)
|
||||
)
|
||||
|
||||
if (thirdCountryPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSCCMention = lang === 'de'
|
||||
? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
||||
: documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
||||
|
||||
if (!hasSCCMention) {
|
||||
return {
|
||||
type: 'warning',
|
||||
code: 'MISSING_SCC_SECTION',
|
||||
message: lang === 'de'
|
||||
? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.`
|
||||
: `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
|
||||
: 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
|
||||
affectedDataPoints: thirdCountryPoints
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns ValidationWarning oder null
|
||||
*/
|
||||
export function checkExplicitConsentWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
|
||||
|
||||
if (explicitConsentPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasConsentSection = lang === 'de'
|
||||
? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
|
||||
: documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
|
||||
|
||||
if (!hasConsentSection) {
|
||||
return {
|
||||
type: 'warning',
|
||||
code: 'MISSING_CONSENT_SECTION',
|
||||
message: lang === 'de'
|
||||
? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
|
||||
: `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
|
||||
: 'Add a section about the right to withdraw consent.',
|
||||
affectedDataPoints: explicitConsentPoints
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt alle Validierungsprüfungen durch
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns Array aller Warnungen
|
||||
*/
|
||||
export function validateDocument(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning[] {
|
||||
const warnings: ValidationWarning[] = []
|
||||
|
||||
const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
|
||||
if (specialCatWarning) warnings.push(specialCatWarning)
|
||||
|
||||
const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
|
||||
if (thirdCountryWarning) warnings.push(thirdCountryWarning)
|
||||
|
||||
const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
|
||||
if (consentWarning) warnings.push(consentWarning)
|
||||
|
||||
return warnings
|
||||
}
|
||||
export type {
|
||||
Language,
|
||||
DataPointPlaceholders,
|
||||
} from './datapoint-generators'
|
||||
|
||||
export {
|
||||
generateDataPointsTable,
|
||||
groupByRetention,
|
||||
groupByCategory,
|
||||
generateSpecialCategorySection,
|
||||
generatePurposesList,
|
||||
generateLegalBasisList,
|
||||
generateRetentionList,
|
||||
generateRecipientsList,
|
||||
generateThirdCountrySection,
|
||||
generateRiskSummary,
|
||||
generateAllPlaceholders,
|
||||
} from './datapoint-generators'
|
||||
|
||||
export type {
|
||||
ValidationWarning,
|
||||
} from './datapoint-validators'
|
||||
|
||||
export {
|
||||
checkSpecialCategoriesWarning,
|
||||
checkThirdCountryWarning,
|
||||
checkExplicitConsentWarning,
|
||||
validateDocument,
|
||||
} from './datapoint-validators'
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Datapoint Helpers — Validation Functions
|
||||
*
|
||||
* Document validation checks for DSGVO compliance.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
|
||||
import type { Language } from './datapoint-generators'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ValidationWarning {
|
||||
type: 'error' | 'warning' | 'info'
|
||||
code: string
|
||||
message: string
|
||||
suggestion: string
|
||||
affectedDataPoints?: DataPoint[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function checkSpecialCategoriesWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
|
||||
|
||||
if (specialCategories.length === 0) return null
|
||||
|
||||
const hasSection = lang === 'de'
|
||||
? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
|
||||
: documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
|
||||
|
||||
if (!hasSection) {
|
||||
return {
|
||||
type: 'error',
|
||||
code: 'MISSING_ART9_SECTION',
|
||||
message: lang === 'de'
|
||||
? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewaehlt, aber kein entsprechender Abschnitt im Dokument gefunden.`
|
||||
: `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Fuegen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
|
||||
: 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
|
||||
affectedDataPoints: specialCategories
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function checkThirdCountryWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
|
||||
|
||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
||||
dp.thirdPartyRecipients?.some(r =>
|
||||
thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
|
||||
)
|
||||
)
|
||||
|
||||
if (thirdCountryPoints.length === 0) return null
|
||||
|
||||
const hasSCCMention = lang === 'de'
|
||||
? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
||||
: documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
||||
|
||||
if (!hasSCCMention) {
|
||||
return {
|
||||
type: 'warning',
|
||||
code: 'MISSING_SCC_SECTION',
|
||||
message: lang === 'de'
|
||||
? `Drittland-Uebermittlung fuer ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwaehnt.`
|
||||
: `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Erwaegen Sie die Aufnahme eines Abschnitts zu Drittland-Uebermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
|
||||
: 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
|
||||
affectedDataPoints: thirdCountryPoints
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function checkExplicitConsentWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
|
||||
|
||||
if (explicitConsentPoints.length === 0) return null
|
||||
|
||||
const hasConsentSection = lang === 'de'
|
||||
? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
|
||||
: documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
|
||||
|
||||
if (!hasConsentSection) {
|
||||
return {
|
||||
type: 'warning',
|
||||
code: 'MISSING_CONSENT_SECTION',
|
||||
message: lang === 'de'
|
||||
? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrueckliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
|
||||
: `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Fuegen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
|
||||
: 'Add a section about the right to withdraw consent.',
|
||||
affectedDataPoints: explicitConsentPoints
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function validateDocument(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning[] {
|
||||
const warnings: ValidationWarning[] = []
|
||||
|
||||
const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
|
||||
if (specialCatWarning) warnings.push(specialCatWarning)
|
||||
|
||||
const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
|
||||
if (thirdCountryWarning) warnings.push(thirdCountryWarning)
|
||||
|
||||
const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
|
||||
if (consentWarning) warnings.push(consentWarning)
|
||||
|
||||
return warnings
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
98
admin-compliance/lib/sdk/dsfa/types/api-types.ts
Normal file
98
admin-compliance/lib/sdk/dsfa/types/api-types.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// =============================================================================
|
||||
// API REQUEST/RESPONSE TYPES
|
||||
// =============================================================================
|
||||
|
||||
import type { DSFAStatus, DSFARiskLevel } from './enums-constants'
|
||||
import type { DSFA } from './main-dsfa'
|
||||
|
||||
export interface DSFAListResponse {
|
||||
dsfas: DSFA[]
|
||||
}
|
||||
|
||||
export interface DSFAStatsResponse {
|
||||
status_stats: Record<DSFAStatus | 'total', number>
|
||||
risk_stats: Record<DSFARiskLevel, number>
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface CreateDSFARequest {
|
||||
name: string
|
||||
description?: string
|
||||
processing_description?: string
|
||||
processing_purpose?: string
|
||||
data_categories?: string[]
|
||||
legal_basis?: string
|
||||
}
|
||||
|
||||
export interface CreateDSFAFromAssessmentRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface CreateDSFAFromAssessmentResponse {
|
||||
dsfa: DSFA
|
||||
prefilled: boolean
|
||||
assessment: unknown // UCCA Assessment
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface UpdateDSFASectionRequest {
|
||||
// Section 1
|
||||
processing_description?: string
|
||||
processing_purpose?: string
|
||||
data_categories?: string[]
|
||||
data_subjects?: string[]
|
||||
recipients?: string[]
|
||||
legal_basis?: string
|
||||
legal_basis_details?: string
|
||||
|
||||
// Section 2
|
||||
necessity_assessment?: string
|
||||
proportionality_assessment?: string
|
||||
data_minimization?: string
|
||||
alternatives_considered?: string
|
||||
retention_justification?: string
|
||||
|
||||
// Section 3
|
||||
overall_risk_level?: DSFARiskLevel
|
||||
risk_score?: number
|
||||
affected_rights?: string[]
|
||||
|
||||
// Section 5
|
||||
dpo_consulted?: boolean
|
||||
dpo_name?: string
|
||||
dpo_opinion?: string
|
||||
authority_consulted?: boolean
|
||||
authority_reference?: string
|
||||
authority_decision?: string
|
||||
}
|
||||
|
||||
export interface SubmitForReviewResponse {
|
||||
message: string
|
||||
dsfa: DSFA
|
||||
}
|
||||
|
||||
export interface ApproveDSFARequest {
|
||||
dpo_opinion: string
|
||||
approved: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UCCA INTEGRATION TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFATriggerInfo {
|
||||
required: boolean
|
||||
reason: string
|
||||
triggered_rules: string[]
|
||||
assessment_id?: string
|
||||
existing_dsfa_id?: string
|
||||
}
|
||||
|
||||
export interface UCCATriggeredRule {
|
||||
code: string
|
||||
title: string
|
||||
description: string
|
||||
severity: 'INFO' | 'WARN' | 'BLOCK'
|
||||
gdpr_ref?: string
|
||||
}
|
||||
171
admin-compliance/lib/sdk/dsfa/types/authority-resources.ts
Normal file
171
admin-compliance/lib/sdk/dsfa/types/authority-resources.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// =============================================================================
|
||||
// DSFA MUSS-LISTEN NACH BUNDESLÄNDERN
|
||||
// Quellen: Jeweilige Landesdatenschutzbeauftragte
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFAAuthorityResource {
|
||||
id: string
|
||||
name: string
|
||||
shortName: string
|
||||
state: string // Bundesland oder 'Bund'
|
||||
overviewUrl: string
|
||||
publicSectorListUrl?: string
|
||||
privateSectorListUrl?: string
|
||||
templateUrl?: string
|
||||
additionalResources?: Array<{ title: string; url: string }>
|
||||
}
|
||||
|
||||
export const DSFA_AUTHORITY_RESOURCES: DSFAAuthorityResource[] = [
|
||||
{
|
||||
id: 'bund',
|
||||
name: 'Bundesbeauftragter für den Datenschutz und die Informationsfreiheit',
|
||||
shortName: 'BfDI',
|
||||
state: 'Bund',
|
||||
overviewUrl: 'https://www.bfdi.bund.de/DE/Fachthemen/Inhalte/Technik/Datenschutz-Folgenabschaetzungen.html',
|
||||
publicSectorListUrl: 'https://www.bfdi.bund.de/SharedDocs/Downloads/DE/Muster/Liste_VerarbeitungsvorgaengeArt35.pdf',
|
||||
templateUrl: 'https://www.bfdi.bund.de/SharedDocs/Downloads/DE/Muster/Muster_Hinweise_DSFA.html',
|
||||
},
|
||||
{
|
||||
id: 'bw',
|
||||
name: 'Landesbeauftragter für den Datenschutz und die Informationsfreiheit Baden-Württemberg',
|
||||
shortName: 'LfDI BW',
|
||||
state: 'Baden-Württemberg',
|
||||
overviewUrl: 'https://www.baden-wuerttemberg.datenschutz.de/datenschutz-folgenabschaetzung/',
|
||||
privateSectorListUrl: 'https://www.baden-wuerttemberg.datenschutz.de/wp-content/uploads/2018/05/Liste-von-Verarbeitungsvorg%C3%A4ngen-nach-Art.-35-Abs.-4-DS-GVO-LfDI-BW.pdf',
|
||||
},
|
||||
{
|
||||
id: 'by',
|
||||
name: 'Bayerischer Landesbeauftragter für den Datenschutz',
|
||||
shortName: 'BayLfD',
|
||||
state: 'Bayern',
|
||||
overviewUrl: 'https://www.datenschutz-bayern.de/dsfa/',
|
||||
additionalResources: [
|
||||
{ title: 'DSFA-Module und Formulare', url: 'https://www.datenschutz-bayern.de/dsfa/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'be',
|
||||
name: 'Berliner Beauftragte für Datenschutz und Informationsfreiheit',
|
||||
shortName: 'BlnBDI',
|
||||
state: 'Berlin',
|
||||
overviewUrl: 'https://www.datenschutz-berlin.de/themen/unternehmen/datenschutz-folgenabschaetzung/',
|
||||
publicSectorListUrl: 'https://www.datenschutz-berlin.de/fileadmin/user_upload/pdf/dokumente/2018-BlnBDI_DSFA-oeffentlich.pdf',
|
||||
privateSectorListUrl: 'https://www.datenschutz-berlin.de/fileadmin/user_upload/pdf/dokumente/2018-BlnBDI_DSFA-nicht-oeffentlich.pdf',
|
||||
},
|
||||
{
|
||||
id: 'bb',
|
||||
name: 'Landesbeauftragte für den Datenschutz und für das Recht auf Akteneinsicht Brandenburg',
|
||||
shortName: 'LDA BB',
|
||||
state: 'Brandenburg',
|
||||
overviewUrl: 'https://www.lda.brandenburg.de/lda/de/datenschutz/datenschutz-folgenabschaetzung/',
|
||||
publicSectorListUrl: 'https://www.lda.brandenburg.de/sixcms/media.php/9/DSFA-Liste_%C3%B6ffentlicher_Bereich.pdf',
|
||||
privateSectorListUrl: 'https://www.lda.brandenburg.de/sixcms/media.php/9/DSFA-Liste_nicht_%C3%B6ffentlicher_Bereich.pdf',
|
||||
},
|
||||
{
|
||||
id: 'hb',
|
||||
name: 'Landesbeauftragte für Datenschutz und Informationsfreiheit Bremen',
|
||||
shortName: 'LfDI HB',
|
||||
state: 'Bremen',
|
||||
overviewUrl: 'https://www.datenschutz.bremen.de/datenschutz/datenschutz-folgenabschaetzung-3884',
|
||||
publicSectorListUrl: 'https://www.datenschutz.bremen.de/sixcms/media.php/13/Liste%20von%20Verarbeitungsvorg%C3%A4ngen%20nach%20Artikel%2035.pdf',
|
||||
privateSectorListUrl: 'https://www.datenschutz.bremen.de/sixcms/media.php/13/DSFA%20Muss-Liste%20LfDI%20HB.pdf',
|
||||
},
|
||||
{
|
||||
id: 'hh',
|
||||
name: 'Hamburgischer Beauftragter für Datenschutz und Informationsfreiheit',
|
||||
shortName: 'HmbBfDI',
|
||||
state: 'Hamburg',
|
||||
overviewUrl: 'https://datenschutz-hamburg.de/datenschutz-folgenabschaetzung',
|
||||
publicSectorListUrl: 'https://datenschutz-hamburg.de/fileadmin/user_upload/HmbBfDI/Datenschutz/Informationen/Liste_Art_35-4_DSGVO_HmbBfDI-oeffentlicher_Bereich_v2.0a.pdf',
|
||||
privateSectorListUrl: 'https://datenschutz-hamburg.de/fileadmin/user_upload/HmbBfDI/Datenschutz/Informationen/DSFA_Muss-Liste_fuer_den_nicht-oeffentlicher_Bereich_-_Stand_17.10.2018.pdf',
|
||||
},
|
||||
{
|
||||
id: 'he',
|
||||
name: 'Hessischer Beauftragter für Datenschutz und Informationsfreiheit',
|
||||
shortName: 'HBDI',
|
||||
state: 'Hessen',
|
||||
overviewUrl: 'https://datenschutz.hessen.de/datenschutz/it-und-datenschutz/datenschutz-folgenabschaetzung',
|
||||
},
|
||||
{
|
||||
id: 'mv',
|
||||
name: 'Landesbeauftragter für Datenschutz und Informationsfreiheit Mecklenburg-Vorpommern',
|
||||
shortName: 'LfDI MV',
|
||||
state: 'Mecklenburg-Vorpommern',
|
||||
overviewUrl: 'https://www.datenschutz-mv.de/datenschutz/DSGVO/Hilfsmittel-zur-Umsetzung/',
|
||||
publicSectorListUrl: 'https://www.datenschutz-mv.de/static/DS/Dateien/DS-GVO/HilfsmittelzurUmsetzung/MV-DSFA-Muss-Liste-Oeffentlicher-Bereich.pdf',
|
||||
},
|
||||
{
|
||||
id: 'ni',
|
||||
name: 'Die Landesbeauftragte für den Datenschutz Niedersachsen',
|
||||
shortName: 'LfD NI',
|
||||
state: 'Niedersachsen',
|
||||
overviewUrl: 'https://www.lfd.niedersachsen.de/dsgvo/liste_von_verarbeitungsvorgangen_nach_art_35_abs_4_ds_gvo/muss-listen-zur-datenschutz-folgenabschatzung-179663.html',
|
||||
publicSectorListUrl: 'https://www.lfd.niedersachsen.de/download/134414/DSFA_Muss-Liste_fuer_den_oeffentlichen_Bereich.pdf',
|
||||
privateSectorListUrl: 'https://www.lfd.niedersachsen.de/download/131098/Liste_von_Verarbeitungsvorgaengen_nach_Art._35_Abs._4_DS-GVO.pdf',
|
||||
},
|
||||
{
|
||||
id: 'nw',
|
||||
name: 'Landesbeauftragte für Datenschutz und Informationsfreiheit Nordrhein-Westfalen',
|
||||
shortName: 'LDI NRW',
|
||||
state: 'Nordrhein-Westfalen',
|
||||
overviewUrl: 'https://www.ldi.nrw.de/datenschutz/wirtschaft/datenschutz-folgenabschaetzung',
|
||||
publicSectorListUrl: 'https://www.ldi.nrw.de/liste-von-verarbeitungsvorgaengen-nach-art-35-abs-4-ds-gvo-fuer-den-oeffentlichen-bereich',
|
||||
},
|
||||
{
|
||||
id: 'rp',
|
||||
name: 'Landesbeauftragter für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz',
|
||||
shortName: 'LfDI RP',
|
||||
state: 'Rheinland-Pfalz',
|
||||
overviewUrl: 'https://www.datenschutz.rlp.de/themen/datenschutz-folgenabschaetzung',
|
||||
},
|
||||
{
|
||||
id: 'sl',
|
||||
name: 'Unabhängiges Datenschutzzentrum Saarland',
|
||||
shortName: 'UDZ SL',
|
||||
state: 'Saarland',
|
||||
overviewUrl: 'https://www.datenschutz.saarland.de/themen/datenschutz-folgenabschaetzung',
|
||||
privateSectorListUrl: 'https://www.datenschutz.saarland.de/fileadmin/user_upload/uds/alle_Dateien_und_Ordner_bis_2025/Download/dsfa_muss_liste_dsk_de.pdf',
|
||||
},
|
||||
{
|
||||
id: 'sn',
|
||||
name: 'Sächsische Datenschutz- und Transparenzbeauftragte',
|
||||
shortName: 'SDTB',
|
||||
state: 'Sachsen',
|
||||
overviewUrl: 'https://www.datenschutz.sachsen.de/datenschutz-folgenabschaetzung.html',
|
||||
additionalResources: [
|
||||
{ title: 'Erforderlichkeit der DSFA', url: 'https://www.datenschutz.sachsen.de/erforderlichkeit.html' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'st',
|
||||
name: 'Landesbeauftragter für den Datenschutz Sachsen-Anhalt',
|
||||
shortName: 'LfD ST',
|
||||
state: 'Sachsen-Anhalt',
|
||||
overviewUrl: 'https://datenschutz.sachsen-anhalt.de/informationen/datenschutz-grundverordnung/liste-datenschutz-folgenabschaetzung',
|
||||
publicSectorListUrl: 'https://datenschutz.sachsen-anhalt.de/fileadmin/Bibliothek/Landesaemter/LfD/Informationen/Internationales/Datenschutz-Grundverordnung/Liste_DSFA/Art-35-Liste-oeffentlicher_Bereich.pdf',
|
||||
privateSectorListUrl: 'https://datenschutz.sachsen-anhalt.de/fileadmin/Bibliothek/Landesaemter/LfD/Informationen/Internationales/Datenschutz-Grundverordnung/Liste_DSFA/Art-35-Liste-nichtoeffentlicher_Bereich.pdf',
|
||||
},
|
||||
{
|
||||
id: 'sh',
|
||||
name: 'Unabhängiges Landeszentrum für Datenschutz Schleswig-Holstein',
|
||||
shortName: 'ULD SH',
|
||||
state: 'Schleswig-Holstein',
|
||||
overviewUrl: 'https://www.datenschutzzentrum.de/datenschutzfolgenabschaetzung/',
|
||||
privateSectorListUrl: 'https://www.datenschutzzentrum.de/uploads/datenschutzfolgenabschaetzung/20180525_LfD-SH_DSFA_Muss-Liste_V1.0.pdf',
|
||||
additionalResources: [
|
||||
{ title: 'Begleittext zur DSFA-Liste', url: 'https://www.datenschutzzentrum.de/uploads/dsgvo/2018_0807_LfD-SH_DSFA_Begleittext_V1.0a.pdf' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'th',
|
||||
name: 'Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit',
|
||||
shortName: 'TLfDI',
|
||||
state: 'Thüringen',
|
||||
overviewUrl: 'https://www.tlfdi.de/datenschutz/datenschutz-folgenabschaetzung/',
|
||||
privateSectorListUrl: 'https://tlfdi.de/fileadmin/tlfdi/datenschutz/dsfa_muss-liste_04_07_18.pdf',
|
||||
additionalResources: [
|
||||
{ title: 'Handreichung DS-FA (nicht-öffentlich)', url: 'https://tlfdi.de/fileadmin/tlfdi/datenschutz/handreichung_ds-fa.pdf' },
|
||||
{ title: 'Handreichung DS-FA (öffentlich)', url: 'https://tlfdi.de/fileadmin/tlfdi/Europa/Handreichung_zur_Datenschutz-Folgenabschaetzung_oeffentlicher_Bereich.pdf' },
|
||||
],
|
||||
},
|
||||
]
|
||||
84
admin-compliance/lib/sdk/dsfa/types/dsk-references.ts
Normal file
84
admin-compliance/lib/sdk/dsfa/types/dsk-references.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// =============================================================================
|
||||
// DSK KURZPAPIER NR. 5 REFERENZEN
|
||||
// =============================================================================
|
||||
|
||||
export const DSK_KURZPAPIER_5 = {
|
||||
title: 'Kurzpapier Nr. 5: Datenschutz-Folgenabschätzung nach Art. 35 DS-GVO',
|
||||
source: 'Datenschutzkonferenz (DSK)',
|
||||
url: 'https://www.datenschutzkonferenz-online.de/media/kp/dsk_kpnr_5.pdf',
|
||||
license: 'Datenlizenz Deutschland – Namensnennung – Version 2.0 (DL-DE BY 2.0)',
|
||||
licenseUrl: 'https://www.govdata.de/dl-de/by-2-0',
|
||||
processSteps: [
|
||||
{ step: 1, title: 'Projektteam bilden', description: 'Interdisziplinäres Team aus Datenschutz, Fachprozess, IT/Sicherheit' },
|
||||
{ step: 2, title: 'Verarbeitung abgrenzen', description: 'Scope definieren, Datenflüsse und Zwecke beschreiben' },
|
||||
{ step: 3, title: 'Prüfung der Notwendigkeit', description: 'Alternativen prüfen, Datenminimierung bewerten' },
|
||||
{ step: 4, title: 'Risiken identifizieren', description: 'Risikoquellen ermitteln, Schäden bewerten' },
|
||||
{ step: 5, title: 'Maßnahmen festlegen', description: 'TOM definieren, Restrisiko bewerten' },
|
||||
{ step: 6, title: 'Bericht erstellen', description: 'DSFA-Bericht dokumentieren, ggf. veröffentlichen' },
|
||||
{ step: 7, title: 'Fortschreibung', description: 'DSFA bei Änderungen aktualisieren' },
|
||||
],
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ART. 35 ABS. 3 DSGVO - REGELBEISPIELE
|
||||
// =============================================================================
|
||||
|
||||
export const ART35_ABS3_CASES = [
|
||||
{
|
||||
id: 'profiling_legal_effects',
|
||||
lit: 'a',
|
||||
title: 'Profiling mit Rechtswirkung',
|
||||
description: 'Systematische und umfassende Bewertung persönlicher Aspekte natürlicher Personen, die sich auf automatisierte Verarbeitung einschließlich Profiling gründet und die ihrerseits als Grundlage für Entscheidungen dient, die Rechtswirkung gegenüber natürlichen Personen entfalten oder diese in ähnlich erheblicher Weise beeinträchtigen.',
|
||||
gdprRef: 'Art. 35 Abs. 3 lit. a DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'special_categories',
|
||||
lit: 'b',
|
||||
title: 'Besondere Datenkategorien in großem Umfang',
|
||||
description: 'Umfangreiche Verarbeitung besonderer Kategorien von personenbezogenen Daten gemäß Artikel 9 Absatz 1 oder von personenbezogenen Daten über strafrechtliche Verurteilungen und Straftaten gemäß Artikel 10.',
|
||||
gdprRef: 'Art. 35 Abs. 3 lit. b DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'public_monitoring',
|
||||
lit: 'c',
|
||||
title: 'Systematische Überwachung öffentlicher Bereiche',
|
||||
description: 'Systematische umfangreiche Überwachung öffentlich zugänglicher Bereiche.',
|
||||
gdprRef: 'Art. 35 Abs. 3 lit. c DSGVO',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// KI-SPEZIFISCHE DSFA-TRIGGER
|
||||
// Quelle: Deutsche DSFA-Liste (nicht-öffentlicher Bereich)
|
||||
// =============================================================================
|
||||
|
||||
export const AI_DSFA_TRIGGERS = [
|
||||
{
|
||||
id: 'ai_interaction',
|
||||
title: 'KI zur Steuerung der Interaktion mit Betroffenen',
|
||||
description: 'Einsatz von künstlicher Intelligenz zur Steuerung der Interaktion mit betroffenen Personen.',
|
||||
examples: ['KI-gestützter Kundensupport', 'Chatbots mit personenbezogener Verarbeitung', 'Automatisierte Kommunikation'],
|
||||
requiresDSFA: true,
|
||||
},
|
||||
{
|
||||
id: 'ai_personal_aspects',
|
||||
title: 'KI zur Bewertung persönlicher Aspekte',
|
||||
description: 'Einsatz von künstlicher Intelligenz zur Bewertung persönlicher Aspekte natürlicher Personen.',
|
||||
examples: ['Automatisierte Stimmungsanalyse', 'Verhaltensvorhersagen', 'Persönlichkeitsprofile'],
|
||||
requiresDSFA: true,
|
||||
},
|
||||
{
|
||||
id: 'ai_decision_making',
|
||||
title: 'KI-basierte automatisierte Entscheidungen',
|
||||
description: 'Automatisierte Entscheidungsfindung auf Basis von KI mit erheblicher Auswirkung auf Betroffene.',
|
||||
examples: ['Automatische Kreditvergabe', 'KI-basiertes Recruiting', 'Algorithmenbasierte Preisgestaltung'],
|
||||
requiresDSFA: true,
|
||||
},
|
||||
{
|
||||
id: 'ai_training_personal_data',
|
||||
title: 'KI-Training mit personenbezogenen Daten',
|
||||
description: 'Training von KI-Modellen mit personenbezogenen Daten, insbesondere sensiblen Daten.',
|
||||
examples: ['Training mit Gesundheitsdaten', 'Fine-Tuning mit Kundendaten', 'ML mit biometrischen Daten'],
|
||||
requiresDSFA: true,
|
||||
},
|
||||
]
|
||||
52
admin-compliance/lib/sdk/dsfa/types/enums-constants.ts
Normal file
52
admin-compliance/lib/sdk/dsfa/types/enums-constants.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type DSFAStatus = 'draft' | 'in_review' | 'approved' | 'rejected' | 'needs_update'
|
||||
|
||||
export type DSFARiskLevel = 'low' | 'medium' | 'high' | 'very_high'
|
||||
|
||||
export type DSFARiskCategory = 'confidentiality' | 'integrity' | 'availability' | 'rights_freedoms'
|
||||
|
||||
export type DSFAMitigationType = 'technical' | 'organizational' | 'legal'
|
||||
|
||||
export type DSFAMitigationStatus = 'planned' | 'in_progress' | 'implemented' | 'verified'
|
||||
|
||||
export const DSFA_STATUS_LABELS: Record<DSFAStatus, string> = {
|
||||
draft: 'Entwurf',
|
||||
in_review: 'In Prüfung',
|
||||
approved: 'Genehmigt',
|
||||
rejected: 'Abgelehnt',
|
||||
needs_update: 'Überarbeitung erforderlich',
|
||||
}
|
||||
|
||||
export const DSFA_RISK_LEVEL_LABELS: Record<DSFARiskLevel, string> = {
|
||||
low: 'Niedrig',
|
||||
medium: 'Mittel',
|
||||
high: 'Hoch',
|
||||
very_high: 'Sehr Hoch',
|
||||
}
|
||||
|
||||
export const DSFA_LEGAL_BASES = {
|
||||
consent: 'Art. 6 Abs. 1 lit. a DSGVO - Einwilligung',
|
||||
contract: 'Art. 6 Abs. 1 lit. b DSGVO - Vertrag',
|
||||
legal_obligation: 'Art. 6 Abs. 1 lit. c DSGVO - Rechtliche Verpflichtung',
|
||||
vital_interests: 'Art. 6 Abs. 1 lit. d DSGVO - Lebenswichtige Interessen',
|
||||
public_interest: 'Art. 6 Abs. 1 lit. e DSGVO - Öffentliches Interesse',
|
||||
legitimate_interest: 'Art. 6 Abs. 1 lit. f DSGVO - Berechtigtes Interesse',
|
||||
}
|
||||
|
||||
export const DSFA_AFFECTED_RIGHTS = [
|
||||
{ id: 'right_to_information', label: 'Recht auf Information (Art. 13/14)' },
|
||||
{ id: 'right_of_access', label: 'Auskunftsrecht (Art. 15)' },
|
||||
{ id: 'right_to_rectification', label: 'Recht auf Berichtigung (Art. 16)' },
|
||||
{ id: 'right_to_erasure', label: 'Recht auf Löschung (Art. 17)' },
|
||||
{ id: 'right_to_restriction', label: 'Recht auf Einschränkung (Art. 18)' },
|
||||
{ id: 'right_to_data_portability', label: 'Recht auf Datenübertragbarkeit (Art. 20)' },
|
||||
{ id: 'right_to_object', label: 'Widerspruchsrecht (Art. 21)' },
|
||||
{ id: 'right_not_to_be_profiled', label: 'Recht bzgl. Profiling (Art. 22)' },
|
||||
{ id: 'freedom_of_expression', label: 'Meinungsfreiheit' },
|
||||
{ id: 'freedom_of_association', label: 'Versammlungsfreiheit' },
|
||||
{ id: 'non_discrimination', label: 'Nichtdiskriminierung' },
|
||||
{ id: 'data_security', label: 'Datensicherheit' },
|
||||
]
|
||||
162
admin-compliance/lib/sdk/dsfa/types/helper-functions.ts
Normal file
162
admin-compliance/lib/sdk/dsfa/types/helper-functions.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
import type { DSFARiskLevel } from './enums-constants'
|
||||
import type { DSFAConsultationRequirement, DSFAReviewTrigger, DSFAReviewSchedule } from './sub-types'
|
||||
import type { DSFAAuthorityResource } from './authority-resources'
|
||||
import { DSFA_AUTHORITY_RESOURCES } from './authority-resources'
|
||||
import { WP248_CRITERIA } from './wp248-criteria'
|
||||
import { ART35_ABS3_CASES, AI_DSFA_TRIGGERS } from './dsk-references'
|
||||
|
||||
/**
|
||||
* Prüft anhand der WP248-Kriterien, ob eine DSFA erforderlich ist.
|
||||
* Regel: Bei >= 2 erfüllten Kriterien ist DSFA in den meisten Fällen erforderlich.
|
||||
* @param criteriaIds Array der erfüllten Kriterien-IDs (z.B. ['K1', 'K4'])
|
||||
* @returns Objekt mit Ergebnis und Begründung
|
||||
*/
|
||||
export function checkDSFARequiredByWP248(criteriaIds: string[]): {
|
||||
required: boolean
|
||||
confidence: 'definite' | 'likely' | 'possible' | 'unlikely'
|
||||
reason: string
|
||||
} {
|
||||
const count = criteriaIds.length
|
||||
|
||||
if (count >= 2) {
|
||||
return {
|
||||
required: true,
|
||||
confidence: 'definite',
|
||||
reason: `${count} WP248-Kriterien erfüllt (>= 2). DSFA ist in den meisten Fällen erforderlich.`,
|
||||
}
|
||||
}
|
||||
|
||||
if (count === 1) {
|
||||
return {
|
||||
required: false,
|
||||
confidence: 'possible',
|
||||
reason: '1 WP248-Kriterium erfüllt. DSFA kann je nach Risiko dennoch erforderlich sein. Einzelfallprüfung empfohlen.',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
required: false,
|
||||
confidence: 'unlikely',
|
||||
reason: 'Keine WP248-Kriterien erfüllt. DSFA wahrscheinlich nicht erforderlich, sofern kein Art. 35 Abs. 3 Fall vorliegt.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob eine Konsultation der Aufsichtsbehörde gem. Art. 36 DSGVO erforderlich ist.
|
||||
* Erforderlich wenn: Hohes Restrisiko trotz geplanter Maßnahmen.
|
||||
*/
|
||||
export function checkArt36ConsultationRequired(
|
||||
residualRiskLevel: DSFARiskLevel,
|
||||
mitigationsImplemented: boolean
|
||||
): DSFAConsultationRequirement {
|
||||
const highResidual = residualRiskLevel === 'high' || residualRiskLevel === 'very_high'
|
||||
const consultationRequired = highResidual && mitigationsImplemented
|
||||
|
||||
return {
|
||||
high_residual_risk: highResidual,
|
||||
consultation_required: consultationRequired,
|
||||
consultation_reason: consultationRequired
|
||||
? 'Trotz geplanter Maßnahmen verbleibt ein hohes Restrisiko. Gem. Art. 36 Abs. 1 DSGVO ist vor der Verarbeitung die Aufsichtsbehörde zu konsultieren.'
|
||||
: highResidual
|
||||
? 'Hohes Restrisiko festgestellt, aber Maßnahmen noch nicht vollständig umgesetzt.'
|
||||
: undefined,
|
||||
authority_notified: false,
|
||||
waiting_period_observed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die zuständige Aufsichtsbehörde für ein Bundesland zurück.
|
||||
*/
|
||||
export function getAuthorityResource(stateId: string): DSFAAuthorityResource | undefined {
|
||||
return DSFA_AUTHORITY_RESOURCES.find(r => r.id === stateId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Bundesländer als Auswahlliste zurück.
|
||||
*/
|
||||
export function getFederalStateOptions(): Array<{ value: string; label: string }> {
|
||||
return DSFA_AUTHORITY_RESOURCES.map(r => ({
|
||||
value: r.id,
|
||||
label: r.state,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Review-Trigger eine Aktualisierung der DSFA erfordert.
|
||||
*/
|
||||
export function checkReviewRequired(triggers: DSFAReviewTrigger[]): {
|
||||
required: boolean
|
||||
pendingTriggers: DSFAReviewTrigger[]
|
||||
} {
|
||||
const pendingTriggers = triggers.filter(t => t.review_required && !t.review_completed)
|
||||
return {
|
||||
required: pendingTriggers.length > 0,
|
||||
pendingTriggers,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet das nächste Review-Datum basierend auf dem Schedule.
|
||||
*/
|
||||
export function calculateNextReviewDate(schedule: DSFAReviewSchedule): Date {
|
||||
const lastReview = schedule.last_review_date
|
||||
? new Date(schedule.last_review_date)
|
||||
: new Date()
|
||||
|
||||
const nextReview = new Date(lastReview)
|
||||
nextReview.setMonth(nextReview.getMonth() + schedule.review_frequency_months)
|
||||
return nextReview
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob KI-spezifische DSFA-Trigger erfüllt sind.
|
||||
*/
|
||||
export function checkAIDSFATriggers(
|
||||
aiTriggerIds: string[]
|
||||
): { triggered: boolean; triggers: typeof AI_DSFA_TRIGGERS } {
|
||||
const triggered = AI_DSFA_TRIGGERS.filter(t => aiTriggerIds.includes(t.id))
|
||||
return {
|
||||
triggered: triggered.length > 0,
|
||||
triggers: triggered,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine Checkliste für die Schwellwertanalyse.
|
||||
*/
|
||||
export function generateThresholdAnalysisChecklist(): Array<{
|
||||
category: string
|
||||
items: Array<{ id: string; label: string; description: string }>
|
||||
}> {
|
||||
return [
|
||||
{
|
||||
category: 'WP248 Kriterien (Art.-29-Datenschutzgruppe)',
|
||||
items: WP248_CRITERIA.map(c => ({
|
||||
id: c.id,
|
||||
label: `${c.code}: ${c.title}`,
|
||||
description: c.description,
|
||||
})),
|
||||
},
|
||||
{
|
||||
category: 'Art. 35 Abs. 3 DSGVO Regelbeispiele',
|
||||
items: ART35_ABS3_CASES.map(c => ({
|
||||
id: c.id,
|
||||
label: `lit. ${c.lit}: ${c.title}`,
|
||||
description: c.description,
|
||||
})),
|
||||
},
|
||||
{
|
||||
category: 'KI-spezifische Trigger (Deutsche DSFA-Liste)',
|
||||
items: AI_DSFA_TRIGGERS.map(t => ({
|
||||
id: t.id,
|
||||
label: t.title,
|
||||
description: t.description,
|
||||
})),
|
||||
},
|
||||
]
|
||||
}
|
||||
17
admin-compliance/lib/sdk/dsfa/types/index.ts
Normal file
17
admin-compliance/lib/sdk/dsfa/types/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* DSFA Types - Datenschutz-Folgenabschätzung (Art. 35 DSGVO)
|
||||
*
|
||||
* Barrel re-export of all domain modules.
|
||||
*/
|
||||
|
||||
export * from './sdm-goals'
|
||||
export * from './enums-constants'
|
||||
export * from './wp248-criteria'
|
||||
export * from './authority-resources'
|
||||
export * from './dsk-references'
|
||||
export * from './sub-types'
|
||||
export * from './main-dsfa'
|
||||
export * from './api-types'
|
||||
export * from './ui-helpers'
|
||||
export * from './risk-matrix'
|
||||
export * from './helper-functions'
|
||||
116
admin-compliance/lib/sdk/dsfa/types/main-dsfa.ts
Normal file
116
admin-compliance/lib/sdk/dsfa/types/main-dsfa.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// =============================================================================
|
||||
// MAIN DSFA TYPE
|
||||
// =============================================================================
|
||||
|
||||
import type { AIUseCaseModule } from '../ai-use-case-types'
|
||||
export type { AIUseCaseModule } from '../ai-use-case-types'
|
||||
|
||||
import type { DSFAStatus, DSFARiskLevel } from './enums-constants'
|
||||
import type {
|
||||
DSFARisk,
|
||||
DSFAMitigation,
|
||||
DSFAReviewComment,
|
||||
DSFASectionProgress,
|
||||
DSFAThresholdAnalysis,
|
||||
DSFAStakeholderConsultation,
|
||||
DSFAConsultationRequirement,
|
||||
DSFAReviewSchedule,
|
||||
DSFAReviewTrigger,
|
||||
} from './sub-types'
|
||||
|
||||
export interface DSFA {
|
||||
id: string
|
||||
tenant_id: string
|
||||
namespace_id?: string
|
||||
processing_activity_id?: string
|
||||
assessment_id?: string
|
||||
name: string
|
||||
description: string
|
||||
|
||||
// Section 0: Schwellwertanalyse / Vorabprüfung (NEU - Art. 35 Abs. 1)
|
||||
threshold_analysis?: DSFAThresholdAnalysis
|
||||
wp248_criteria_met?: string[] // IDs der erfüllten WP248-Kriterien (K1-K9)
|
||||
art35_abs3_triggered?: string[] // IDs der ausgelösten Art. 35 Abs. 3 Fälle
|
||||
|
||||
// Section 1: Systematische Beschreibung (Art. 35 Abs. 7 lit. a)
|
||||
processing_description: string
|
||||
processing_purpose: string
|
||||
data_categories: string[]
|
||||
data_subjects: string[]
|
||||
recipients: string[]
|
||||
legal_basis: string
|
||||
legal_basis_details?: string
|
||||
|
||||
// Section 2: Notwendigkeit & Verhältnismäßigkeit (Art. 35 Abs. 7 lit. b)
|
||||
necessity_assessment: string
|
||||
proportionality_assessment: string
|
||||
data_minimization?: string
|
||||
alternatives_considered?: string
|
||||
retention_justification?: string
|
||||
|
||||
// Section 3: Risikobewertung (Art. 35 Abs. 7 lit. c)
|
||||
risks: DSFARisk[]
|
||||
overall_risk_level: DSFARiskLevel
|
||||
risk_score: number
|
||||
affected_rights?: string[]
|
||||
triggered_rule_codes?: string[]
|
||||
|
||||
// KI-spezifische Trigger (NEU)
|
||||
involves_ai?: boolean
|
||||
ai_trigger_ids?: string[] // IDs der ausgelösten KI-Trigger
|
||||
|
||||
// Section 8: KI-Anwendungsfälle (NEU)
|
||||
ai_use_case_modules?: AIUseCaseModule[]
|
||||
|
||||
// Section 4: Abhilfemaßnahmen (Art. 35 Abs. 7 lit. d)
|
||||
mitigations: DSFAMitigation[]
|
||||
tom_references?: string[]
|
||||
residual_risk_level?: DSFARiskLevel // Restrisiko nach Maßnahmen
|
||||
|
||||
// Section 5: Stellungnahme DSB (Art. 35 Abs. 2 + Art. 36)
|
||||
dpo_consulted: boolean
|
||||
dpo_consulted_at?: string
|
||||
dpo_name?: string
|
||||
dpo_opinion?: string
|
||||
dpo_approved?: boolean
|
||||
authority_consulted: boolean
|
||||
authority_consulted_at?: string
|
||||
authority_reference?: string
|
||||
authority_decision?: string
|
||||
|
||||
// Art. 36 Konsultationspflicht (NEU)
|
||||
consultation_requirement?: DSFAConsultationRequirement
|
||||
|
||||
// Betroffenenperspektive (NEU - Art. 35 Abs. 9)
|
||||
stakeholder_consultations?: DSFAStakeholderConsultation[]
|
||||
stakeholder_consultation_not_appropriate?: boolean
|
||||
stakeholder_consultation_not_appropriate_reason?: string
|
||||
|
||||
// Workflow & Approval
|
||||
status: DSFAStatus
|
||||
submitted_for_review_at?: string
|
||||
submitted_by?: string
|
||||
conclusion: string
|
||||
review_comments?: DSFAReviewComment[]
|
||||
|
||||
// Section Progress Tracking
|
||||
section_progress: DSFASectionProgress
|
||||
|
||||
// Fortschreibung / Review (NEU - Art. 35 Abs. 11)
|
||||
review_schedule?: DSFAReviewSchedule
|
||||
review_triggers?: DSFAReviewTrigger[]
|
||||
version: number // DSFA-Version für Fortschreibung
|
||||
previous_version_id?: string
|
||||
|
||||
// Referenzen zu behördlichen Ressourcen
|
||||
federal_state?: string // Bundesland für zuständige Aufsichtsbehörde
|
||||
authority_resource_id?: string // ID aus DSFA_AUTHORITY_RESOURCES
|
||||
|
||||
// Metadata & Audit
|
||||
metadata?: Record<string, unknown>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
approved_by?: string
|
||||
approved_at?: string
|
||||
}
|
||||
35
admin-compliance/lib/sdk/dsfa/types/risk-matrix.ts
Normal file
35
admin-compliance/lib/sdk/dsfa/types/risk-matrix.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// =============================================================================
|
||||
// RISK MATRIX HELPERS
|
||||
// =============================================================================
|
||||
|
||||
import type { DSFARiskLevel } from './enums-constants'
|
||||
|
||||
export interface RiskMatrixCell {
|
||||
likelihood: 'low' | 'medium' | 'high'
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
level: DSFARiskLevel
|
||||
score: number
|
||||
}
|
||||
|
||||
export const RISK_MATRIX: RiskMatrixCell[] = [
|
||||
// Low likelihood
|
||||
{ likelihood: 'low', impact: 'low', level: 'low', score: 10 },
|
||||
{ likelihood: 'low', impact: 'medium', level: 'low', score: 20 },
|
||||
{ likelihood: 'low', impact: 'high', level: 'medium', score: 40 },
|
||||
// Medium likelihood
|
||||
{ likelihood: 'medium', impact: 'low', level: 'low', score: 20 },
|
||||
{ likelihood: 'medium', impact: 'medium', level: 'medium', score: 50 },
|
||||
{ likelihood: 'medium', impact: 'high', level: 'high', score: 70 },
|
||||
// High likelihood
|
||||
{ likelihood: 'high', impact: 'low', level: 'medium', score: 40 },
|
||||
{ likelihood: 'high', impact: 'medium', level: 'high', score: 70 },
|
||||
{ likelihood: 'high', impact: 'high', level: 'very_high', score: 90 },
|
||||
]
|
||||
|
||||
export function calculateRiskLevel(
|
||||
likelihood: 'low' | 'medium' | 'high',
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
): { level: DSFARiskLevel; score: number } {
|
||||
const cell = RISK_MATRIX.find(c => c.likelihood === likelihood && c.impact === impact)
|
||||
return cell ? { level: cell.level, score: cell.score } : { level: 'medium', score: 50 }
|
||||
}
|
||||
50
admin-compliance/lib/sdk/dsfa/types/sdm-goals.ts
Normal file
50
admin-compliance/lib/sdk/dsfa/types/sdm-goals.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// =============================================================================
|
||||
// SDM GEWAEHRLEISTUNGSZIELE (Standard-Datenschutzmodell V2.0)
|
||||
// =============================================================================
|
||||
|
||||
export type SDMGoal =
|
||||
| 'datenminimierung'
|
||||
| 'verfuegbarkeit'
|
||||
| 'integritaet'
|
||||
| 'vertraulichkeit'
|
||||
| 'nichtverkettung'
|
||||
| 'transparenz'
|
||||
| 'intervenierbarkeit'
|
||||
|
||||
export const SDM_GOALS: Record<SDMGoal, { name: string; description: string; article: string }> = {
|
||||
datenminimierung: {
|
||||
name: 'Datenminimierung',
|
||||
description: 'Verarbeitung personenbezogener Daten auf das dem Zweck angemessene, erhebliche und notwendige Mass beschraenken.',
|
||||
article: 'Art. 5 Abs. 1 lit. c DSGVO',
|
||||
},
|
||||
verfuegbarkeit: {
|
||||
name: 'Verfuegbarkeit',
|
||||
description: 'Personenbezogene Daten muessen dem Verantwortlichen zur Verfuegung stehen und ordnungsgemaess im vorgesehenen Prozess verwendet werden koennen.',
|
||||
article: 'Art. 32 Abs. 1 lit. b DSGVO',
|
||||
},
|
||||
integritaet: {
|
||||
name: 'Integritaet',
|
||||
description: 'Personenbezogene Daten bleiben waehrend der Verarbeitung unversehrt, vollstaendig und aktuell.',
|
||||
article: 'Art. 5 Abs. 1 lit. d DSGVO',
|
||||
},
|
||||
vertraulichkeit: {
|
||||
name: 'Vertraulichkeit',
|
||||
description: 'Kein unbefugter Zugriff auf personenbezogene Daten. Nur befugte Personen koennen auf Daten zugreifen.',
|
||||
article: 'Art. 32 Abs. 1 lit. b DSGVO',
|
||||
},
|
||||
nichtverkettung: {
|
||||
name: 'Nichtverkettung',
|
||||
description: 'Personenbezogene Daten duerfen nicht ohne Weiteres fuer einen anderen als den erhobenen Zweck zusammengefuehrt werden (Zweckbindung).',
|
||||
article: 'Art. 5 Abs. 1 lit. b DSGVO',
|
||||
},
|
||||
transparenz: {
|
||||
name: 'Transparenz',
|
||||
description: 'Die Verarbeitung personenbezogener Daten muss fuer Betroffene und Aufsichtsbehoerden nachvollziehbar sein.',
|
||||
article: 'Art. 5 Abs. 1 lit. a DSGVO',
|
||||
},
|
||||
intervenierbarkeit: {
|
||||
name: 'Intervenierbarkeit',
|
||||
description: 'Den Betroffenen werden wirksame Moeglichkeiten der Einflussnahme (Auskunft, Berichtigung, Loeschung, Widerspruch) auf die Verarbeitung gewaehrt.',
|
||||
article: 'Art. 15-21 DSGVO',
|
||||
},
|
||||
}
|
||||
136
admin-compliance/lib/sdk/dsfa/types/sub-types.ts
Normal file
136
admin-compliance/lib/sdk/dsfa/types/sub-types.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// =============================================================================
|
||||
// SUB-TYPES & SECTION PROGRESS
|
||||
// =============================================================================
|
||||
|
||||
import type { DSFARiskCategory, DSFAMitigationType, DSFAMitigationStatus } from './enums-constants'
|
||||
|
||||
export interface DSFARisk {
|
||||
id: string
|
||||
category: DSFARiskCategory
|
||||
description: string
|
||||
likelihood: 'low' | 'medium' | 'high'
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
risk_level: string
|
||||
affected_data: string[]
|
||||
}
|
||||
|
||||
export interface DSFAMitigation {
|
||||
id: string
|
||||
risk_id: string
|
||||
description: string
|
||||
type: DSFAMitigationType
|
||||
status: DSFAMitigationStatus
|
||||
implemented_at?: string
|
||||
verified_at?: string
|
||||
residual_risk: 'low' | 'medium' | 'high'
|
||||
tom_reference?: string
|
||||
responsible_party: string
|
||||
}
|
||||
|
||||
export interface DSFAReviewComment {
|
||||
id: string
|
||||
section: number
|
||||
comment: string
|
||||
created_by: string
|
||||
created_at: string
|
||||
resolved: boolean
|
||||
}
|
||||
|
||||
export interface DSFASectionProgress {
|
||||
section_0_complete: boolean // Schwellwertanalyse
|
||||
section_1_complete: boolean // Systematische Beschreibung
|
||||
section_2_complete: boolean // Notwendigkeit & Verhältnismäßigkeit
|
||||
section_3_complete: boolean // Risikobewertung
|
||||
section_4_complete: boolean // Abhilfemaßnahmen
|
||||
section_5_complete: boolean // Betroffenenperspektive (optional)
|
||||
section_6_complete: boolean // DSB & Behördenkonsultation
|
||||
section_7_complete: boolean // Fortschreibung & Review
|
||||
section_8_complete?: boolean // KI-Anwendungsfälle (optional)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SCHWELLWERTANALYSE / VORABPRÜFUNG (Art. 35 Abs. 1 DSGVO)
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFAThresholdAnalysis {
|
||||
id: string
|
||||
dsfa_id?: string
|
||||
performed_at: string
|
||||
performed_by: string
|
||||
|
||||
// WP248 Kriterien-Bewertung
|
||||
criteria_assessment: Array<{
|
||||
criterion_id: string // K1-K9
|
||||
applies: boolean
|
||||
justification: string
|
||||
}>
|
||||
|
||||
// Art. 35 Abs. 3 Prüfung
|
||||
art35_abs3_assessment: Array<{
|
||||
case_id: string // a, b, c
|
||||
applies: boolean
|
||||
justification: string
|
||||
}>
|
||||
|
||||
// Ergebnis
|
||||
dsfa_required: boolean
|
||||
decision_justification: string
|
||||
|
||||
// Dokumentation der Entscheidung (gem. DSK Kurzpapier Nr. 5)
|
||||
documented: boolean
|
||||
documentation_reference?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BETROFFENENPERSPEKTIVE (Art. 35 Abs. 9 DSGVO)
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFAStakeholderConsultation {
|
||||
id: string
|
||||
stakeholder_type: 'data_subjects' | 'representatives' | 'works_council' | 'other'
|
||||
stakeholder_description: string
|
||||
consultation_date?: string
|
||||
consultation_method: 'survey' | 'interview' | 'workshop' | 'written' | 'other'
|
||||
summary: string
|
||||
concerns_raised: string[]
|
||||
addressed_in_dsfa: boolean
|
||||
response_documentation?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ART. 36 KONSULTATIONSPFLICHT
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFAConsultationRequirement {
|
||||
high_residual_risk: boolean
|
||||
consultation_required: boolean // Art. 36 Abs. 1 DSGVO
|
||||
consultation_reason?: string
|
||||
authority_notified: boolean
|
||||
notification_date?: string
|
||||
authority_response?: string
|
||||
authority_recommendations?: string[]
|
||||
waiting_period_observed: boolean // 8 Wochen gem. Art. 36 Abs. 2
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FORTSCHREIBUNG / REVIEW (Art. 35 Abs. 11 DSGVO)
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFAReviewTrigger {
|
||||
id: string
|
||||
trigger_type: 'scheduled' | 'risk_change' | 'new_technology' | 'new_purpose' | 'incident' | 'regulatory' | 'other'
|
||||
description: string
|
||||
detected_at: string
|
||||
detected_by: string
|
||||
review_required: boolean
|
||||
review_completed: boolean
|
||||
review_date?: string
|
||||
changes_made: string[]
|
||||
}
|
||||
|
||||
export interface DSFAReviewSchedule {
|
||||
next_review_date: string
|
||||
review_frequency_months: number
|
||||
last_review_date?: string
|
||||
review_responsible: string
|
||||
}
|
||||
97
admin-compliance/lib/sdk/dsfa/types/ui-helpers.ts
Normal file
97
admin-compliance/lib/sdk/dsfa/types/ui-helpers.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// =============================================================================
|
||||
// HELPER TYPES FOR UI
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFASectionConfig {
|
||||
number: number
|
||||
title: string
|
||||
titleDE: string
|
||||
description: string
|
||||
gdprRef: string
|
||||
fields: string[]
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export const DSFA_SECTIONS: DSFASectionConfig[] = [
|
||||
{
|
||||
number: 0,
|
||||
title: 'Threshold Analysis',
|
||||
titleDE: 'Schwellwertanalyse',
|
||||
description: 'Prüfen Sie anhand der WP248-Kriterien und Art. 35 Abs. 3, ob eine DSFA erforderlich ist. Die Entscheidung ist zu dokumentieren.',
|
||||
gdprRef: 'Art. 35 Abs. 1 DSGVO, WP248 rev.01',
|
||||
fields: ['threshold_analysis', 'wp248_criteria_met', 'art35_abs3_triggered'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 1,
|
||||
title: 'Processing Description',
|
||||
titleDE: 'Systematische Beschreibung',
|
||||
description: 'Beschreiben Sie die geplante Verarbeitung, ihren Zweck, die Datenkategorien und Rechtsgrundlage.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. a DSGVO',
|
||||
fields: ['processing_description', 'processing_purpose', 'data_categories', 'data_subjects', 'recipients', 'legal_basis'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Necessity & Proportionality',
|
||||
titleDE: 'Notwendigkeit & Verhältnismäßigkeit',
|
||||
description: 'Begründen Sie, warum die Verarbeitung notwendig ist und welche Alternativen geprüft wurden.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. b DSGVO',
|
||||
fields: ['necessity_assessment', 'proportionality_assessment', 'data_minimization', 'alternatives_considered'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Risk Assessment',
|
||||
titleDE: 'Risikobewertung',
|
||||
description: 'Identifizieren und bewerten Sie die Risiken für die Rechte und Freiheiten der Betroffenen.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. c DSGVO',
|
||||
fields: ['risks', 'overall_risk_level', 'risk_score', 'affected_rights', 'involves_ai', 'ai_trigger_ids'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: 'Mitigation Measures',
|
||||
titleDE: 'Abhilfemaßnahmen',
|
||||
description: 'Definieren Sie technische und organisatorische Maßnahmen zur Risikominimierung und bewerten Sie das Restrisiko.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. d DSGVO',
|
||||
fields: ['mitigations', 'tom_references', 'residual_risk_level'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
title: 'Stakeholder Consultation',
|
||||
titleDE: 'Betroffenenperspektive',
|
||||
description: 'Dokumentieren Sie, ob und wie die Standpunkte der Betroffenen eingeholt wurden (z.B. Betriebsrat, Nutzerumfragen).',
|
||||
gdprRef: 'Art. 35 Abs. 9 DSGVO',
|
||||
fields: ['stakeholder_consultations', 'stakeholder_consultation_not_appropriate', 'stakeholder_consultation_not_appropriate_reason'],
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
number: 6,
|
||||
title: 'DPO Opinion & Authority Consultation',
|
||||
titleDE: 'DSB-Stellungnahme & Behördenkonsultation',
|
||||
description: 'Dokumentieren Sie die Konsultation des DSB und prüfen Sie, ob bei hohem Restrisiko eine Behördenkonsultation erforderlich ist.',
|
||||
gdprRef: 'Art. 35 Abs. 2, Art. 36 DSGVO',
|
||||
fields: ['dpo_consulted', 'dpo_opinion', 'consultation_requirement', 'authority_consulted', 'authority_reference'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 7,
|
||||
title: 'Review & Maintenance',
|
||||
titleDE: 'Fortschreibung & Review',
|
||||
description: 'Planen Sie regelmäßige Überprüfungen und dokumentieren Sie Änderungen, die eine Aktualisierung der DSFA erfordern.',
|
||||
gdprRef: 'Art. 35 Abs. 11 DSGVO',
|
||||
fields: ['review_schedule', 'review_triggers', 'version'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 8,
|
||||
title: 'AI Use Cases',
|
||||
titleDE: 'KI-Anwendungsfälle',
|
||||
description: 'Modulare Anhänge für KI-spezifische Risiken und Maßnahmen nach Art. 22 DSGVO und EU AI Act.',
|
||||
gdprRef: 'Art. 35 DSGVO, Art. 22 DSGVO, EU AI Act',
|
||||
fields: ['ai_use_case_modules'],
|
||||
required: false,
|
||||
},
|
||||
]
|
||||
91
admin-compliance/lib/sdk/dsfa/types/wp248-criteria.ts
Normal file
91
admin-compliance/lib/sdk/dsfa/types/wp248-criteria.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// =============================================================================
|
||||
// WP248 REV.01 KRITERIEN (Schwellwertanalyse)
|
||||
// Quelle: Artikel-29-Datenschutzgruppe, bestätigt durch EDSA
|
||||
// =============================================================================
|
||||
|
||||
export interface WP248Criterion {
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
description: string
|
||||
examples: string[]
|
||||
gdprRef?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* WP248 rev.01 Kriterien zur Bestimmung der DSFA-Pflicht
|
||||
* Regel: Bei >= 2 erfüllten Kriterien ist DSFA in den meisten Fällen erforderlich
|
||||
*/
|
||||
export const WP248_CRITERIA: WP248Criterion[] = [
|
||||
{
|
||||
id: 'scoring_profiling',
|
||||
code: 'K1',
|
||||
title: 'Bewertung oder Scoring',
|
||||
description: 'Einschließlich Profiling und Prognosen, insbesondere zu Arbeitsleistung, wirtschaftlicher Lage, Gesundheit, persönlichen Vorlieben, Zuverlässigkeit, Verhalten, Aufenthaltsort oder Ortswechsel.',
|
||||
examples: ['Bonitätsprüfung', 'Leistungsbeurteilung', 'Verhaltensanalyse'],
|
||||
gdprRef: 'Art. 35 Abs. 3 lit. a DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'automated_decision',
|
||||
code: 'K2',
|
||||
title: 'Automatisierte Entscheidungsfindung mit Rechtswirkung',
|
||||
description: 'Automatisierte Verarbeitung, die als Grundlage für Entscheidungen dient, die Rechtswirkung gegenüber natürlichen Personen entfalten oder diese erheblich beeinträchtigen.',
|
||||
examples: ['Automatische Kreditvergabe', 'Automatische Bewerbungsablehnung', 'Algorithmenbasierte Preisgestaltung'],
|
||||
gdprRef: 'Art. 22 DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'systematic_monitoring',
|
||||
code: 'K3',
|
||||
title: 'Systematische Überwachung',
|
||||
description: 'Verarbeitung zur Beobachtung, Überwachung oder Kontrolle von betroffenen Personen, einschließlich Datenerhebung über Netzwerke oder systematische Überwachung öffentlicher Bereiche.',
|
||||
examples: ['Videoüberwachung', 'WLAN-Tracking', 'GPS-Ortung', 'Mitarbeiterüberwachung'],
|
||||
gdprRef: 'Art. 35 Abs. 3 lit. c DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'sensitive_data',
|
||||
code: 'K4',
|
||||
title: 'Sensible Daten oder höchst persönliche Daten',
|
||||
description: 'Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9), strafrechtlicher Daten (Art. 10) oder anderer höchst persönlicher Daten wie Kommunikationsinhalte, Standortdaten, Finanzinformationen.',
|
||||
examples: ['Gesundheitsdaten', 'Biometrische Daten', 'Genetische Daten', 'Politische Meinungen', 'Gewerkschaftszugehörigkeit'],
|
||||
gdprRef: 'Art. 9, Art. 10 DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'large_scale',
|
||||
code: 'K5',
|
||||
title: 'Datenverarbeitung in großem Umfang',
|
||||
description: 'Berücksichtigt werden: Zahl der Betroffenen, Datenmenge, Dauer der Verarbeitung, geografische Reichweite.',
|
||||
examples: ['Landesweite Datenbanken', 'Millionen von Nutzern', 'Mehrjährige Speicherung'],
|
||||
gdprRef: 'Erwägungsgrund 91 DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'matching_combining',
|
||||
code: 'K6',
|
||||
title: 'Abgleichen oder Zusammenführen von Datensätzen',
|
||||
description: 'Datensätze aus verschiedenen Quellen, die für unterschiedliche Zwecke und/oder von verschiedenen Verantwortlichen erhoben wurden, werden abgeglichen oder zusammengeführt.',
|
||||
examples: ['Data Warehousing', 'Big Data Analytics', 'Zusammenführung von Online-/Offline-Daten'],
|
||||
},
|
||||
{
|
||||
id: 'vulnerable_subjects',
|
||||
code: 'K7',
|
||||
title: 'Daten zu schutzbedürftigen Betroffenen',
|
||||
description: 'Verarbeitung von Daten schutzbedürftiger Personen, bei denen ein Ungleichgewicht zwischen Betroffenem und Verantwortlichem besteht.',
|
||||
examples: ['Kinder/Minderjährige', 'Arbeitnehmer', 'Patienten', 'Ältere Menschen', 'Asylbewerber'],
|
||||
gdprRef: 'Erwägungsgrund 75 DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'innovative_technology',
|
||||
code: 'K8',
|
||||
title: 'Innovative Nutzung oder Anwendung neuer technologischer oder organisatorischer Lösungen',
|
||||
description: 'Einsatz neuer Technologien kann neue Formen der Datenerhebung und -nutzung mit sich bringen, möglicherweise mit hohem Risiko für Rechte und Freiheiten.',
|
||||
examples: ['Künstliche Intelligenz', 'Machine Learning', 'IoT-Geräte', 'Biometrische Erkennung', 'Blockchain'],
|
||||
gdprRef: 'Erwägungsgrund 89, 91 DSGVO',
|
||||
},
|
||||
{
|
||||
id: 'preventing_rights',
|
||||
code: 'K9',
|
||||
title: 'Verarbeitung, die Betroffene an der Ausübung eines Rechts oder der Nutzung einer Dienstleistung hindert',
|
||||
description: 'Verarbeitungsvorgänge, die darauf abzielen, einer Person den Zugang zu einer Dienstleistung oder den Abschluss eines Vertrags zu ermöglichen oder zu verweigern.',
|
||||
examples: ['Zugang zu Sozialleistungen', 'Kreditvergabe', 'Versicherungsabschluss'],
|
||||
gdprRef: 'Art. 22 DSGVO',
|
||||
},
|
||||
]
|
||||
146
admin-compliance/lib/sdk/dsr/api-crud.ts
Normal file
146
admin-compliance/lib/sdk/dsr/api-crud.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* DSR API CRUD Operations
|
||||
*
|
||||
* List, create, read, update operations for DSR requests.
|
||||
*/
|
||||
|
||||
import {
|
||||
DSRRequest,
|
||||
DSRCreateRequest,
|
||||
DSRStatistics,
|
||||
} from './types'
|
||||
import { BackendDSR, transformBackendDSR, getSdkHeaders } from './api-types'
|
||||
|
||||
// =============================================================================
|
||||
// LIST & STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch DSR list from compliance backend via proxy
|
||||
*/
|
||||
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
|
||||
const [listRes, statsRes] = await Promise.all([
|
||||
fetch('/api/sdk/v1/compliance/dsr?limit=100', { headers: getSdkHeaders() }),
|
||||
fetch('/api/sdk/v1/compliance/dsr/stats', { headers: getSdkHeaders() }),
|
||||
])
|
||||
|
||||
if (!listRes.ok) {
|
||||
throw new Error(`HTTP ${listRes.status}`)
|
||||
}
|
||||
|
||||
const listData = await listRes.json()
|
||||
const backendDSRs: BackendDSR[] = listData.requests || []
|
||||
const requests = backendDSRs.map(transformBackendDSR)
|
||||
|
||||
let statistics: DSRStatistics
|
||||
if (statsRes.ok) {
|
||||
const statsData = await statsRes.json()
|
||||
statistics = {
|
||||
total: statsData.total || 0,
|
||||
byStatus: statsData.by_status || { intake: 0, identity_verification: 0, processing: 0, completed: 0, rejected: 0, cancelled: 0 },
|
||||
byType: statsData.by_type || { access: 0, rectification: 0, erasure: 0, restriction: 0, portability: 0, objection: 0 },
|
||||
overdue: statsData.overdue || 0,
|
||||
dueThisWeek: statsData.due_this_week || 0,
|
||||
averageProcessingDays: statsData.average_processing_days || 0,
|
||||
completedThisMonth: statsData.completed_this_month || 0,
|
||||
}
|
||||
} else {
|
||||
statistics = {
|
||||
total: requests.length,
|
||||
byStatus: {
|
||||
intake: requests.filter(r => r.status === 'intake').length,
|
||||
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
|
||||
processing: requests.filter(r => r.status === 'processing').length,
|
||||
completed: requests.filter(r => r.status === 'completed').length,
|
||||
rejected: requests.filter(r => r.status === 'rejected').length,
|
||||
cancelled: requests.filter(r => r.status === 'cancelled').length,
|
||||
},
|
||||
byType: {
|
||||
access: requests.filter(r => r.type === 'access').length,
|
||||
rectification: requests.filter(r => r.type === 'rectification').length,
|
||||
erasure: requests.filter(r => r.type === 'erasure').length,
|
||||
restriction: requests.filter(r => r.type === 'restriction').length,
|
||||
portability: requests.filter(r => r.type === 'portability').length,
|
||||
objection: requests.filter(r => r.type === 'objection').length,
|
||||
},
|
||||
overdue: 0,
|
||||
dueThisWeek: 0,
|
||||
averageProcessingDays: 0,
|
||||
completedThisMonth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return { requests, statistics }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SINGLE RESOURCE OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create a new DSR via compliance backend
|
||||
*/
|
||||
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
||||
const body = {
|
||||
request_type: request.type,
|
||||
requester_name: request.requester.name,
|
||||
requester_email: request.requester.email,
|
||||
requester_phone: request.requester.phone || null,
|
||||
requester_address: request.requester.address || null,
|
||||
requester_customer_id: request.requester.customerId || null,
|
||||
source: request.source,
|
||||
source_details: request.sourceDetails || null,
|
||||
request_text: request.requestText || '',
|
||||
priority: request.priority || 'normal',
|
||||
}
|
||||
const res = await fetch('/api/sdk/v1/compliance/dsr', {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single DSR by ID from compliance backend
|
||||
*/
|
||||
export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) {
|
||||
return null
|
||||
}
|
||||
const data = await res.json()
|
||||
if (!data || !data.id) return null
|
||||
return transformBackendDSR(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DSR status via compliance backend
|
||||
*/
|
||||
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DSR fields (priority, notes, etc.)
|
||||
*/
|
||||
export async function updateDSR(id: string, data: Record<string, any>): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
259
admin-compliance/lib/sdk/dsr/api-mock.ts
Normal file
259
admin-compliance/lib/sdk/dsr/api-mock.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* DSR Mock Data
|
||||
*
|
||||
* Mock DSR requests and statistics for development/testing fallback.
|
||||
*/
|
||||
|
||||
import { DSRRequest, DSRStatistics } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function createMockDSRList(): DSRRequest[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'dsr-001',
|
||||
referenceNumber: 'DSR-2025-000001',
|
||||
type: 'access',
|
||||
status: 'intake',
|
||||
priority: 'high',
|
||||
requester: {
|
||||
name: 'Max Mustermann',
|
||||
email: 'max.mustermann@example.de'
|
||||
},
|
||||
source: 'web_form',
|
||||
sourceDetails: 'Kontaktformular auf breakpilot.de',
|
||||
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: { verified: false },
|
||||
assignment: { assignedTo: null },
|
||||
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-002',
|
||||
referenceNumber: 'DSR-2025-000002',
|
||||
type: 'erasure',
|
||||
status: 'identity_verification',
|
||||
priority: 'high',
|
||||
requester: {
|
||||
name: 'Anna Schmidt',
|
||||
email: 'anna.schmidt@example.de',
|
||||
phone: '+49 170 1234567'
|
||||
},
|
||||
source: 'email',
|
||||
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
|
||||
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: { verified: false },
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-003',
|
||||
referenceNumber: 'DSR-2025-000003',
|
||||
type: 'rectification',
|
||||
status: 'processing',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Peter Meier',
|
||||
email: 'peter.meier@example.de'
|
||||
},
|
||||
source: 'email',
|
||||
requestText: 'Meine Adresse ist falsch gespeichert.',
|
||||
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'existing_account',
|
||||
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
rectificationDetails: {
|
||||
fieldsToCorrect: [
|
||||
{
|
||||
field: 'Adresse',
|
||||
currentValue: 'Musterstr. 1, 12345 Berlin',
|
||||
requestedValue: 'Musterstr. 10, 12345 Berlin',
|
||||
corrected: false
|
||||
}
|
||||
]
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-004',
|
||||
referenceNumber: 'DSR-2025-000004',
|
||||
type: 'portability',
|
||||
status: 'processing',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Lisa Weber',
|
||||
email: 'lisa.weber@example.de'
|
||||
},
|
||||
source: 'web_form',
|
||||
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'id_document',
|
||||
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'IT Team',
|
||||
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
notes: 'JSON-Export wird vorbereitet',
|
||||
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-005',
|
||||
referenceNumber: 'DSR-2025-000005',
|
||||
type: 'objection',
|
||||
status: 'rejected',
|
||||
priority: 'low',
|
||||
requester: {
|
||||
name: 'Thomas Klein',
|
||||
email: 'thomas.klein@example.de'
|
||||
},
|
||||
source: 'letter',
|
||||
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
|
||||
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'postal',
|
||||
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'Rechtsabteilung',
|
||||
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
objectionDetails: {
|
||||
processingPurpose: 'Marketing',
|
||||
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
|
||||
objectionGrounds: 'Keine konkreten Gruende genannt',
|
||||
decision: 'rejected',
|
||||
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
|
||||
decisionBy: 'Rechtsabteilung',
|
||||
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
|
||||
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-006',
|
||||
referenceNumber: 'DSR-2025-000006',
|
||||
type: 'access',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Sarah Braun',
|
||||
email: 'sarah.braun@example.de'
|
||||
},
|
||||
source: 'email',
|
||||
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'id_document',
|
||||
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
dataExport: {
|
||||
format: 'pdf',
|
||||
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
generatedBy: 'DSB Mueller',
|
||||
fileName: 'datenauskunft_sarah_braun.pdf',
|
||||
fileSize: 245000,
|
||||
includesThirdPartyData: false
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function createMockStatistics(): DSRStatistics {
|
||||
return {
|
||||
total: 6,
|
||||
byStatus: {
|
||||
intake: 1,
|
||||
identity_verification: 1,
|
||||
processing: 2,
|
||||
completed: 1,
|
||||
rejected: 1,
|
||||
cancelled: 0
|
||||
},
|
||||
byType: {
|
||||
access: 2,
|
||||
rectification: 1,
|
||||
erasure: 1,
|
||||
restriction: 0,
|
||||
portability: 1,
|
||||
objection: 1
|
||||
},
|
||||
overdue: 0,
|
||||
dueThisWeek: 2,
|
||||
averageProcessingDays: 18,
|
||||
completedThisMonth: 1
|
||||
}
|
||||
}
|
||||
133
admin-compliance/lib/sdk/dsr/api-types.ts
Normal file
133
admin-compliance/lib/sdk/dsr/api-types.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* DSR API Types & Transform
|
||||
*
|
||||
* Backend DSR type definition and transformation to frontend DSRRequest format.
|
||||
*/
|
||||
|
||||
import { DSRRequest } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// BACKEND TYPE
|
||||
// =============================================================================
|
||||
|
||||
export interface BackendDSR {
|
||||
id: string
|
||||
tenant_id: string
|
||||
request_number: string
|
||||
request_type: string
|
||||
status: string
|
||||
priority: string
|
||||
requester_name: string
|
||||
requester_email: string
|
||||
requester_phone?: string
|
||||
requester_address?: string
|
||||
requester_customer_id?: string
|
||||
source: string
|
||||
source_details?: string
|
||||
request_text?: string
|
||||
notes?: string
|
||||
internal_notes?: string
|
||||
received_at: string
|
||||
deadline_at: string
|
||||
extended_deadline_at?: string
|
||||
extension_reason?: string
|
||||
extension_approved_by?: string
|
||||
extension_approved_at?: string
|
||||
identity_verified: boolean
|
||||
verification_method?: string
|
||||
verified_at?: string
|
||||
verified_by?: string
|
||||
verification_notes?: string
|
||||
verification_document_ref?: string
|
||||
assigned_to?: string
|
||||
assigned_at?: string
|
||||
assigned_by?: string
|
||||
completed_at?: string
|
||||
completion_notes?: string
|
||||
rejection_reason?: string
|
||||
rejection_legal_basis?: string
|
||||
erasure_checklist?: any[]
|
||||
data_export?: any
|
||||
rectification_details?: any
|
||||
objection_details?: any
|
||||
affected_systems?: string[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRANSFORM
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Transform flat backend DSR to nested SDK DSRRequest format.
|
||||
* New compliance backend already uses the same status names as frontend types.
|
||||
*/
|
||||
export function transformBackendDSR(b: BackendDSR): DSRRequest {
|
||||
return {
|
||||
id: b.id,
|
||||
referenceNumber: b.request_number,
|
||||
type: b.request_type as DSRRequest['type'],
|
||||
status: (b.status as DSRRequest['status']) || 'intake',
|
||||
priority: (b.priority as DSRRequest['priority']) || 'normal',
|
||||
requester: {
|
||||
name: b.requester_name,
|
||||
email: b.requester_email,
|
||||
phone: b.requester_phone,
|
||||
address: b.requester_address,
|
||||
customerId: b.requester_customer_id,
|
||||
},
|
||||
source: (b.source as DSRRequest['source']) || 'email',
|
||||
sourceDetails: b.source_details,
|
||||
requestText: b.request_text,
|
||||
receivedAt: b.received_at,
|
||||
deadline: {
|
||||
originalDeadline: b.deadline_at,
|
||||
currentDeadline: b.extended_deadline_at || b.deadline_at,
|
||||
extended: !!b.extended_deadline_at,
|
||||
extensionReason: b.extension_reason,
|
||||
extensionApprovedBy: b.extension_approved_by,
|
||||
extensionApprovedAt: b.extension_approved_at,
|
||||
},
|
||||
completedAt: b.completed_at,
|
||||
identityVerification: {
|
||||
verified: b.identity_verified,
|
||||
method: b.verification_method as any,
|
||||
verifiedAt: b.verified_at,
|
||||
verifiedBy: b.verified_by,
|
||||
notes: b.verification_notes,
|
||||
documentRef: b.verification_document_ref,
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: b.assigned_to || null,
|
||||
assignedAt: b.assigned_at,
|
||||
assignedBy: b.assigned_by,
|
||||
},
|
||||
notes: b.notes,
|
||||
internalNotes: b.internal_notes,
|
||||
erasureChecklist: b.erasure_checklist ? { items: b.erasure_checklist, canProceedWithErasure: true } : undefined,
|
||||
dataExport: b.data_export && Object.keys(b.data_export).length > 0 ? b.data_export : undefined,
|
||||
rectificationDetails: b.rectification_details && Object.keys(b.rectification_details).length > 0 ? b.rectification_details : undefined,
|
||||
objectionDetails: b.objection_details && Object.keys(b.objection_details).length > 0 ? b.objection_details : undefined,
|
||||
createdAt: b.created_at,
|
||||
createdBy: b.created_by || 'system',
|
||||
updatedAt: b.updated_at,
|
||||
updatedBy: b.updated_by,
|
||||
tenantId: b.tenant_id,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SHARED HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function getSdkHeaders(): HeadersInit {
|
||||
if (typeof window === 'undefined') return {}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||
}
|
||||
}
|
||||
161
admin-compliance/lib/sdk/dsr/api-workflow.ts
Normal file
161
admin-compliance/lib/sdk/dsr/api-workflow.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* DSR API Workflow Actions
|
||||
*
|
||||
* Workflow operations: identity verification, assignment, deadline extension,
|
||||
* completion, rejection, communications, exception checks, and history.
|
||||
*/
|
||||
|
||||
import { DSRRequest } from './types'
|
||||
import { transformBackendDSR, getSdkHeaders } from './api-types'
|
||||
|
||||
// =============================================================================
|
||||
// WORKFLOW ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Verify identity of DSR requester
|
||||
*/
|
||||
export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign DSR to a user
|
||||
*/
|
||||
export async function assignDSR(id: string, assigneeId: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ assignee_id: assigneeId }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend DSR deadline (Art. 12 Abs. 3 DSGVO)
|
||||
*/
|
||||
export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ reason, days }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a DSR
|
||||
*/
|
||||
export async function completeDSR(id: string, summary?: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ summary }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a DSR with legal basis
|
||||
*/
|
||||
export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ reason, legal_basis: legalBasis }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch communications for a DSR
|
||||
*/
|
||||
export async function fetchDSRCommunications(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a communication for a DSR
|
||||
*/
|
||||
export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise<any> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXCEPTION CHECKS (Art. 17)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch exception checks for an erasure DSR
|
||||
*/
|
||||
export async function fetchDSRExceptionChecks(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Art. 17(3) exception checks for an erasure DSR
|
||||
*/
|
||||
export async function initDSRExceptionChecks(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single exception check
|
||||
*/
|
||||
export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise<any> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, {
|
||||
method: 'PUT',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HISTORY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch status change history for a DSR
|
||||
*/
|
||||
export async function fetchDSRHistory(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
@@ -1,669 +1,38 @@
|
||||
/**
|
||||
* DSR API Client
|
||||
*
|
||||
* API client for Data Subject Request management.
|
||||
* Connects to the native compliance backend (Python/FastAPI).
|
||||
* DSR API Client — Barrel re-exports
|
||||
* Preserves the original public API so existing imports work unchanged.
|
||||
*/
|
||||
|
||||
import {
|
||||
DSRRequest,
|
||||
DSRCreateRequest,
|
||||
DSRStatistics,
|
||||
} from './types'
|
||||
// Types & transform
|
||||
export { transformBackendDSR, getSdkHeaders } from './api-types'
|
||||
export type { BackendDSR } from './api-types'
|
||||
|
||||
// =============================================================================
|
||||
// SDK API FUNCTIONS (via Next.js proxy to compliance backend)
|
||||
// =============================================================================
|
||||
// CRUD operations
|
||||
export {
|
||||
fetchSDKDSRList,
|
||||
createSDKDSR,
|
||||
fetchSDKDSR,
|
||||
updateSDKDSRStatus,
|
||||
updateDSR,
|
||||
} from './api-crud'
|
||||
|
||||
interface BackendDSR {
|
||||
id: string
|
||||
tenant_id: string
|
||||
request_number: string
|
||||
request_type: string
|
||||
status: string
|
||||
priority: string
|
||||
requester_name: string
|
||||
requester_email: string
|
||||
requester_phone?: string
|
||||
requester_address?: string
|
||||
requester_customer_id?: string
|
||||
source: string
|
||||
source_details?: string
|
||||
request_text?: string
|
||||
notes?: string
|
||||
internal_notes?: string
|
||||
received_at: string
|
||||
deadline_at: string
|
||||
extended_deadline_at?: string
|
||||
extension_reason?: string
|
||||
extension_approved_by?: string
|
||||
extension_approved_at?: string
|
||||
identity_verified: boolean
|
||||
verification_method?: string
|
||||
verified_at?: string
|
||||
verified_by?: string
|
||||
verification_notes?: string
|
||||
verification_document_ref?: string
|
||||
assigned_to?: string
|
||||
assigned_at?: string
|
||||
assigned_by?: string
|
||||
completed_at?: string
|
||||
completion_notes?: string
|
||||
rejection_reason?: string
|
||||
rejection_legal_basis?: string
|
||||
erasure_checklist?: any[]
|
||||
data_export?: any
|
||||
rectification_details?: any
|
||||
objection_details?: any
|
||||
affected_systems?: string[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
}
|
||||
// Workflow actions
|
||||
export {
|
||||
verifyDSRIdentity,
|
||||
assignDSR,
|
||||
extendDSRDeadline,
|
||||
completeDSR,
|
||||
rejectDSR,
|
||||
fetchDSRCommunications,
|
||||
sendDSRCommunication,
|
||||
fetchDSRExceptionChecks,
|
||||
initDSRExceptionChecks,
|
||||
updateDSRExceptionCheck,
|
||||
fetchDSRHistory,
|
||||
} from './api-workflow'
|
||||
|
||||
/**
|
||||
* Transform flat backend DSR to nested SDK DSRRequest format.
|
||||
* New compliance backend already uses the same status names as frontend types.
|
||||
*/
|
||||
export function transformBackendDSR(b: BackendDSR): DSRRequest {
|
||||
return {
|
||||
id: b.id,
|
||||
referenceNumber: b.request_number,
|
||||
type: b.request_type as DSRRequest['type'],
|
||||
status: (b.status as DSRRequest['status']) || 'intake',
|
||||
priority: (b.priority as DSRRequest['priority']) || 'normal',
|
||||
requester: {
|
||||
name: b.requester_name,
|
||||
email: b.requester_email,
|
||||
phone: b.requester_phone,
|
||||
address: b.requester_address,
|
||||
customerId: b.requester_customer_id,
|
||||
},
|
||||
source: (b.source as DSRRequest['source']) || 'email',
|
||||
sourceDetails: b.source_details,
|
||||
requestText: b.request_text,
|
||||
receivedAt: b.received_at,
|
||||
deadline: {
|
||||
originalDeadline: b.deadline_at,
|
||||
currentDeadline: b.extended_deadline_at || b.deadline_at,
|
||||
extended: !!b.extended_deadline_at,
|
||||
extensionReason: b.extension_reason,
|
||||
extensionApprovedBy: b.extension_approved_by,
|
||||
extensionApprovedAt: b.extension_approved_at,
|
||||
},
|
||||
completedAt: b.completed_at,
|
||||
identityVerification: {
|
||||
verified: b.identity_verified,
|
||||
method: b.verification_method as any,
|
||||
verifiedAt: b.verified_at,
|
||||
verifiedBy: b.verified_by,
|
||||
notes: b.verification_notes,
|
||||
documentRef: b.verification_document_ref,
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: b.assigned_to || null,
|
||||
assignedAt: b.assigned_at,
|
||||
assignedBy: b.assigned_by,
|
||||
},
|
||||
notes: b.notes,
|
||||
internalNotes: b.internal_notes,
|
||||
erasureChecklist: b.erasure_checklist ? { items: b.erasure_checklist, canProceedWithErasure: true } : undefined,
|
||||
dataExport: b.data_export && Object.keys(b.data_export).length > 0 ? b.data_export : undefined,
|
||||
rectificationDetails: b.rectification_details && Object.keys(b.rectification_details).length > 0 ? b.rectification_details : undefined,
|
||||
objectionDetails: b.objection_details && Object.keys(b.objection_details).length > 0 ? b.objection_details : undefined,
|
||||
createdAt: b.created_at,
|
||||
createdBy: b.created_by || 'system',
|
||||
updatedAt: b.updated_at,
|
||||
updatedBy: b.updated_by,
|
||||
tenantId: b.tenant_id,
|
||||
}
|
||||
}
|
||||
|
||||
function getSdkHeaders(): HeadersInit {
|
||||
if (typeof window === 'undefined') return {}
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch DSR list from compliance backend via proxy
|
||||
*/
|
||||
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
|
||||
// Fetch list and stats in parallel
|
||||
const [listRes, statsRes] = await Promise.all([
|
||||
fetch('/api/sdk/v1/compliance/dsr?limit=100', { headers: getSdkHeaders() }),
|
||||
fetch('/api/sdk/v1/compliance/dsr/stats', { headers: getSdkHeaders() }),
|
||||
])
|
||||
|
||||
if (!listRes.ok) {
|
||||
throw new Error(`HTTP ${listRes.status}`)
|
||||
}
|
||||
|
||||
const listData = await listRes.json()
|
||||
const backendDSRs: BackendDSR[] = listData.requests || []
|
||||
const requests = backendDSRs.map(transformBackendDSR)
|
||||
|
||||
let statistics: DSRStatistics
|
||||
if (statsRes.ok) {
|
||||
const statsData = await statsRes.json()
|
||||
statistics = {
|
||||
total: statsData.total || 0,
|
||||
byStatus: statsData.by_status || { intake: 0, identity_verification: 0, processing: 0, completed: 0, rejected: 0, cancelled: 0 },
|
||||
byType: statsData.by_type || { access: 0, rectification: 0, erasure: 0, restriction: 0, portability: 0, objection: 0 },
|
||||
overdue: statsData.overdue || 0,
|
||||
dueThisWeek: statsData.due_this_week || 0,
|
||||
averageProcessingDays: statsData.average_processing_days || 0,
|
||||
completedThisMonth: statsData.completed_this_month || 0,
|
||||
}
|
||||
} else {
|
||||
// Fallback: calculate locally
|
||||
const now = new Date()
|
||||
statistics = {
|
||||
total: requests.length,
|
||||
byStatus: {
|
||||
intake: requests.filter(r => r.status === 'intake').length,
|
||||
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
|
||||
processing: requests.filter(r => r.status === 'processing').length,
|
||||
completed: requests.filter(r => r.status === 'completed').length,
|
||||
rejected: requests.filter(r => r.status === 'rejected').length,
|
||||
cancelled: requests.filter(r => r.status === 'cancelled').length,
|
||||
},
|
||||
byType: {
|
||||
access: requests.filter(r => r.type === 'access').length,
|
||||
rectification: requests.filter(r => r.type === 'rectification').length,
|
||||
erasure: requests.filter(r => r.type === 'erasure').length,
|
||||
restriction: requests.filter(r => r.type === 'restriction').length,
|
||||
portability: requests.filter(r => r.type === 'portability').length,
|
||||
objection: requests.filter(r => r.type === 'objection').length,
|
||||
},
|
||||
overdue: 0,
|
||||
dueThisWeek: 0,
|
||||
averageProcessingDays: 0,
|
||||
completedThisMonth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return { requests, statistics }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DSR via compliance backend
|
||||
*/
|
||||
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
||||
const body = {
|
||||
request_type: request.type,
|
||||
requester_name: request.requester.name,
|
||||
requester_email: request.requester.email,
|
||||
requester_phone: request.requester.phone || null,
|
||||
requester_address: request.requester.address || null,
|
||||
requester_customer_id: request.requester.customerId || null,
|
||||
source: request.source,
|
||||
source_details: request.sourceDetails || null,
|
||||
request_text: request.requestText || '',
|
||||
priority: request.priority || 'normal',
|
||||
}
|
||||
const res = await fetch('/api/sdk/v1/compliance/dsr', {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single DSR by ID from compliance backend
|
||||
*/
|
||||
export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) {
|
||||
return null
|
||||
}
|
||||
const data = await res.json()
|
||||
if (!data || !data.id) return null
|
||||
return transformBackendDSR(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DSR status via compliance backend
|
||||
*/
|
||||
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WORKFLOW ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Verify identity of DSR requester
|
||||
*/
|
||||
export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign DSR to a user
|
||||
*/
|
||||
export async function assignDSR(id: string, assigneeId: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ assignee_id: assigneeId }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend DSR deadline (Art. 12 Abs. 3 DSGVO)
|
||||
*/
|
||||
export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ reason, days }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a DSR
|
||||
*/
|
||||
export async function completeDSR(id: string, summary?: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ summary }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a DSR with legal basis
|
||||
*/
|
||||
export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ reason, legal_basis: legalBasis }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch communications for a DSR
|
||||
*/
|
||||
export async function fetchDSRCommunications(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a communication for a DSR
|
||||
*/
|
||||
export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise<any> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXCEPTION CHECKS (Art. 17)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch exception checks for an erasure DSR
|
||||
*/
|
||||
export async function fetchDSRExceptionChecks(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Art. 17(3) exception checks for an erasure DSR
|
||||
*/
|
||||
export async function initDSRExceptionChecks(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single exception check
|
||||
*/
|
||||
export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise<any> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, {
|
||||
method: 'PUT',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HISTORY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch status change history for a DSR
|
||||
*/
|
||||
export async function fetchDSRHistory(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DSR fields (priority, notes, etc.)
|
||||
*/
|
||||
export async function updateDSR(id: string, data: Record<string, any>): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA FUNCTIONS (kept as fallback)
|
||||
// =============================================================================
|
||||
|
||||
export function createMockDSRList(): DSRRequest[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'dsr-001',
|
||||
referenceNumber: 'DSR-2025-000001',
|
||||
type: 'access',
|
||||
status: 'intake',
|
||||
priority: 'high',
|
||||
requester: {
|
||||
name: 'Max Mustermann',
|
||||
email: 'max.mustermann@example.de'
|
||||
},
|
||||
source: 'web_form',
|
||||
sourceDetails: 'Kontaktformular auf breakpilot.de',
|
||||
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: false
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: null
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-002',
|
||||
referenceNumber: 'DSR-2025-000002',
|
||||
type: 'erasure',
|
||||
status: 'identity_verification',
|
||||
priority: 'high',
|
||||
requester: {
|
||||
name: 'Anna Schmidt',
|
||||
email: 'anna.schmidt@example.de',
|
||||
phone: '+49 170 1234567'
|
||||
},
|
||||
source: 'email',
|
||||
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
|
||||
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: false
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-003',
|
||||
referenceNumber: 'DSR-2025-000003',
|
||||
type: 'rectification',
|
||||
status: 'processing',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Peter Meier',
|
||||
email: 'peter.meier@example.de'
|
||||
},
|
||||
source: 'email',
|
||||
requestText: 'Meine Adresse ist falsch gespeichert.',
|
||||
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'existing_account',
|
||||
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
rectificationDetails: {
|
||||
fieldsToCorrect: [
|
||||
{
|
||||
field: 'Adresse',
|
||||
currentValue: 'Musterstr. 1, 12345 Berlin',
|
||||
requestedValue: 'Musterstr. 10, 12345 Berlin',
|
||||
corrected: false
|
||||
}
|
||||
]
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-004',
|
||||
referenceNumber: 'DSR-2025-000004',
|
||||
type: 'portability',
|
||||
status: 'processing',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Lisa Weber',
|
||||
email: 'lisa.weber@example.de'
|
||||
},
|
||||
source: 'web_form',
|
||||
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'id_document',
|
||||
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'IT Team',
|
||||
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
notes: 'JSON-Export wird vorbereitet',
|
||||
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-005',
|
||||
referenceNumber: 'DSR-2025-000005',
|
||||
type: 'objection',
|
||||
status: 'rejected',
|
||||
priority: 'low',
|
||||
requester: {
|
||||
name: 'Thomas Klein',
|
||||
email: 'thomas.klein@example.de'
|
||||
},
|
||||
source: 'letter',
|
||||
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
|
||||
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'postal',
|
||||
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'Rechtsabteilung',
|
||||
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
objectionDetails: {
|
||||
processingPurpose: 'Marketing',
|
||||
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
|
||||
objectionGrounds: 'Keine konkreten Gruende genannt',
|
||||
decision: 'rejected',
|
||||
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
|
||||
decisionBy: 'Rechtsabteilung',
|
||||
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
|
||||
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-006',
|
||||
referenceNumber: 'DSR-2025-000006',
|
||||
type: 'access',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Sarah Braun',
|
||||
email: 'sarah.braun@example.de'
|
||||
},
|
||||
source: 'email',
|
||||
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'id_document',
|
||||
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
dataExport: {
|
||||
format: 'pdf',
|
||||
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
generatedBy: 'DSB Mueller',
|
||||
fileName: 'datenauskunft_sarah_braun.pdf',
|
||||
fileSize: 245000,
|
||||
includesThirdPartyData: false
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function createMockStatistics(): DSRStatistics {
|
||||
return {
|
||||
total: 6,
|
||||
byStatus: {
|
||||
intake: 1,
|
||||
identity_verification: 1,
|
||||
processing: 2,
|
||||
completed: 1,
|
||||
rejected: 1,
|
||||
cancelled: 0
|
||||
},
|
||||
byType: {
|
||||
access: 2,
|
||||
rectification: 1,
|
||||
erasure: 1,
|
||||
restriction: 0,
|
||||
portability: 1,
|
||||
objection: 1
|
||||
},
|
||||
overdue: 0,
|
||||
dueThisWeek: 2,
|
||||
averageProcessingDays: 18,
|
||||
completedThisMonth: 1
|
||||
}
|
||||
}
|
||||
// Mock data
|
||||
export {
|
||||
createMockDSRList,
|
||||
createMockStatistics,
|
||||
} from './api-mock'
|
||||
|
||||
243
admin-compliance/lib/sdk/dsr/types-api.ts
Normal file
243
admin-compliance/lib/sdk/dsr/types-api.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* DSR Types — API Types, Communication, Audit, Templates, Statistics & Helpers
|
||||
*/
|
||||
|
||||
import type {
|
||||
DSRType,
|
||||
DSRStatus,
|
||||
DSRPriority,
|
||||
DSRSource,
|
||||
DSRRequester,
|
||||
DSRAssignment,
|
||||
DSRRequest,
|
||||
DSRDataExport,
|
||||
IdentityVerificationMethod,
|
||||
CommunicationType,
|
||||
CommunicationChannel,
|
||||
DSRTypeInfo,
|
||||
} from './types-core'
|
||||
|
||||
export { DSR_TYPE_INFO } from './types-core'
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATION
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRCommunication {
|
||||
id: string
|
||||
dsrId: string
|
||||
type: CommunicationType
|
||||
channel: CommunicationChannel
|
||||
subject?: string
|
||||
content: string
|
||||
templateUsed?: string
|
||||
attachments?: {
|
||||
name: string
|
||||
url: string
|
||||
size: number
|
||||
type: string
|
||||
}[]
|
||||
sentAt?: string
|
||||
sentBy?: string
|
||||
receivedAt?: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDIT LOG
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRAuditEntry {
|
||||
id: string
|
||||
dsrId: string
|
||||
action: string
|
||||
previousValue?: string
|
||||
newValue?: string
|
||||
performedBy: string
|
||||
performedAt: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSREmailTemplate {
|
||||
id: string
|
||||
name: string
|
||||
subject: string
|
||||
body: string
|
||||
type: DSRType | 'general'
|
||||
stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion'
|
||||
language: 'de' | 'en'
|
||||
variables: string[]
|
||||
}
|
||||
|
||||
export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [
|
||||
{
|
||||
id: 'intake_confirmation',
|
||||
name: 'Eingangsbestaetigung',
|
||||
subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}',
|
||||
body: `Sehr geehrte(r) {{requesterName}},
|
||||
|
||||
wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}.
|
||||
|
||||
Referenznummer: {{referenceNumber}}
|
||||
Art der Anfrage: {{requestType}}
|
||||
|
||||
Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten.
|
||||
|
||||
Mit freundlichen Gruessen
|
||||
{{senderName}}
|
||||
Datenschutzbeauftragter`,
|
||||
type: 'general',
|
||||
stage: 'intake',
|
||||
language: 'de',
|
||||
variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName']
|
||||
},
|
||||
{
|
||||
id: 'identity_request',
|
||||
name: 'Identitaetsanfrage',
|
||||
subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}',
|
||||
body: `Sehr geehrte(r) {{requesterName}},
|
||||
|
||||
um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet.
|
||||
|
||||
Bitte senden Sie uns eines der folgenden Dokumente:
|
||||
- Kopie Ihres Personalausweises (Vorder- und Rueckseite)
|
||||
- Kopie Ihres Reisepasses
|
||||
|
||||
Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht.
|
||||
|
||||
Mit freundlichen Gruessen
|
||||
{{senderName}}
|
||||
Datenschutzbeauftragter`,
|
||||
type: 'general',
|
||||
stage: 'identity_request',
|
||||
language: 'de',
|
||||
variables: ['requesterName', 'referenceNumber', 'senderName']
|
||||
}
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRFilters {
|
||||
status?: DSRStatus | DSRStatus[]
|
||||
type?: DSRType | DSRType[]
|
||||
priority?: DSRPriority
|
||||
assignedTo?: string
|
||||
overdue?: boolean
|
||||
search?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
export interface DSRListResponse {
|
||||
requests: DSRRequest[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface DSRCreateRequest {
|
||||
type: DSRType
|
||||
requester: DSRRequester
|
||||
source: DSRSource
|
||||
sourceDetails?: string
|
||||
requestText?: string
|
||||
priority?: DSRPriority
|
||||
}
|
||||
|
||||
export interface DSRUpdateRequest {
|
||||
status?: DSRStatus
|
||||
priority?: DSRPriority
|
||||
notes?: string
|
||||
internalNotes?: string
|
||||
assignment?: DSRAssignment
|
||||
}
|
||||
|
||||
export interface DSRVerifyIdentityRequest {
|
||||
method: IdentityVerificationMethod
|
||||
notes?: string
|
||||
documentRef?: string
|
||||
}
|
||||
|
||||
export interface DSRCompleteRequest {
|
||||
completionNotes?: string
|
||||
dataExport?: DSRDataExport
|
||||
}
|
||||
|
||||
export interface DSRRejectRequest {
|
||||
reason: string
|
||||
legalBasis?: string
|
||||
}
|
||||
|
||||
export interface DSRExtendDeadlineRequest {
|
||||
extensionMonths: 1 | 2
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface DSRSendCommunicationRequest {
|
||||
type: CommunicationType
|
||||
channel: CommunicationChannel
|
||||
subject?: string
|
||||
content: string
|
||||
templateId?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRStatistics {
|
||||
total: number
|
||||
byStatus: Record<DSRStatus, number>
|
||||
byType: Record<DSRType, number>
|
||||
overdue: number
|
||||
dueThisWeek: number
|
||||
averageProcessingDays: number
|
||||
completedThisMonth: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function getDaysRemaining(deadline: string): number {
|
||||
const deadlineDate = new Date(deadline)
|
||||
const now = new Date()
|
||||
const diff = deadlineDate.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
export function isOverdue(request: DSRRequest): boolean {
|
||||
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
|
||||
return false
|
||||
}
|
||||
return getDaysRemaining(request.deadline.currentDeadline) < 0
|
||||
}
|
||||
|
||||
export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean {
|
||||
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
|
||||
return false
|
||||
}
|
||||
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
|
||||
return daysRemaining >= 0 && daysRemaining <= thresholdDays
|
||||
}
|
||||
|
||||
export function generateReferenceNumber(year: number, sequence: number): string {
|
||||
return `DSR-${year}-${String(sequence).padStart(6, '0')}`
|
||||
}
|
||||
|
||||
export function getTypeInfo(type: DSRType): DSRTypeInfo {
|
||||
const { DSR_TYPE_INFO } = require('./types-core')
|
||||
return DSR_TYPE_INFO[type]
|
||||
}
|
||||
|
||||
export function getStatusInfo(status: DSRStatus) {
|
||||
const { DSR_STATUS_INFO } = require('./types-core')
|
||||
return DSR_STATUS_INFO[status]
|
||||
}
|
||||
235
admin-compliance/lib/sdk/dsr/types-core.ts
Normal file
235
admin-compliance/lib/sdk/dsr/types-core.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* DSR (Data Subject Request) Types — Core Types & Constants
|
||||
*
|
||||
* Enums, constants, metadata, and main interfaces for GDPR Art. 15-21
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type DSRType =
|
||||
| 'access' // Art. 15
|
||||
| 'rectification' // Art. 16
|
||||
| 'erasure' // Art. 17
|
||||
| 'restriction' // Art. 18
|
||||
| 'portability' // Art. 20
|
||||
| 'objection' // Art. 21
|
||||
|
||||
export type DSRStatus =
|
||||
| 'intake'
|
||||
| 'identity_verification'
|
||||
| 'processing'
|
||||
| 'completed'
|
||||
| 'rejected'
|
||||
| 'cancelled'
|
||||
|
||||
export type DSRPriority = 'low' | 'normal' | 'high' | 'critical'
|
||||
|
||||
export type DSRSource =
|
||||
| 'web_form' | 'email' | 'letter' | 'phone' | 'in_person' | 'other'
|
||||
|
||||
export type IdentityVerificationMethod =
|
||||
| 'id_document' | 'email' | 'phone' | 'postal' | 'existing_account' | 'other'
|
||||
|
||||
export type CommunicationType = 'incoming' | 'outgoing' | 'internal'
|
||||
|
||||
export type CommunicationChannel =
|
||||
| 'email' | 'letter' | 'phone' | 'portal' | 'internal_note'
|
||||
|
||||
// =============================================================================
|
||||
// DSR TYPE METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRTypeInfo {
|
||||
type: DSRType
|
||||
article: string
|
||||
label: string
|
||||
labelShort: string
|
||||
description: string
|
||||
defaultDeadlineDays: number
|
||||
maxExtensionMonths: number
|
||||
color: string
|
||||
bgColor: string
|
||||
processDocument?: string
|
||||
}
|
||||
|
||||
export const DSR_TYPE_INFO: Record<DSRType, DSRTypeInfo> = {
|
||||
access: {
|
||||
type: 'access', article: 'Art. 15', label: 'Auskunftsrecht', labelShort: 'Auskunft',
|
||||
description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten',
|
||||
defaultDeadlineDays: 30, maxExtensionMonths: 2,
|
||||
color: 'text-blue-700', bgColor: 'bg-blue-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf'
|
||||
},
|
||||
rectification: {
|
||||
type: 'rectification', article: 'Art. 16', label: 'Berichtigungsrecht', labelShort: 'Berichtigung',
|
||||
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
|
||||
defaultDeadlineDays: 14, maxExtensionMonths: 2,
|
||||
color: 'text-yellow-700', bgColor: 'bg-yellow-100',
|
||||
processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf'
|
||||
},
|
||||
erasure: {
|
||||
type: 'erasure', article: 'Art. 17', label: 'Loeschungsrecht', labelShort: 'Loeschung',
|
||||
description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")',
|
||||
defaultDeadlineDays: 14, maxExtensionMonths: 2,
|
||||
color: 'text-red-700', bgColor: 'bg-red-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf'
|
||||
},
|
||||
restriction: {
|
||||
type: 'restriction', article: 'Art. 18', label: 'Einschraenkungsrecht', labelShort: 'Einschraenkung',
|
||||
description: 'Recht auf Einschraenkung der Verarbeitung',
|
||||
defaultDeadlineDays: 14, maxExtensionMonths: 2,
|
||||
color: 'text-orange-700', bgColor: 'bg-orange-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf'
|
||||
},
|
||||
portability: {
|
||||
type: 'portability', article: 'Art. 20', label: 'Datenuebertragbarkeit', labelShort: 'Uebertragung',
|
||||
description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format',
|
||||
defaultDeadlineDays: 30, maxExtensionMonths: 2,
|
||||
color: 'text-purple-700', bgColor: 'bg-purple-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf'
|
||||
},
|
||||
objection: {
|
||||
type: 'objection', article: 'Art. 21', label: 'Widerspruchsrecht', labelShort: 'Widerspruch',
|
||||
description: 'Recht auf Widerspruch gegen die Verarbeitung',
|
||||
defaultDeadlineDays: 30, maxExtensionMonths: 0,
|
||||
color: 'text-gray-700', bgColor: 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
export const DSR_STATUS_INFO: Record<DSRStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
|
||||
intake: { label: 'Eingang', color: 'text-blue-700', bgColor: 'bg-blue-100', borderColor: 'border-blue-200' },
|
||||
identity_verification: { label: 'ID-Pruefung', color: 'text-yellow-700', bgColor: 'bg-yellow-100', borderColor: 'border-yellow-200' },
|
||||
processing: { label: 'In Bearbeitung', color: 'text-purple-700', bgColor: 'bg-purple-100', borderColor: 'border-purple-200' },
|
||||
completed: { label: 'Abgeschlossen', color: 'text-green-700', bgColor: 'bg-green-100', borderColor: 'border-green-200' },
|
||||
rejected: { label: 'Abgelehnt', color: 'text-red-700', bgColor: 'bg-red-100', borderColor: 'border-red-200' },
|
||||
cancelled: { label: 'Storniert', color: 'text-gray-700', bgColor: 'bg-gray-100', borderColor: 'border-gray-200' },
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRRequester {
|
||||
name: string
|
||||
email: string
|
||||
phone?: string
|
||||
address?: string
|
||||
customerId?: string
|
||||
}
|
||||
|
||||
export interface DSRIdentityVerification {
|
||||
verified: boolean
|
||||
method?: IdentityVerificationMethod
|
||||
verifiedAt?: string
|
||||
verifiedBy?: string
|
||||
notes?: string
|
||||
documentRef?: string
|
||||
}
|
||||
|
||||
export interface DSRAssignment {
|
||||
assignedTo: string | null
|
||||
assignedAt?: string
|
||||
assignedBy?: string
|
||||
}
|
||||
|
||||
export interface DSRDeadline {
|
||||
originalDeadline: string
|
||||
currentDeadline: string
|
||||
extended: boolean
|
||||
extensionReason?: string
|
||||
extensionApprovedBy?: string
|
||||
extensionApprovedAt?: string
|
||||
}
|
||||
|
||||
export interface DSRRequest {
|
||||
id: string
|
||||
referenceNumber: string
|
||||
type: DSRType
|
||||
status: DSRStatus
|
||||
priority: DSRPriority
|
||||
requester: DSRRequester
|
||||
source: DSRSource
|
||||
sourceDetails?: string
|
||||
requestText?: string
|
||||
receivedAt: string
|
||||
deadline: DSRDeadline
|
||||
completedAt?: string
|
||||
identityVerification: DSRIdentityVerification
|
||||
assignment: DSRAssignment
|
||||
notes?: string
|
||||
internalNotes?: string
|
||||
erasureChecklist?: DSRErasureChecklist
|
||||
dataExport?: DSRDataExport
|
||||
rectificationDetails?: DSRRectificationDetails
|
||||
objectionDetails?: DSRObjectionDetails
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
updatedAt: string
|
||||
updatedBy?: string
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TYPE-SPECIFIC INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRErasureChecklistItem {
|
||||
id: string
|
||||
article: string
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
applies: boolean
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface DSRErasureChecklist {
|
||||
items: DSRErasureChecklistItem[]
|
||||
canProceedWithErasure: boolean
|
||||
reviewedBy?: string
|
||||
reviewedAt?: string
|
||||
}
|
||||
|
||||
export const ERASURE_EXCEPTIONS: Omit<DSRErasureChecklistItem, 'checked' | 'applies' | 'notes'>[] = [
|
||||
{ id: 'art17_3_a', article: '17(3)(a)', label: 'Meinungs- und Informationsfreiheit', description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information' },
|
||||
{ id: 'art17_3_b', article: '17(3)(b)', label: 'Rechtliche Verpflichtung', description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)' },
|
||||
{ id: 'art17_3_c', article: '17(3)(c)', label: 'Oeffentliches Interesse', description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit' },
|
||||
{ id: 'art17_3_d', article: '17(3)(d)', label: 'Archivzwecke', description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik' },
|
||||
{ id: 'art17_3_e', article: '17(3)(e)', label: 'Rechtsansprueche', description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen' },
|
||||
]
|
||||
|
||||
export interface DSRDataExport {
|
||||
format: 'json' | 'csv' | 'xml' | 'pdf'
|
||||
generatedAt?: string
|
||||
generatedBy?: string
|
||||
fileUrl?: string
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
includesThirdPartyData: boolean
|
||||
anonymizedFields?: string[]
|
||||
transferMethod?: 'download' | 'email' | 'third_party'
|
||||
transferRecipient?: string
|
||||
}
|
||||
|
||||
export interface DSRRectificationDetails {
|
||||
fieldsToCorrect: {
|
||||
field: string
|
||||
currentValue: string
|
||||
requestedValue: string
|
||||
corrected: boolean
|
||||
correctedAt?: string
|
||||
correctedBy?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface DSRObjectionDetails {
|
||||
processingPurpose: string
|
||||
legalBasis: string
|
||||
objectionGrounds: string
|
||||
decision: 'accepted' | 'rejected' | 'pending'
|
||||
decisionReason?: string
|
||||
decisionBy?: string
|
||||
decisionAt?: string
|
||||
}
|
||||
@@ -1,581 +1,60 @@
|
||||
/**
|
||||
* DSR (Data Subject Request) Types
|
||||
* DSR (Data Subject Request) Types — barrel re-export
|
||||
*
|
||||
* TypeScript definitions for GDPR Art. 15-21 Data Subject Requests
|
||||
* Based on the Go Consent Service backend API structure
|
||||
* Split into:
|
||||
* - types-core.ts (enums, constants, metadata, main interfaces)
|
||||
* - types-api.ts (API types, communication, audit, templates, helpers)
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type DSRType =
|
||||
| 'access' // Art. 15 - Auskunftsrecht
|
||||
| 'rectification' // Art. 16 - Berichtigungsrecht
|
||||
| 'erasure' // Art. 17 - Loeschungsrecht
|
||||
| 'restriction' // Art. 18 - Einschraenkungsrecht
|
||||
| 'portability' // Art. 20 - Datenuebertragbarkeit
|
||||
| 'objection' // Art. 21 - Widerspruchsrecht
|
||||
|
||||
export type DSRStatus =
|
||||
| 'intake' // Eingang - Anfrage dokumentiert
|
||||
| 'identity_verification' // Identitaetspruefung
|
||||
| 'processing' // In Bearbeitung
|
||||
| 'completed' // Abgeschlossen
|
||||
| 'rejected' // Abgelehnt
|
||||
| 'cancelled' // Storniert
|
||||
|
||||
export type DSRPriority = 'low' | 'normal' | 'high' | 'critical'
|
||||
|
||||
export type DSRSource =
|
||||
| 'web_form' // Kontaktformular/Portal
|
||||
| 'email' // E-Mail
|
||||
| 'letter' // Brief
|
||||
| 'phone' // Telefon
|
||||
| 'in_person' // Persoenlich
|
||||
| 'other' // Sonstiges
|
||||
|
||||
export type IdentityVerificationMethod =
|
||||
| 'id_document' // Ausweiskopie
|
||||
| 'email' // E-Mail-Bestaetigung
|
||||
| 'phone' // Telefonische Bestaetigung
|
||||
| 'postal' // Postalische Bestaetigung
|
||||
| 'existing_account' // Bestehendes Kundenkonto
|
||||
| 'other' // Sonstiges
|
||||
|
||||
export type CommunicationType =
|
||||
| 'incoming' // Eingehend (vom Betroffenen)
|
||||
| 'outgoing' // Ausgehend (an Betroffenen)
|
||||
| 'internal' // Intern (Notizen)
|
||||
|
||||
export type CommunicationChannel =
|
||||
| 'email'
|
||||
| 'letter'
|
||||
| 'phone'
|
||||
| 'portal'
|
||||
| 'internal_note'
|
||||
|
||||
// =============================================================================
|
||||
// DSR TYPE METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRTypeInfo {
|
||||
type: DSRType
|
||||
article: string
|
||||
label: string
|
||||
labelShort: string
|
||||
description: string
|
||||
defaultDeadlineDays: number
|
||||
maxExtensionMonths: number
|
||||
color: string
|
||||
bgColor: string
|
||||
processDocument?: string // Reference to process document
|
||||
}
|
||||
|
||||
export const DSR_TYPE_INFO: Record<DSRType, DSRTypeInfo> = {
|
||||
access: {
|
||||
type: 'access',
|
||||
article: 'Art. 15',
|
||||
label: 'Auskunftsrecht',
|
||||
labelShort: 'Auskunft',
|
||||
description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten',
|
||||
defaultDeadlineDays: 30,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf'
|
||||
},
|
||||
rectification: {
|
||||
type: 'rectification',
|
||||
article: 'Art. 16',
|
||||
label: 'Berichtigungsrecht',
|
||||
labelShort: 'Berichtigung',
|
||||
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
|
||||
defaultDeadlineDays: 14,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100',
|
||||
processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf'
|
||||
},
|
||||
erasure: {
|
||||
type: 'erasure',
|
||||
article: 'Art. 17',
|
||||
label: 'Loeschungsrecht',
|
||||
labelShort: 'Loeschung',
|
||||
description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")',
|
||||
defaultDeadlineDays: 14,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf'
|
||||
},
|
||||
restriction: {
|
||||
type: 'restriction',
|
||||
article: 'Art. 18',
|
||||
label: 'Einschraenkungsrecht',
|
||||
labelShort: 'Einschraenkung',
|
||||
description: 'Recht auf Einschraenkung der Verarbeitung',
|
||||
defaultDeadlineDays: 14,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf'
|
||||
},
|
||||
portability: {
|
||||
type: 'portability',
|
||||
article: 'Art. 20',
|
||||
label: 'Datenuebertragbarkeit',
|
||||
labelShort: 'Uebertragung',
|
||||
description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format',
|
||||
defaultDeadlineDays: 30,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf'
|
||||
},
|
||||
objection: {
|
||||
type: 'objection',
|
||||
article: 'Art. 21',
|
||||
label: 'Widerspruchsrecht',
|
||||
labelShort: 'Widerspruch',
|
||||
description: 'Recht auf Widerspruch gegen die Verarbeitung',
|
||||
defaultDeadlineDays: 30,
|
||||
maxExtensionMonths: 0, // No extension allowed for objections
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
export const DSR_STATUS_INFO: Record<DSRStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
|
||||
intake: {
|
||||
label: 'Eingang',
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100',
|
||||
borderColor: 'border-blue-200'
|
||||
},
|
||||
identity_verification: {
|
||||
label: 'ID-Pruefung',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100',
|
||||
borderColor: 'border-yellow-200'
|
||||
},
|
||||
processing: {
|
||||
label: 'In Bearbeitung',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100',
|
||||
borderColor: 'border-purple-200'
|
||||
},
|
||||
completed: {
|
||||
label: 'Abgeschlossen',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100',
|
||||
borderColor: 'border-green-200'
|
||||
},
|
||||
rejected: {
|
||||
label: 'Abgelehnt',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100',
|
||||
borderColor: 'border-red-200'
|
||||
},
|
||||
cancelled: {
|
||||
label: 'Storniert',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100',
|
||||
borderColor: 'border-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRRequester {
|
||||
name: string
|
||||
email: string
|
||||
phone?: string
|
||||
address?: string
|
||||
customerId?: string // If existing customer
|
||||
}
|
||||
|
||||
export interface DSRIdentityVerification {
|
||||
verified: boolean
|
||||
method?: IdentityVerificationMethod
|
||||
verifiedAt?: string
|
||||
verifiedBy?: string
|
||||
notes?: string
|
||||
documentRef?: string // Reference to uploaded ID document
|
||||
}
|
||||
|
||||
export interface DSRAssignment {
|
||||
assignedTo: string | null
|
||||
assignedAt?: string
|
||||
assignedBy?: string
|
||||
}
|
||||
|
||||
export interface DSRDeadline {
|
||||
originalDeadline: string
|
||||
currentDeadline: string
|
||||
extended: boolean
|
||||
extensionReason?: string
|
||||
extensionApprovedBy?: string
|
||||
extensionApprovedAt?: string
|
||||
}
|
||||
|
||||
export interface DSRRequest {
|
||||
id: string
|
||||
referenceNumber: string // e.g., "DSR-2025-000042"
|
||||
type: DSRType
|
||||
status: DSRStatus
|
||||
priority: DSRPriority
|
||||
|
||||
// Requester info
|
||||
requester: DSRRequester
|
||||
|
||||
// Request details
|
||||
source: DSRSource
|
||||
sourceDetails?: string // e.g., "Kontaktformular auf website.de"
|
||||
requestText?: string // Original request text
|
||||
|
||||
// Dates
|
||||
receivedAt: string
|
||||
deadline: DSRDeadline
|
||||
completedAt?: string
|
||||
|
||||
// Verification
|
||||
identityVerification: DSRIdentityVerification
|
||||
|
||||
// Assignment
|
||||
assignment: DSRAssignment
|
||||
|
||||
// Processing
|
||||
notes?: string
|
||||
internalNotes?: string
|
||||
|
||||
// Type-specific data
|
||||
erasureChecklist?: DSRErasureChecklist // For Art. 17
|
||||
dataExport?: DSRDataExport // For Art. 15, 20
|
||||
rectificationDetails?: DSRRectificationDetails // For Art. 16
|
||||
objectionDetails?: DSRObjectionDetails // For Art. 21
|
||||
|
||||
// Audit
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
updatedAt: string
|
||||
updatedBy?: string
|
||||
|
||||
// Metadata
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TYPE-SPECIFIC INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
// Art. 17(3) Erasure Exceptions Checklist
|
||||
export interface DSRErasureChecklistItem {
|
||||
id: string
|
||||
article: string // e.g., "17(3)(a)"
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
applies: boolean
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface DSRErasureChecklist {
|
||||
items: DSRErasureChecklistItem[]
|
||||
canProceedWithErasure: boolean
|
||||
reviewedBy?: string
|
||||
reviewedAt?: string
|
||||
}
|
||||
|
||||
export const ERASURE_EXCEPTIONS: Omit<DSRErasureChecklistItem, 'checked' | 'applies' | 'notes'>[] = [
|
||||
{
|
||||
id: 'art17_3_a',
|
||||
article: '17(3)(a)',
|
||||
label: 'Meinungs- und Informationsfreiheit',
|
||||
description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_b',
|
||||
article: '17(3)(b)',
|
||||
label: 'Rechtliche Verpflichtung',
|
||||
description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_c',
|
||||
article: '17(3)(c)',
|
||||
label: 'Oeffentliches Interesse',
|
||||
description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_d',
|
||||
article: '17(3)(d)',
|
||||
label: 'Archivzwecke',
|
||||
description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_e',
|
||||
article: '17(3)(e)',
|
||||
label: 'Rechtsansprueche',
|
||||
description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen'
|
||||
}
|
||||
]
|
||||
|
||||
// Data Export for Art. 15, 20
|
||||
export interface DSRDataExport {
|
||||
format: 'json' | 'csv' | 'xml' | 'pdf'
|
||||
generatedAt?: string
|
||||
generatedBy?: string
|
||||
fileUrl?: string
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
includesThirdPartyData: boolean
|
||||
anonymizedFields?: string[]
|
||||
transferMethod?: 'download' | 'email' | 'third_party' // For Art. 20 transfer
|
||||
transferRecipient?: string // For Art. 20 transfer to another controller
|
||||
}
|
||||
|
||||
// Rectification Details for Art. 16
|
||||
export interface DSRRectificationDetails {
|
||||
fieldsToCorrect: {
|
||||
field: string
|
||||
currentValue: string
|
||||
requestedValue: string
|
||||
corrected: boolean
|
||||
correctedAt?: string
|
||||
correctedBy?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
// Objection Details for Art. 21
|
||||
export interface DSRObjectionDetails {
|
||||
processingPurpose: string
|
||||
legalBasis: string
|
||||
objectionGrounds: string
|
||||
decision: 'accepted' | 'rejected' | 'pending'
|
||||
decisionReason?: string
|
||||
decisionBy?: string
|
||||
decisionAt?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATION
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRCommunication {
|
||||
id: string
|
||||
dsrId: string
|
||||
type: CommunicationType
|
||||
channel: CommunicationChannel
|
||||
subject?: string
|
||||
content: string
|
||||
templateUsed?: string // Reference to email template
|
||||
attachments?: {
|
||||
name: string
|
||||
url: string
|
||||
size: number
|
||||
type: string
|
||||
}[]
|
||||
sentAt?: string
|
||||
sentBy?: string
|
||||
receivedAt?: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDIT LOG
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRAuditEntry {
|
||||
id: string
|
||||
dsrId: string
|
||||
action: string // e.g., "status_changed", "identity_verified", "assigned"
|
||||
previousValue?: string
|
||||
newValue?: string
|
||||
performedBy: string
|
||||
performedAt: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSREmailTemplate {
|
||||
id: string
|
||||
name: string
|
||||
subject: string
|
||||
body: string
|
||||
type: DSRType | 'general'
|
||||
stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion'
|
||||
language: 'de' | 'en'
|
||||
variables: string[] // e.g., ["requesterName", "referenceNumber", "deadline"]
|
||||
}
|
||||
|
||||
export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [
|
||||
{
|
||||
id: 'intake_confirmation',
|
||||
name: 'Eingangsbestaetigung',
|
||||
subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}',
|
||||
body: `Sehr geehrte(r) {{requesterName}},
|
||||
|
||||
wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}.
|
||||
|
||||
Referenznummer: {{referenceNumber}}
|
||||
Art der Anfrage: {{requestType}}
|
||||
|
||||
Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten.
|
||||
|
||||
Mit freundlichen Gruessen
|
||||
{{senderName}}
|
||||
Datenschutzbeauftragter`,
|
||||
type: 'general',
|
||||
stage: 'intake',
|
||||
language: 'de',
|
||||
variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName']
|
||||
},
|
||||
{
|
||||
id: 'identity_request',
|
||||
name: 'Identitaetsanfrage',
|
||||
subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}',
|
||||
body: `Sehr geehrte(r) {{requesterName}},
|
||||
|
||||
um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet.
|
||||
|
||||
Bitte senden Sie uns eines der folgenden Dokumente:
|
||||
- Kopie Ihres Personalausweises (Vorder- und Rueckseite)
|
||||
- Kopie Ihres Reisepasses
|
||||
|
||||
Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht.
|
||||
|
||||
Mit freundlichen Gruessen
|
||||
{{senderName}}
|
||||
Datenschutzbeauftragter`,
|
||||
type: 'general',
|
||||
stage: 'identity_request',
|
||||
language: 'de',
|
||||
variables: ['requesterName', 'referenceNumber', 'senderName']
|
||||
}
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRFilters {
|
||||
status?: DSRStatus | DSRStatus[]
|
||||
type?: DSRType | DSRType[]
|
||||
priority?: DSRPriority
|
||||
assignedTo?: string
|
||||
overdue?: boolean
|
||||
search?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
export interface DSRListResponse {
|
||||
requests: DSRRequest[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface DSRCreateRequest {
|
||||
type: DSRType
|
||||
requester: DSRRequester
|
||||
source: DSRSource
|
||||
sourceDetails?: string
|
||||
requestText?: string
|
||||
priority?: DSRPriority
|
||||
}
|
||||
|
||||
export interface DSRUpdateRequest {
|
||||
status?: DSRStatus
|
||||
priority?: DSRPriority
|
||||
notes?: string
|
||||
internalNotes?: string
|
||||
assignment?: DSRAssignment
|
||||
}
|
||||
|
||||
export interface DSRVerifyIdentityRequest {
|
||||
method: IdentityVerificationMethod
|
||||
notes?: string
|
||||
documentRef?: string
|
||||
}
|
||||
|
||||
export interface DSRCompleteRequest {
|
||||
completionNotes?: string
|
||||
dataExport?: DSRDataExport
|
||||
}
|
||||
|
||||
export interface DSRRejectRequest {
|
||||
reason: string
|
||||
legalBasis?: string // e.g., Art. 17(3) exception
|
||||
}
|
||||
|
||||
export interface DSRExtendDeadlineRequest {
|
||||
extensionMonths: 1 | 2
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface DSRSendCommunicationRequest {
|
||||
type: CommunicationType
|
||||
channel: CommunicationChannel
|
||||
subject?: string
|
||||
content: string
|
||||
templateId?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRStatistics {
|
||||
total: number
|
||||
byStatus: Record<DSRStatus, number>
|
||||
byType: Record<DSRType, number>
|
||||
overdue: number
|
||||
dueThisWeek: number
|
||||
averageProcessingDays: number
|
||||
completedThisMonth: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function getDaysRemaining(deadline: string): number {
|
||||
const deadlineDate = new Date(deadline)
|
||||
const now = new Date()
|
||||
const diff = deadlineDate.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
export function isOverdue(request: DSRRequest): boolean {
|
||||
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
|
||||
return false
|
||||
}
|
||||
return getDaysRemaining(request.deadline.currentDeadline) < 0
|
||||
}
|
||||
|
||||
export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean {
|
||||
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
|
||||
return false
|
||||
}
|
||||
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
|
||||
return daysRemaining >= 0 && daysRemaining <= thresholdDays
|
||||
}
|
||||
|
||||
export function generateReferenceNumber(year: number, sequence: number): string {
|
||||
return `DSR-${year}-${String(sequence).padStart(6, '0')}`
|
||||
}
|
||||
|
||||
export function getTypeInfo(type: DSRType): DSRTypeInfo {
|
||||
return DSR_TYPE_INFO[type]
|
||||
}
|
||||
|
||||
export function getStatusInfo(status: DSRStatus) {
|
||||
return DSR_STATUS_INFO[status]
|
||||
}
|
||||
export type {
|
||||
DSRType,
|
||||
DSRStatus,
|
||||
DSRPriority,
|
||||
DSRSource,
|
||||
IdentityVerificationMethod,
|
||||
CommunicationType,
|
||||
CommunicationChannel,
|
||||
DSRTypeInfo,
|
||||
DSRRequester,
|
||||
DSRIdentityVerification,
|
||||
DSRAssignment,
|
||||
DSRDeadline,
|
||||
DSRRequest,
|
||||
DSRErasureChecklistItem,
|
||||
DSRErasureChecklist,
|
||||
DSRDataExport,
|
||||
DSRRectificationDetails,
|
||||
DSRObjectionDetails,
|
||||
} from './types-core'
|
||||
|
||||
export {
|
||||
DSR_TYPE_INFO,
|
||||
DSR_STATUS_INFO,
|
||||
ERASURE_EXCEPTIONS,
|
||||
} from './types-core'
|
||||
|
||||
export type {
|
||||
DSRCommunication,
|
||||
DSRAuditEntry,
|
||||
DSREmailTemplate,
|
||||
DSRFilters,
|
||||
DSRListResponse,
|
||||
DSRCreateRequest,
|
||||
DSRUpdateRequest,
|
||||
DSRVerifyIdentityRequest,
|
||||
DSRCompleteRequest,
|
||||
DSRRejectRequest,
|
||||
DSRExtendDeadlineRequest,
|
||||
DSRSendCommunicationRequest,
|
||||
DSRStatistics,
|
||||
} from './types-api'
|
||||
|
||||
export {
|
||||
DSR_EMAIL_TEMPLATES,
|
||||
getDaysRemaining,
|
||||
isOverdue,
|
||||
isUrgent,
|
||||
generateReferenceNumber,
|
||||
getTypeInfo,
|
||||
getStatusInfo,
|
||||
} from './types-api'
|
||||
|
||||
@@ -1,669 +1,12 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Einwilligungen Context & Reducer
|
||||
*
|
||||
* Zentrale State-Verwaltung fuer das Datenpunktkatalog & DSI-Generator Modul.
|
||||
* Verwendet React Context + useReducer fuer vorhersehbare State-Updates.
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useCallback,
|
||||
useMemo,
|
||||
ReactNode,
|
||||
Dispatch,
|
||||
} from 'react'
|
||||
import {
|
||||
EinwilligungenState,
|
||||
EinwilligungenAction,
|
||||
EinwilligungenTab,
|
||||
DataPoint,
|
||||
DataPointCatalog,
|
||||
GeneratedPrivacyPolicy,
|
||||
CookieBannerConfig,
|
||||
CompanyInfo,
|
||||
ConsentStatistics,
|
||||
PrivacyPolicySection,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
RiskLevel,
|
||||
} from './types'
|
||||
import {
|
||||
PREDEFINED_DATA_POINTS,
|
||||
RETENTION_MATRIX,
|
||||
DEFAULT_COOKIE_CATEGORIES,
|
||||
createDefaultCatalog,
|
||||
getDataPointById,
|
||||
getDataPointsByCategory,
|
||||
countDataPointsByCategory,
|
||||
countDataPointsByRiskLevel,
|
||||
} from './catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE
|
||||
// Einwilligungen Context — Barrel re-exports
|
||||
// Preserves the original public API so existing imports work unchanged.
|
||||
// =============================================================================
|
||||
|
||||
const initialState: EinwilligungenState = {
|
||||
// Data
|
||||
catalog: null,
|
||||
selectedDataPoints: [],
|
||||
privacyPolicy: null,
|
||||
cookieBannerConfig: null,
|
||||
companyInfo: null,
|
||||
consentStatistics: null,
|
||||
|
||||
// UI State
|
||||
activeTab: 'catalog',
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: null,
|
||||
|
||||
// Editor State
|
||||
editingDataPoint: null,
|
||||
editingSection: null,
|
||||
|
||||
// Preview
|
||||
previewLanguage: 'de',
|
||||
previewFormat: 'HTML',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REDUCER
|
||||
// =============================================================================
|
||||
|
||||
function einwilligungenReducer(
|
||||
state: EinwilligungenState,
|
||||
action: EinwilligungenAction
|
||||
): EinwilligungenState {
|
||||
switch (action.type) {
|
||||
case 'SET_CATALOG':
|
||||
return {
|
||||
...state,
|
||||
catalog: action.payload,
|
||||
// Automatisch alle aktiven Datenpunkte auswaehlen
|
||||
selectedDataPoints: [
|
||||
...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||
...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||
],
|
||||
}
|
||||
|
||||
case 'SET_SELECTED_DATA_POINTS':
|
||||
return {
|
||||
...state,
|
||||
selectedDataPoints: action.payload,
|
||||
}
|
||||
|
||||
case 'TOGGLE_DATA_POINT': {
|
||||
const id = action.payload
|
||||
const isSelected = state.selectedDataPoints.includes(id)
|
||||
return {
|
||||
...state,
|
||||
selectedDataPoints: isSelected
|
||||
? state.selectedDataPoints.filter((dpId) => dpId !== id)
|
||||
: [...state.selectedDataPoints, id],
|
||||
}
|
||||
}
|
||||
|
||||
case 'ADD_CUSTOM_DATA_POINT':
|
||||
if (!state.catalog) return state
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: [...state.catalog.customDataPoints, action.payload],
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
selectedDataPoints: [...state.selectedDataPoints, action.payload.id],
|
||||
}
|
||||
|
||||
case 'UPDATE_DATA_POINT': {
|
||||
if (!state.catalog) return state
|
||||
const { id, data } = action.payload
|
||||
|
||||
// Pruefe ob es ein vordefinierter oder kundenspezifischer Datenpunkt ist
|
||||
const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id)
|
||||
|
||||
if (isCustom) {
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: state.catalog.customDataPoints.map((dp) =>
|
||||
dp.id === id ? { ...dp, ...data } : dp
|
||||
),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Vordefinierte Datenpunkte: nur isActive aendern
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
dataPoints: state.catalog.dataPoints.map((dp) =>
|
||||
dp.id === id ? { ...dp, ...data } : dp
|
||||
),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'DELETE_CUSTOM_DATA_POINT':
|
||||
if (!state.catalog) return state
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload),
|
||||
}
|
||||
|
||||
case 'SET_PRIVACY_POLICY':
|
||||
return {
|
||||
...state,
|
||||
privacyPolicy: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_COOKIE_BANNER_CONFIG':
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: action.payload,
|
||||
}
|
||||
|
||||
case 'UPDATE_COOKIE_BANNER_STYLING':
|
||||
if (!state.cookieBannerConfig) return state
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: {
|
||||
...state.cookieBannerConfig,
|
||||
styling: {
|
||||
...state.cookieBannerConfig.styling,
|
||||
...action.payload,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
case 'UPDATE_COOKIE_BANNER_TEXTS':
|
||||
if (!state.cookieBannerConfig) return state
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: {
|
||||
...state.cookieBannerConfig,
|
||||
texts: {
|
||||
...state.cookieBannerConfig.texts,
|
||||
...action.payload,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
case 'SET_COMPANY_INFO':
|
||||
return {
|
||||
...state,
|
||||
companyInfo: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_CONSENT_STATISTICS':
|
||||
return {
|
||||
...state,
|
||||
consentStatistics: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_ACTIVE_TAB':
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_SAVING':
|
||||
return {
|
||||
...state,
|
||||
isSaving: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_EDITING_DATA_POINT':
|
||||
return {
|
||||
...state,
|
||||
editingDataPoint: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_EDITING_SECTION':
|
||||
return {
|
||||
...state,
|
||||
editingSection: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_PREVIEW_LANGUAGE':
|
||||
return {
|
||||
...state,
|
||||
previewLanguage: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_PREVIEW_FORMAT':
|
||||
return {
|
||||
...state,
|
||||
previewFormat: action.payload,
|
||||
}
|
||||
|
||||
case 'RESET_STATE':
|
||||
return initialState
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
interface EinwilligungenContextValue {
|
||||
state: EinwilligungenState
|
||||
dispatch: Dispatch<EinwilligungenAction>
|
||||
|
||||
// Computed Values
|
||||
allDataPoints: DataPoint[]
|
||||
selectedDataPointsData: DataPoint[]
|
||||
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
|
||||
categoryStats: Record<DataPointCategory, number>
|
||||
riskStats: Record<RiskLevel, number>
|
||||
legalBasisStats: Record<LegalBasis, number>
|
||||
|
||||
// Actions
|
||||
initializeCatalog: (tenantId: string) => void
|
||||
loadCatalog: (tenantId: string) => Promise<void>
|
||||
saveCatalog: () => Promise<void>
|
||||
toggleDataPoint: (id: string) => void
|
||||
addCustomDataPoint: (dataPoint: DataPoint) => void
|
||||
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
|
||||
deleteCustomDataPoint: (id: string) => void
|
||||
setActiveTab: (tab: EinwilligungenTab) => void
|
||||
setPreviewLanguage: (language: SupportedLanguage) => void
|
||||
setPreviewFormat: (format: ExportFormat) => void
|
||||
setCompanyInfo: (info: CompanyInfo) => void
|
||||
generatePrivacyPolicy: () => Promise<void>
|
||||
generateCookieBannerConfig: () => void
|
||||
}
|
||||
|
||||
const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
|
||||
|
||||
// =============================================================================
|
||||
// PROVIDER
|
||||
// =============================================================================
|
||||
|
||||
interface EinwilligungenProviderProps {
|
||||
children: ReactNode
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
|
||||
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// COMPUTED VALUES
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const allDataPoints = useMemo(() => {
|
||||
if (!state.catalog) return PREDEFINED_DATA_POINTS
|
||||
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
|
||||
}, [state.catalog])
|
||||
|
||||
const selectedDataPointsData = useMemo(() => {
|
||||
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
|
||||
}, [allDataPoints, state.selectedDataPoints])
|
||||
|
||||
const dataPointsByCategory = useMemo(() => {
|
||||
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
|
||||
// 18 Kategorien (A-R)
|
||||
const categories: DataPointCategory[] = [
|
||||
'MASTER_DATA', // A
|
||||
'CONTACT_DATA', // B
|
||||
'AUTHENTICATION', // C
|
||||
'CONSENT', // D
|
||||
'COMMUNICATION', // E
|
||||
'PAYMENT', // F
|
||||
'USAGE_DATA', // G
|
||||
'LOCATION', // H
|
||||
'DEVICE_DATA', // I
|
||||
'MARKETING', // J
|
||||
'ANALYTICS', // K
|
||||
'SOCIAL_MEDIA', // L
|
||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
||||
'CONTRACT_DATA', // O
|
||||
'LOG_DATA', // P
|
||||
'AI_DATA', // Q - AI Act
|
||||
'SECURITY', // R
|
||||
]
|
||||
for (const cat of categories) {
|
||||
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
|
||||
}
|
||||
return result as Record<DataPointCategory, DataPoint[]>
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const categoryStats = useMemo(() => {
|
||||
const counts: Partial<Record<DataPointCategory, number>> = {}
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.category] = (counts[dp.category] || 0) + 1
|
||||
}
|
||||
return counts as Record<DataPointCategory, number>
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const riskStats = useMemo(() => {
|
||||
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.riskLevel]++
|
||||
}
|
||||
return counts
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const legalBasisStats = useMemo(() => {
|
||||
// Alle 7 Rechtsgrundlagen
|
||||
const counts: Record<LegalBasis, number> = {
|
||||
CONTRACT: 0,
|
||||
CONSENT: 0,
|
||||
EXPLICIT_CONSENT: 0,
|
||||
LEGITIMATE_INTEREST: 0,
|
||||
LEGAL_OBLIGATION: 0,
|
||||
VITAL_INTERESTS: 0,
|
||||
PUBLIC_INTEREST: 0,
|
||||
}
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.legalBasis]++
|
||||
}
|
||||
return counts
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACTIONS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const initializeCatalog = useCallback(
|
||||
(tid: string) => {
|
||||
const catalog = createDefaultCatalog(tid)
|
||||
dispatch({ type: 'SET_CATALOG', payload: catalog })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const loadCatalog = useCallback(
|
||||
async (tid: string) => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||
headers: {
|
||||
'X-Tenant-ID': tid,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
|
||||
if (data.companyInfo) {
|
||||
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
|
||||
}
|
||||
if (data.cookieBannerConfig) {
|
||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
// Katalog existiert noch nicht - erstelle Default
|
||||
initializeCatalog(tid)
|
||||
} else {
|
||||
throw new Error('Failed to load catalog')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading catalog:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
|
||||
// Fallback zu Default
|
||||
initializeCatalog(tid)
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
},
|
||||
[dispatch, initializeCatalog]
|
||||
)
|
||||
|
||||
const saveCatalog = useCallback(async () => {
|
||||
if (!state.catalog) return
|
||||
|
||||
dispatch({ type: 'SET_SAVING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': state.catalog.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
catalog: state.catalog,
|
||||
companyInfo: state.companyInfo,
|
||||
cookieBannerConfig: state.cookieBannerConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save catalog')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving catalog:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
|
||||
} finally {
|
||||
dispatch({ type: 'SET_SAVING', payload: false })
|
||||
}
|
||||
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
|
||||
|
||||
const toggleDataPoint = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const addCustomDataPoint = useCallback(
|
||||
(dataPoint: DataPoint) => {
|
||||
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateDataPoint = useCallback(
|
||||
(id: string, data: Partial<DataPoint>) => {
|
||||
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteCustomDataPoint = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(tab: EinwilligungenTab) => {
|
||||
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setPreviewLanguage = useCallback(
|
||||
(language: SupportedLanguage) => {
|
||||
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setPreviewFormat = useCallback(
|
||||
(format: ExportFormat) => {
|
||||
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setCompanyInfo = useCallback(
|
||||
(info: CompanyInfo) => {
|
||||
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const generatePrivacyPolicy = useCallback(async () => {
|
||||
if (!state.catalog || !state.companyInfo) {
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': state.catalog.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
dataPointIds: state.selectedDataPoints,
|
||||
companyInfo: state.companyInfo,
|
||||
language: state.previewLanguage,
|
||||
format: state.previewFormat,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const policy = await response.json()
|
||||
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
|
||||
} else {
|
||||
throw new Error('Failed to generate privacy policy')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating privacy policy:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
}, [
|
||||
state.catalog,
|
||||
state.companyInfo,
|
||||
state.selectedDataPoints,
|
||||
state.previewLanguage,
|
||||
state.previewFormat,
|
||||
dispatch,
|
||||
])
|
||||
|
||||
const generateCookieBannerConfig = useCallback(() => {
|
||||
if (!state.catalog) return
|
||||
|
||||
const config: CookieBannerConfig = {
|
||||
id: `cookie-banner-${state.catalog.tenantId}`,
|
||||
tenantId: state.catalog.tenantId,
|
||||
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
|
||||
...cat,
|
||||
// Filtere nur die ausgewaehlten Datenpunkte
|
||||
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
|
||||
})),
|
||||
styling: {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1',
|
||||
borderRadius: 12,
|
||||
},
|
||||
texts: {
|
||||
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website.',
|
||||
},
|
||||
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
||||
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
|
||||
customize: { de: 'Anpassen', en: 'Customize' },
|
||||
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
||||
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
|
||||
}, [state.catalog, state.selectedDataPoints, dispatch])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CONTEXT VALUE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const value: EinwilligungenContextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
|
||||
// Computed Values
|
||||
allDataPoints,
|
||||
selectedDataPointsData,
|
||||
dataPointsByCategory,
|
||||
categoryStats,
|
||||
riskStats,
|
||||
legalBasisStats,
|
||||
|
||||
// Actions
|
||||
initializeCatalog,
|
||||
loadCatalog,
|
||||
saveCatalog,
|
||||
toggleDataPoint,
|
||||
addCustomDataPoint,
|
||||
updateDataPoint,
|
||||
deleteCustomDataPoint,
|
||||
setActiveTab,
|
||||
setPreviewLanguage,
|
||||
setPreviewFormat,
|
||||
setCompanyInfo,
|
||||
generatePrivacyPolicy,
|
||||
generateCookieBannerConfig,
|
||||
}
|
||||
|
||||
return (
|
||||
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useEinwilligungen(): EinwilligungenContextValue {
|
||||
const context = useContext(EinwilligungenContext)
|
||||
if (!context) {
|
||||
throw new Error('useEinwilligungen must be used within EinwilligungenProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export { initialState, einwilligungenReducer }
|
||||
export { EinwilligungenProvider } from './provider'
|
||||
export { EinwilligungenContext } from './provider'
|
||||
export type { EinwilligungenContextValue } from './provider'
|
||||
export { useEinwilligungen } from './hooks'
|
||||
export { initialState, einwilligungenReducer } from './reducer'
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Cookie Banner — Configuration & Category Generation
|
||||
*
|
||||
* Default texts, styling, and category generation from data points.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CookieBannerCategory,
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerTexts,
|
||||
CookieInfo,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEFAULT CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
|
||||
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
|
||||
},
|
||||
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
||||
rejectAll: { de: 'Nur notwendige', en: 'Essential Only' },
|
||||
customize: { de: 'Einstellungen', en: 'Customize' },
|
||||
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
||||
privacyPolicyLink: {
|
||||
de: 'Mehr in unserer Datenschutzerklaerung',
|
||||
en: 'More in our Privacy Policy',
|
||||
},
|
||||
}
|
||||
|
||||
export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: '#f1f5f9',
|
||||
textColor: '#1e293b',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
maxWidth: 480,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getExpiryFromRetention(retention: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'24_HOURS': '24 Stunden / 24 hours',
|
||||
'30_DAYS': '30 Tage / 30 days',
|
||||
'90_DAYS': '90 Tage / 90 days',
|
||||
'12_MONTHS': '1 Jahr / 1 year',
|
||||
'24_MONTHS': '2 Jahre / 2 years',
|
||||
'36_MONTHS': '3 Jahre / 3 years',
|
||||
'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
|
||||
'UNTIL_PURPOSE_FULFILLED': 'Session',
|
||||
'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
|
||||
}
|
||||
return mapping[retention] || 'Session'
|
||||
}
|
||||
|
||||
export function generateCookieCategories(
|
||||
dataPoints: DataPoint[]
|
||||
): CookieBannerCategory[] {
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
|
||||
const categoryDataPoints = cookieDataPoints.filter(
|
||||
(dp) => dp.cookieCategory === defaultCat.id
|
||||
)
|
||||
|
||||
const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
|
||||
name: dp.code,
|
||||
provider: 'First Party',
|
||||
purpose: dp.purpose,
|
||||
expiry: getExpiryFromRetention(dp.retentionPeriod),
|
||||
type: 'FIRST_PARTY',
|
||||
}))
|
||||
|
||||
return {
|
||||
...defaultCat,
|
||||
dataPointIds: categoryDataPoints.map((dp) => dp.id),
|
||||
cookies,
|
||||
}
|
||||
}).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
|
||||
}
|
||||
|
||||
export function generateCookieBannerConfig(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
customTexts?: Partial<CookieBannerTexts>,
|
||||
customStyling?: Partial<CookieBannerStyling>
|
||||
): CookieBannerConfig {
|
||||
const categories = generateCookieCategories(dataPoints)
|
||||
|
||||
return {
|
||||
id: `cookie-banner-${tenantId}`,
|
||||
tenantId,
|
||||
categories,
|
||||
styling: { ...DEFAULT_COOKIE_BANNER_STYLING, ...customStyling },
|
||||
texts: { ...DEFAULT_COOKIE_BANNER_TEXTS, ...customTexts },
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Cookie Banner — Embed Code Generation (CSS, HTML, JS)
|
||||
*
|
||||
* Generates the embeddable cookie banner code from configuration.
|
||||
*/
|
||||
|
||||
import {
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerEmbedCode,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// MAIN EXPORT
|
||||
// =============================================================================
|
||||
|
||||
export function generateEmbedCode(
|
||||
config: CookieBannerConfig,
|
||||
privacyPolicyUrl: string = '/datenschutz'
|
||||
): CookieBannerEmbedCode {
|
||||
const css = generateCSS(config.styling)
|
||||
const html = generateHTML(config, privacyPolicyUrl)
|
||||
const js = generateJS(config)
|
||||
|
||||
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
|
||||
|
||||
return { html, css, js, scriptTag }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CSS GENERATION
|
||||
// =============================================================================
|
||||
|
||||
function generateCSS(styling: CookieBannerStyling): string {
|
||||
const positionStyles: Record<string, string> = {
|
||||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||||
TOP: 'top: 0; left: 0; right: 0;',
|
||||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
||||
}
|
||||
|
||||
const isDark = styling.theme === 'DARK'
|
||||
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
|
||||
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
|
||||
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||||
|
||||
return `
|
||||
/* Cookie Banner Styles */
|
||||
.cookie-banner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 9998;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
${positionStyles[styling.position]}
|
||||
z-index: 9999;
|
||||
background: ${bgColor};
|
||||
color: ${textColor};
|
||||
border-radius: ${styling.borderRadius || 12}px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
max-width: ${styling.maxWidth}px;
|
||||
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cookie-banner-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 12px 20px;
|
||||
border-radius: ${(styling.borderRadius || 12) / 2}px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary {
|
||||
background: ${styling.primaryColor};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary {
|
||||
background: ${styling.secondaryColor || borderColor};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.cookie-banner-link {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: ${styling.primaryColor};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cookie-banner-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Category Details */
|
||||
.cookie-banner-details {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid ${borderColor};
|
||||
padding-top: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cookie-banner-details.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cookie-banner-category {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
.cookie-banner-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cookie-banner-category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-category-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cookie-banner-category-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
background: ${borderColor};
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active {
|
||||
background: ${styling.primaryColor};
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active::after {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cookie-banner {
|
||||
margin: 0;
|
||||
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTML GENERATION
|
||||
// =============================================================================
|
||||
|
||||
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
|
||||
const categoriesHTML = config.categories
|
||||
.map((cat) => {
|
||||
const isRequired = cat.isRequired
|
||||
return `
|
||||
<div class="cookie-banner-category" data-category="${cat.id}">
|
||||
<div class="cookie-banner-category-info">
|
||||
<div class="cookie-banner-category-name">${cat.name.de}</div>
|
||||
<div class="cookie-banner-category-desc">${cat.description.de}</div>
|
||||
</div>
|
||||
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
|
||||
data-category="${cat.id}"
|
||||
data-required="${isRequired}"></div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
|
||||
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
|
||||
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
|
||||
<div class="cookie-banner-description">${config.texts.description.de}</div>
|
||||
|
||||
<div class="cookie-banner-buttons">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
|
||||
${config.texts.rejectAll.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
|
||||
${config.texts.customize.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
|
||||
${config.texts.acceptAll.de}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cookie-banner-details" id="cookieBannerDetails">
|
||||
${categoriesHTML}
|
||||
<div class="cookie-banner-buttons" style="margin-top: 16px;">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
|
||||
${config.texts.save.de}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
|
||||
${config.texts.privacyPolicyLink.de}
|
||||
</a>
|
||||
</div>
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JS GENERATION
|
||||
// =============================================================================
|
||||
|
||||
function generateJS(config: CookieBannerConfig): string {
|
||||
const categoryIds = config.categories.map((c) => c.id)
|
||||
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
|
||||
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const COOKIE_NAME = 'cookie_consent';
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
const CATEGORIES = ${JSON.stringify(categoryIds)};
|
||||
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
|
||||
|
||||
function getConsent() {
|
||||
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveConsent(consent) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
|
||||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
|
||||
';expires=' + date.toUTCString() +
|
||||
';path=/;SameSite=Lax';
|
||||
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
|
||||
}
|
||||
|
||||
function hasConsent(category) {
|
||||
const consent = getConsent();
|
||||
if (!consent) return REQUIRED_CATEGORIES.includes(category);
|
||||
return consent[category] === true;
|
||||
}
|
||||
|
||||
function initBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
const details = document.getElementById('cookieBannerDetails');
|
||||
|
||||
if (!banner) return;
|
||||
|
||||
const consent = getConsent();
|
||||
if (consent) return;
|
||||
|
||||
setTimeout(() => {
|
||||
banner.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
}, 500);
|
||||
|
||||
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = true);
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
|
||||
details.classList.toggle('active');
|
||||
});
|
||||
|
||||
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => {
|
||||
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
|
||||
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
|
||||
});
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
|
||||
if (toggle.dataset.required === 'true') return;
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
overlay?.addEventListener('click', () => {
|
||||
// Don't close - user must make a choice
|
||||
});
|
||||
}
|
||||
|
||||
function closeBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
banner?.classList.remove('active');
|
||||
overlay?.classList.remove('active');
|
||||
}
|
||||
|
||||
window.CookieConsent = {
|
||||
getConsent,
|
||||
saveConsent,
|
||||
hasConsent,
|
||||
show: () => {
|
||||
document.getElementById('cookieBanner')?.classList.add('active');
|
||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||||
},
|
||||
hide: closeBanner
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBanner);
|
||||
} else {
|
||||
initBanner();
|
||||
}
|
||||
})();
|
||||
`.trim()
|
||||
}
|
||||
@@ -1,595 +1,18 @@
|
||||
/**
|
||||
* Cookie Banner Generator
|
||||
* Cookie Banner Generator — barrel re-export
|
||||
*
|
||||
* Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog.
|
||||
* Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet.
|
||||
* Split into:
|
||||
* - cookie-banner-config.ts (defaults, category generation, config builder)
|
||||
* - cookie-banner-embed.ts (CSS, HTML, JS embed code generation)
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CookieCategory,
|
||||
CookieBannerCategory,
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerTexts,
|
||||
CookieBannerEmbedCode,
|
||||
CookieInfo,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Holt den lokalisierten Text
|
||||
*/
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COOKIE BANNER CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Standard Cookie Banner Texte
|
||||
*/
|
||||
export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
|
||||
title: {
|
||||
de: 'Cookie-Einstellungen',
|
||||
en: 'Cookie Settings',
|
||||
},
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
|
||||
},
|
||||
acceptAll: {
|
||||
de: 'Alle akzeptieren',
|
||||
en: 'Accept All',
|
||||
},
|
||||
rejectAll: {
|
||||
de: 'Nur notwendige',
|
||||
en: 'Essential Only',
|
||||
},
|
||||
customize: {
|
||||
de: 'Einstellungen',
|
||||
en: 'Customize',
|
||||
},
|
||||
save: {
|
||||
de: 'Auswahl speichern',
|
||||
en: 'Save Selection',
|
||||
},
|
||||
privacyPolicyLink: {
|
||||
de: 'Mehr in unserer Datenschutzerklaerung',
|
||||
en: 'More in our Privacy Policy',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Styling fuer Cookie Banner
|
||||
*/
|
||||
export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1', // Indigo
|
||||
secondaryColor: '#f1f5f9', // Slate-100
|
||||
textColor: '#1e293b', // Slate-800
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
maxWidth: 480,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert Cookie-Banner Kategorien aus Datenpunkten
|
||||
*/
|
||||
export function generateCookieCategories(
|
||||
dataPoints: DataPoint[]
|
||||
): CookieBannerCategory[] {
|
||||
// Filtere nur Datenpunkte mit Cookie-Kategorie
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
// Erstelle die Kategorien basierend auf den Defaults
|
||||
return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
|
||||
// Filtere die Datenpunkte fuer diese Kategorie
|
||||
const categoryDataPoints = cookieDataPoints.filter(
|
||||
(dp) => dp.cookieCategory === defaultCat.id
|
||||
)
|
||||
|
||||
// Erstelle Cookie-Infos aus den Datenpunkten
|
||||
const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
|
||||
name: dp.code,
|
||||
provider: 'First Party',
|
||||
purpose: dp.purpose,
|
||||
expiry: getExpiryFromRetention(dp.retentionPeriod),
|
||||
type: 'FIRST_PARTY',
|
||||
}))
|
||||
|
||||
return {
|
||||
...defaultCat,
|
||||
dataPointIds: categoryDataPoints.map((dp) => dp.id),
|
||||
cookies,
|
||||
}
|
||||
}).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert Retention Period zu Cookie-Expiry String
|
||||
*/
|
||||
function getExpiryFromRetention(retention: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'24_HOURS': '24 Stunden / 24 hours',
|
||||
'30_DAYS': '30 Tage / 30 days',
|
||||
'90_DAYS': '90 Tage / 90 days',
|
||||
'12_MONTHS': '1 Jahr / 1 year',
|
||||
'24_MONTHS': '2 Jahre / 2 years',
|
||||
'36_MONTHS': '3 Jahre / 3 years',
|
||||
'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
|
||||
'UNTIL_PURPOSE_FULFILLED': 'Session',
|
||||
'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
|
||||
}
|
||||
return mapping[retention] || 'Session'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die vollstaendige Cookie Banner Konfiguration
|
||||
*/
|
||||
export function generateCookieBannerConfig(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
customTexts?: Partial<CookieBannerTexts>,
|
||||
customStyling?: Partial<CookieBannerStyling>
|
||||
): CookieBannerConfig {
|
||||
const categories = generateCookieCategories(dataPoints)
|
||||
|
||||
return {
|
||||
id: `cookie-banner-${tenantId}`,
|
||||
tenantId,
|
||||
categories,
|
||||
styling: {
|
||||
...DEFAULT_COOKIE_BANNER_STYLING,
|
||||
...customStyling,
|
||||
},
|
||||
texts: {
|
||||
...DEFAULT_COOKIE_BANNER_TEXTS,
|
||||
...customTexts,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMBED CODE GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert den Embed-Code fuer den Cookie Banner
|
||||
*/
|
||||
export function generateEmbedCode(
|
||||
config: CookieBannerConfig,
|
||||
privacyPolicyUrl: string = '/datenschutz'
|
||||
): CookieBannerEmbedCode {
|
||||
const css = generateCSS(config.styling)
|
||||
const html = generateHTML(config, privacyPolicyUrl)
|
||||
const js = generateJS(config)
|
||||
|
||||
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
|
||||
|
||||
return {
|
||||
html,
|
||||
css,
|
||||
js,
|
||||
scriptTag,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das CSS fuer den Cookie Banner
|
||||
*/
|
||||
function generateCSS(styling: CookieBannerStyling): string {
|
||||
const positionStyles: Record<string, string> = {
|
||||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||||
TOP: 'top: 0; left: 0; right: 0;',
|
||||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
||||
}
|
||||
|
||||
const isDark = styling.theme === 'DARK'
|
||||
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
|
||||
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
|
||||
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||||
|
||||
return `
|
||||
/* Cookie Banner Styles */
|
||||
.cookie-banner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 9998;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
${positionStyles[styling.position]}
|
||||
z-index: 9999;
|
||||
background: ${bgColor};
|
||||
color: ${textColor};
|
||||
border-radius: ${styling.borderRadius || 12}px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
max-width: ${styling.maxWidth}px;
|
||||
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cookie-banner-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 12px 20px;
|
||||
border-radius: ${(styling.borderRadius || 12) / 2}px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary {
|
||||
background: ${styling.primaryColor};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary {
|
||||
background: ${styling.secondaryColor || borderColor};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.cookie-banner-link {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: ${styling.primaryColor};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cookie-banner-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Category Details */
|
||||
.cookie-banner-details {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid ${borderColor};
|
||||
padding-top: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cookie-banner-details.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cookie-banner-category {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
.cookie-banner-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cookie-banner-category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-category-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cookie-banner-category-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
background: ${borderColor};
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active {
|
||||
background: ${styling.primaryColor};
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active::after {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cookie-banner {
|
||||
margin: 0;
|
||||
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das HTML fuer den Cookie Banner
|
||||
*/
|
||||
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
|
||||
const categoriesHTML = config.categories
|
||||
.map((cat) => {
|
||||
const isRequired = cat.isRequired
|
||||
return `
|
||||
<div class="cookie-banner-category" data-category="${cat.id}">
|
||||
<div class="cookie-banner-category-info">
|
||||
<div class="cookie-banner-category-name">${cat.name.de}</div>
|
||||
<div class="cookie-banner-category-desc">${cat.description.de}</div>
|
||||
</div>
|
||||
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
|
||||
data-category="${cat.id}"
|
||||
data-required="${isRequired}"></div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
|
||||
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
|
||||
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
|
||||
<div class="cookie-banner-description">${config.texts.description.de}</div>
|
||||
|
||||
<div class="cookie-banner-buttons">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
|
||||
${config.texts.rejectAll.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
|
||||
${config.texts.customize.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
|
||||
${config.texts.acceptAll.de}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cookie-banner-details" id="cookieBannerDetails">
|
||||
${categoriesHTML}
|
||||
<div class="cookie-banner-buttons" style="margin-top: 16px;">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
|
||||
${config.texts.save.de}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
|
||||
${config.texts.privacyPolicyLink.de}
|
||||
</a>
|
||||
</div>
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das JavaScript fuer den Cookie Banner
|
||||
*/
|
||||
function generateJS(config: CookieBannerConfig): string {
|
||||
const categoryIds = config.categories.map((c) => c.id)
|
||||
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
|
||||
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const COOKIE_NAME = 'cookie_consent';
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
const CATEGORIES = ${JSON.stringify(categoryIds)};
|
||||
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
|
||||
|
||||
// Get consent from cookie
|
||||
function getConsent() {
|
||||
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save consent to cookie
|
||||
function saveConsent(consent) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
|
||||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
|
||||
';expires=' + date.toUTCString() +
|
||||
';path=/;SameSite=Lax';
|
||||
|
||||
// Dispatch event
|
||||
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
|
||||
}
|
||||
|
||||
// Check if category is consented
|
||||
function hasConsent(category) {
|
||||
const consent = getConsent();
|
||||
if (!consent) return REQUIRED_CATEGORIES.includes(category);
|
||||
return consent[category] === true;
|
||||
}
|
||||
|
||||
// Initialize banner
|
||||
function initBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
const details = document.getElementById('cookieBannerDetails');
|
||||
|
||||
if (!banner) return;
|
||||
|
||||
const consent = getConsent();
|
||||
if (consent) {
|
||||
// User has already consented
|
||||
return;
|
||||
}
|
||||
|
||||
// Show banner
|
||||
setTimeout(() => {
|
||||
banner.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
}, 500);
|
||||
|
||||
// Accept all
|
||||
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = true);
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Reject all (only essential)
|
||||
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Customize
|
||||
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
|
||||
details.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Save selection
|
||||
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => {
|
||||
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
|
||||
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
|
||||
});
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Toggle handlers
|
||||
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
|
||||
if (toggle.dataset.required === 'true') return;
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay?.addEventListener('click', () => {
|
||||
// Don't close - user must make a choice
|
||||
});
|
||||
}
|
||||
|
||||
function closeBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
banner?.classList.remove('active');
|
||||
overlay?.classList.remove('active');
|
||||
}
|
||||
|
||||
// Expose API
|
||||
window.CookieConsent = {
|
||||
getConsent,
|
||||
saveConsent,
|
||||
hasConsent,
|
||||
show: () => {
|
||||
document.getElementById('cookieBanner')?.classList.add('active');
|
||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||||
},
|
||||
hide: closeBanner
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBanner);
|
||||
} else {
|
||||
initBanner();
|
||||
}
|
||||
})();
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// Note: All exports are defined inline with 'export const' and 'export function'
|
||||
export {
|
||||
DEFAULT_COOKIE_BANNER_TEXTS,
|
||||
DEFAULT_COOKIE_BANNER_STYLING,
|
||||
generateCookieCategories,
|
||||
generateCookieBannerConfig,
|
||||
} from './cookie-banner-config'
|
||||
|
||||
export {
|
||||
generateEmbedCode,
|
||||
} from './cookie-banner-embed'
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Privacy Policy Renderers & Main Generator
|
||||
*
|
||||
* Cookies section, changes section, rendering (HTML/Markdown),
|
||||
* and the main generatePrivacyPolicy entry point.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
GeneratedPrivacyPolicy,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
import { RETENTION_MATRIX } from '../catalog/loader'
|
||||
|
||||
import {
|
||||
formatDate,
|
||||
generateControllerSection,
|
||||
generateDataCollectionSection,
|
||||
generatePurposesSection,
|
||||
generateLegalBasisSection,
|
||||
generateRecipientsSection,
|
||||
generateRetentionSection,
|
||||
generateSpecialCategoriesSection,
|
||||
generateRightsSection,
|
||||
} from './privacy-policy-sections'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER
|
||||
// =============================================================================
|
||||
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS (cookies + changes)
|
||||
// =============================================================================
|
||||
|
||||
export function generateCookiesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '8. Cookies und aehnliche Technologien',
|
||||
en: '8. Cookies and Similar Technologies',
|
||||
}
|
||||
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
if (cookieDataPoints.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
||||
en: 'We do not use cookies on this website.',
|
||||
}
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: false,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
||||
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
||||
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
||||
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
||||
|
||||
const sections: string[] = []
|
||||
|
||||
if (essential.length > 0) {
|
||||
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
||||
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (performance.length > 0) {
|
||||
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
||||
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (personalization.length > 0) {
|
||||
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
||||
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (externalMedia.length > 0) {
|
||||
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
||||
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
||||
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateChangesSection(
|
||||
version: string,
|
||||
date: Date,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
||||
en: '9. Changes to this Privacy Policy',
|
||||
}
|
||||
|
||||
const formattedDate = formatDate(date, language)
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
||||
|
||||
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
||||
|
||||
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
||||
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
||||
|
||||
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
||||
|
||||
The new privacy policy will then apply for your next visit.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'changes',
|
||||
order: 9,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function generatePrivacyPolicySections(
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
version: string = '1.0.0'
|
||||
): PrivacyPolicySection[] {
|
||||
const now = new Date()
|
||||
|
||||
const sections: PrivacyPolicySection[] = [
|
||||
generateControllerSection(companyInfo, language),
|
||||
generateDataCollectionSection(dataPoints, language),
|
||||
generatePurposesSection(dataPoints, language),
|
||||
generateLegalBasisSection(dataPoints, language),
|
||||
generateRecipientsSection(dataPoints, language),
|
||||
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
||||
]
|
||||
|
||||
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
||||
if (specialCategoriesSection) {
|
||||
sections.push(specialCategoriesSection)
|
||||
}
|
||||
|
||||
sections.push(
|
||||
generateRightsSection(language),
|
||||
generateCookiesSection(dataPoints, language),
|
||||
generateChangesSection(version, now, language)
|
||||
)
|
||||
|
||||
sections.forEach((section, index) => {
|
||||
section.order = index + 1
|
||||
const titleDe = section.title.de
|
||||
const titleEn = section.title.en
|
||||
if (titleDe.match(/^\d+[a-z]?\./)) {
|
||||
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
if (titleEn.match(/^\d+[a-z]?\./)) {
|
||||
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
export function generatePrivacyPolicy(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat = 'HTML'
|
||||
): GeneratedPrivacyPolicy {
|
||||
const version = '1.0.0'
|
||||
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
||||
const content = renderPrivacyPolicy(sections, language, format)
|
||||
|
||||
return {
|
||||
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
||||
tenantId,
|
||||
language,
|
||||
sections,
|
||||
companyInfo,
|
||||
generatedAt: new Date(),
|
||||
version,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RENDERERS
|
||||
// =============================================================================
|
||||
|
||||
function renderPrivacyPolicy(
|
||||
sections: PrivacyPolicySection[],
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'HTML':
|
||||
return renderAsHTML(sections, language)
|
||||
case 'MARKDOWN':
|
||||
return renderAsMarkdown(sections, language)
|
||||
default:
|
||||
return renderAsMarkdown(sections, language)
|
||||
}
|
||||
}
|
||||
|
||||
export function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsHTML = sections
|
||||
.map((section) => {
|
||||
const content = t(section.content, language)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/### (.+)/g, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
||||
|
||||
return `
|
||||
<section id="${section.id}">
|
||||
<h2>${t(section.title, language)}</h2>
|
||||
<p>${content}</p>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
||||
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
||||
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
||||
p { margin: 1rem 0; }
|
||||
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
||||
li { margin: 0.5rem 0; }
|
||||
strong { font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
${sectionsHTML}
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
export function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsMarkdown = sections
|
||||
.map((section) => {
|
||||
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
||||
})
|
||||
.join('\n\n---\n\n')
|
||||
|
||||
return `# ${title}\n\n${sectionsMarkdown}`
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
/**
|
||||
* Privacy Policy Section Generators
|
||||
*
|
||||
* Generiert die 9 Abschnitte der Datenschutzerklaerung (DSI)
|
||||
* aus dem Datenpunktkatalog.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
SupportedLanguage,
|
||||
LocalizedText,
|
||||
RetentionMatrixEntry,
|
||||
LegalBasis,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// KONSTANTEN
|
||||
// =============================================================================
|
||||
|
||||
const ALL_CATEGORIES: DataPointCategory[] = [
|
||||
'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT',
|
||||
'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION',
|
||||
'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA',
|
||||
'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA',
|
||||
'AI_DATA', 'SECURITY',
|
||||
]
|
||||
|
||||
const ALL_LEGAL_BASES: LegalBasis[] = [
|
||||
'CONTRACT', 'CONSENT', 'EXPLICIT_CONSENT', 'LEGITIMATE_INTEREST',
|
||||
'LEGAL_OBLIGATION', 'VITAL_INTERESTS', 'PUBLIC_INTEREST',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
|
||||
const grouped = new Map<LegalBasis, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.legalBasis) || []
|
||||
grouped.set(dp.legalBasis, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
||||
const thirdParties = new Set<string>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
thirdParties.add(recipient)
|
||||
}
|
||||
}
|
||||
return Array.from(thirdParties).sort()
|
||||
}
|
||||
|
||||
export function formatDate(date: Date, language: SupportedLanguage): string {
|
||||
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS
|
||||
// =============================================================================
|
||||
|
||||
export function generateControllerSection(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '1. Verantwortlicher',
|
||||
en: '1. Data Controller',
|
||||
}
|
||||
|
||||
const dpoSection = companyInfo.dpoName
|
||||
? language === 'de'
|
||||
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
|
||||
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
|
||||
: ''
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
en: `The controller responsible for data processing on this website is:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'controller',
|
||||
order: 1,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateDataCollectionSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '2. Erhobene personenbezogene Daten',
|
||||
en: '2. Personal Data We Collect',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const categoryData = grouped.get(category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
if (!categoryMeta) continue
|
||||
|
||||
const categoryTitle = t(categoryMeta.name, language)
|
||||
|
||||
let categoryNote = ''
|
||||
if (category === 'HEALTH_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
|
||||
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
|
||||
} else if (category === 'EMPLOYEE_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
|
||||
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
|
||||
} else if (category === 'AI_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
|
||||
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
|
||||
}
|
||||
|
||||
const dataList = categoryData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
|
||||
en: 'We collect and process the following personal data:',
|
||||
}
|
||||
|
||||
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
|
||||
? {
|
||||
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
|
||||
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
|
||||
}
|
||||
: { de: '', en: '' }
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'data-collection',
|
||||
order: 2,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePurposesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '3. Zwecke der Datenverarbeitung',
|
||||
en: '3. Purposes of Data Processing',
|
||||
}
|
||||
|
||||
const purposes = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const purpose = t(dp.purpose, language)
|
||||
const existing = purposes.get(purpose) || []
|
||||
purposes.set(purpose, [...existing, dp])
|
||||
}
|
||||
|
||||
const purposeList = Array.from(purposes.entries())
|
||||
.map(([purpose, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
|
||||
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'purposes',
|
||||
order: 3,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateLegalBasisSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '4. Rechtsgrundlagen der Verarbeitung',
|
||||
en: '4. Legal Basis for Processing',
|
||||
}
|
||||
|
||||
const grouped = groupByLegalBasis(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const basis of ALL_LEGAL_BASES) {
|
||||
const basisData = grouped.get(basis)
|
||||
if (!basisData || basisData.length === 0) continue
|
||||
|
||||
const basisInfo = LEGAL_BASIS_INFO[basis]
|
||||
if (!basisInfo) continue
|
||||
|
||||
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
|
||||
const basisDesc = t(basisInfo.description, language)
|
||||
|
||||
let additionalWarning = ''
|
||||
if (basis === 'EXPLICIT_CONSENT') {
|
||||
additionalWarning = language === 'de'
|
||||
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
|
||||
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
|
||||
}
|
||||
|
||||
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
|
||||
const dataList = basisData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
|
||||
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'legal-basis',
|
||||
order: 4,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRecipientsSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '5. Empfaenger und Datenweitergabe',
|
||||
en: '5. Recipients and Data Sharing',
|
||||
}
|
||||
|
||||
const thirdParties = extractThirdParties(dataPoints)
|
||||
|
||||
if (thirdParties.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
||||
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
||||
}
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRetentionSection(
|
||||
dataPoints: DataPoint[],
|
||||
retentionMatrix: RetentionMatrixEntry[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '6. Speicherdauer',
|
||||
en: '6. Data Retention',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const entry of retentionMatrix) {
|
||||
const categoryData = grouped.get(entry.category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryName = t(entry.categoryName, language)
|
||||
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
||||
|
||||
const dataRetention = categoryData
|
||||
.map((dp) => {
|
||||
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
||||
return `- ${t(dp.name, language)}: ${period}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
||||
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'retention',
|
||||
order: 6,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
if (specialCategoryDataPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title: LocalizedText = {
|
||||
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
||||
}
|
||||
|
||||
const dataList = specialCategoryDataPoints
|
||||
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Ihre ausdrueckliche Einwilligung
|
||||
|
||||
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
||||
|
||||
### Ihre Rechte bei Art. 9 Daten
|
||||
|
||||
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
||||
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
||||
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
||||
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
||||
|
||||
### Besondere Schutzmassnahmen
|
||||
|
||||
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
||||
- Ende-zu-Ende-Verschluesselung
|
||||
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
||||
- Audit-Logging aller Zugriffe
|
||||
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
||||
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Your Explicit Consent
|
||||
|
||||
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
||||
|
||||
### Your Rights Regarding Art. 9 Data
|
||||
|
||||
- You can **withdraw your consent at any time**
|
||||
- Withdrawal does not affect the lawfulness of previous processing
|
||||
- Upon withdrawal, your data will be deleted immediately
|
||||
- You have the right to **access, rectification, and erasure**
|
||||
|
||||
### Special Protection Measures
|
||||
|
||||
For this sensitive data, we have implemented special technical and organizational measures:
|
||||
- End-to-end encryption
|
||||
- Strict access control (need-to-know principle)
|
||||
- Audit logging of all access
|
||||
- Regular data protection impact assessments`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'special-categories',
|
||||
order: 6.5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '7. Ihre Rechte als betroffene Person',
|
||||
en: '7. Your Rights as a Data Subject',
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
||||
|
||||
### Auskunftsrecht (Art. 15 DSGVO)
|
||||
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
||||
|
||||
### Recht auf Berichtigung (Art. 16 DSGVO)
|
||||
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
||||
|
||||
### Recht auf Loeschung (Art. 17 DSGVO)
|
||||
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||
|
||||
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
||||
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
||||
|
||||
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
||||
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
||||
|
||||
### Widerspruchsrecht (Art. 21 DSGVO)
|
||||
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
||||
|
||||
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
||||
|
||||
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
||||
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
||||
|
||||
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
||||
en: `You have the following rights regarding your personal data:
|
||||
|
||||
### Right of Access (Art. 15 GDPR)
|
||||
You have the right to request information about the personal data we process about you.
|
||||
|
||||
### Right to Rectification (Art. 16 GDPR)
|
||||
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
||||
|
||||
### Right to Erasure (Art. 17 GDPR)
|
||||
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
||||
|
||||
### Right to Restriction of Processing (Art. 18 GDPR)
|
||||
You have the right to request the restriction of processing of your data.
|
||||
|
||||
### Right to Data Portability (Art. 20 GDPR)
|
||||
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
||||
|
||||
### Right to Object (Art. 21 GDPR)
|
||||
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
||||
|
||||
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
||||
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
||||
|
||||
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
||||
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
||||
|
||||
**To exercise your rights, please contact us using the contact details provided above.**`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'rights',
|
||||
order: 7,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
@@ -1,954 +1,11 @@
|
||||
/**
|
||||
* Privacy Policy Generator
|
||||
* Privacy Policy Generator — barrel re-export
|
||||
*
|
||||
* Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog.
|
||||
* Die DSI wird aus 9 Abschnitten generiert:
|
||||
*
|
||||
* 1. Verantwortlicher (companyInfo)
|
||||
* 2. Erhobene Daten (dataPoints nach Kategorie)
|
||||
* 3. Verarbeitungszwecke (dataPoints.purpose)
|
||||
* 4. Rechtsgrundlagen (dataPoints.legalBasis)
|
||||
* 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients)
|
||||
* 6. Speicherdauer (retentionMatrix)
|
||||
* 7. Betroffenenrechte (statischer Text + Links)
|
||||
* 8. Cookies (cookieCategory-basiert)
|
||||
* 9. Aenderungen (statischer Text + Versionierung)
|
||||
* Split into:
|
||||
* - privacy-policy-sections.ts (section generators 1-7)
|
||||
* - privacy-policy-renderers.ts (sections 8-9, renderers, main generator)
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
GeneratedPrivacyPolicy,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
LocalizedText,
|
||||
RetentionMatrixEntry,
|
||||
LegalBasis,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
ARTICLE_9_WARNING,
|
||||
} from '../types'
|
||||
import { RETENTION_MATRIX } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge
|
||||
// =============================================================================
|
||||
|
||||
const ALL_CATEGORIES: DataPointCategory[] = [
|
||||
'MASTER_DATA', // A
|
||||
'CONTACT_DATA', // B
|
||||
'AUTHENTICATION', // C
|
||||
'CONSENT', // D
|
||||
'COMMUNICATION', // E
|
||||
'PAYMENT', // F
|
||||
'USAGE_DATA', // G
|
||||
'LOCATION', // H
|
||||
'DEVICE_DATA', // I
|
||||
'MARKETING', // J
|
||||
'ANALYTICS', // K
|
||||
'SOCIAL_MEDIA', // L
|
||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
||||
'CONTRACT_DATA', // O
|
||||
'LOG_DATA', // P
|
||||
'AI_DATA', // Q - AI Act
|
||||
'SECURITY', // R
|
||||
]
|
||||
|
||||
// Alle Rechtsgrundlagen in der richtigen Reihenfolge
|
||||
const ALL_LEGAL_BASES: LegalBasis[] = [
|
||||
'CONTRACT',
|
||||
'CONSENT',
|
||||
'EXPLICIT_CONSENT',
|
||||
'LEGITIMATE_INTEREST',
|
||||
'LEGAL_OBLIGATION',
|
||||
'VITAL_INTERESTS',
|
||||
'PUBLIC_INTEREST',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Holt den lokalisierten Text
|
||||
*/
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Kategorie
|
||||
*/
|
||||
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Rechtsgrundlage
|
||||
*/
|
||||
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
|
||||
const grouped = new Map<LegalBasis, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.legalBasis) || []
|
||||
grouped.set(dp.legalBasis, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert alle einzigartigen Drittanbieter
|
||||
*/
|
||||
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
||||
const thirdParties = new Set<string>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
thirdParties.add(recipient)
|
||||
}
|
||||
}
|
||||
return Array.from(thirdParties).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein Datum fuer die Anzeige
|
||||
*/
|
||||
function formatDate(date: Date, language: SupportedLanguage): string {
|
||||
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Abschnitt 1: Verantwortlicher
|
||||
*/
|
||||
function generateControllerSection(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '1. Verantwortlicher',
|
||||
en: '1. Data Controller',
|
||||
}
|
||||
|
||||
const dpoSection = companyInfo.dpoName
|
||||
? language === 'de'
|
||||
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
|
||||
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
|
||||
: ''
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
en: `The controller responsible for data processing on this website is:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'controller',
|
||||
order: 1,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 2: Erhobene Daten (18 Kategorien)
|
||||
*/
|
||||
function generateDataCollectionSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '2. Erhobene personenbezogene Daten',
|
||||
en: '2. Personal Data We Collect',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
// Prüfe ob Art. 9 Daten enthalten sind
|
||||
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const categoryData = grouped.get(category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
if (!categoryMeta) continue
|
||||
|
||||
const categoryTitle = t(categoryMeta.name, language)
|
||||
|
||||
// Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten)
|
||||
let categoryNote = ''
|
||||
if (category === 'HEALTH_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
|
||||
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
|
||||
} else if (category === 'EMPLOYEE_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
|
||||
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
|
||||
} else if (category === 'AI_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
|
||||
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
|
||||
}
|
||||
|
||||
const dataList = categoryData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
|
||||
en: 'We collect and process the following personal data:',
|
||||
}
|
||||
|
||||
// Zusätzlicher Hinweis für Art. 9 Daten
|
||||
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
|
||||
? {
|
||||
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
|
||||
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
|
||||
}
|
||||
: { de: '', en: '' }
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'data-collection',
|
||||
order: 2,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 3: Verarbeitungszwecke
|
||||
*/
|
||||
function generatePurposesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '3. Zwecke der Datenverarbeitung',
|
||||
en: '3. Purposes of Data Processing',
|
||||
}
|
||||
|
||||
// Gruppiere nach Zweck (unique purposes)
|
||||
const purposes = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const purpose = t(dp.purpose, language)
|
||||
const existing = purposes.get(purpose) || []
|
||||
purposes.set(purpose, [...existing, dp])
|
||||
}
|
||||
|
||||
const purposeList = Array.from(purposes.entries())
|
||||
.map(([purpose, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
|
||||
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'purposes',
|
||||
order: 3,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen)
|
||||
*/
|
||||
function generateLegalBasisSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '4. Rechtsgrundlagen der Verarbeitung',
|
||||
en: '4. Legal Basis for Processing',
|
||||
}
|
||||
|
||||
const grouped = groupByLegalBasis(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
// Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge
|
||||
for (const basis of ALL_LEGAL_BASES) {
|
||||
const basisData = grouped.get(basis)
|
||||
if (!basisData || basisData.length === 0) continue
|
||||
|
||||
const basisInfo = LEGAL_BASIS_INFO[basis]
|
||||
if (!basisInfo) continue
|
||||
|
||||
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
|
||||
const basisDesc = t(basisInfo.description, language)
|
||||
|
||||
// Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen
|
||||
let additionalWarning = ''
|
||||
if (basis === 'EXPLICIT_CONSENT') {
|
||||
additionalWarning = language === 'de'
|
||||
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
|
||||
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
|
||||
}
|
||||
|
||||
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
|
||||
const dataList = basisData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
|
||||
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'legal-basis',
|
||||
order: 4,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 5: Empfaenger / Dritte
|
||||
*/
|
||||
function generateRecipientsSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '5. Empfaenger und Datenweitergabe',
|
||||
en: '5. Recipients and Data Sharing',
|
||||
}
|
||||
|
||||
const thirdParties = extractThirdParties(dataPoints)
|
||||
|
||||
if (thirdParties.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
||||
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
||||
}
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Drittanbieter
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 6: Speicherdauer
|
||||
*/
|
||||
function generateRetentionSection(
|
||||
dataPoints: DataPoint[],
|
||||
retentionMatrix: RetentionMatrixEntry[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '6. Speicherdauer',
|
||||
en: '6. Data Retention',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const entry of retentionMatrix) {
|
||||
const categoryData = grouped.get(entry.category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryName = t(entry.categoryName, language)
|
||||
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
||||
|
||||
const dataRetention = categoryData
|
||||
.map((dp) => {
|
||||
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
||||
return `- ${t(dp.name, language)}: ${period}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
||||
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'retention',
|
||||
order: 6,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO)
|
||||
* Wird nur generiert, wenn Art. 9 Daten vorhanden sind
|
||||
*/
|
||||
function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
// Filtere Art. 9 Datenpunkte
|
||||
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
if (specialCategoryDataPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title: LocalizedText = {
|
||||
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
||||
}
|
||||
|
||||
const dataList = specialCategoryDataPoints
|
||||
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Ihre ausdrueckliche Einwilligung
|
||||
|
||||
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
||||
|
||||
### Ihre Rechte bei Art. 9 Daten
|
||||
|
||||
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
||||
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
||||
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
||||
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
||||
|
||||
### Besondere Schutzmassnahmen
|
||||
|
||||
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
||||
- Ende-zu-Ende-Verschluesselung
|
||||
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
||||
- Audit-Logging aller Zugriffe
|
||||
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
||||
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Your Explicit Consent
|
||||
|
||||
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
||||
|
||||
### Your Rights Regarding Art. 9 Data
|
||||
|
||||
- You can **withdraw your consent at any time**
|
||||
- Withdrawal does not affect the lawfulness of previous processing
|
||||
- Upon withdrawal, your data will be deleted immediately
|
||||
- You have the right to **access, rectification, and erasure**
|
||||
|
||||
### Special Protection Measures
|
||||
|
||||
For this sensitive data, we have implemented special technical and organizational measures:
|
||||
- End-to-end encryption
|
||||
- Strict access control (need-to-know principle)
|
||||
- Audit logging of all access
|
||||
- Regular data protection impact assessments`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'special-categories',
|
||||
order: 6.5, // Zwischen Speicherdauer (6) und Rechte (7)
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 7: Betroffenenrechte
|
||||
*/
|
||||
function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '7. Ihre Rechte als betroffene Person',
|
||||
en: '7. Your Rights as a Data Subject',
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
||||
|
||||
### Auskunftsrecht (Art. 15 DSGVO)
|
||||
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
||||
|
||||
### Recht auf Berichtigung (Art. 16 DSGVO)
|
||||
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
||||
|
||||
### Recht auf Loeschung (Art. 17 DSGVO)
|
||||
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||
|
||||
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
||||
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
||||
|
||||
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
||||
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
||||
|
||||
### Widerspruchsrecht (Art. 21 DSGVO)
|
||||
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
||||
|
||||
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
||||
|
||||
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
||||
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
||||
|
||||
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
||||
en: `You have the following rights regarding your personal data:
|
||||
|
||||
### Right of Access (Art. 15 GDPR)
|
||||
You have the right to request information about the personal data we process about you.
|
||||
|
||||
### Right to Rectification (Art. 16 GDPR)
|
||||
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
||||
|
||||
### Right to Erasure (Art. 17 GDPR)
|
||||
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
||||
|
||||
### Right to Restriction of Processing (Art. 18 GDPR)
|
||||
You have the right to request the restriction of processing of your data.
|
||||
|
||||
### Right to Data Portability (Art. 20 GDPR)
|
||||
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
||||
|
||||
### Right to Object (Art. 21 GDPR)
|
||||
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
||||
|
||||
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
||||
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
||||
|
||||
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
||||
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
||||
|
||||
**To exercise your rights, please contact us using the contact details provided above.**`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'rights',
|
||||
order: 7,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 8: Cookies
|
||||
*/
|
||||
function generateCookiesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '8. Cookies und aehnliche Technologien',
|
||||
en: '8. Cookies and Similar Technologies',
|
||||
}
|
||||
|
||||
// Filtere Datenpunkte mit Cookie-Kategorie
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
if (cookieDataPoints.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
||||
en: 'We do not use cookies on this website.',
|
||||
}
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: false,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Cookie-Kategorie
|
||||
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
||||
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
||||
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
||||
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
||||
|
||||
const sections: string[] = []
|
||||
|
||||
if (essential.length > 0) {
|
||||
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
||||
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (performance.length > 0) {
|
||||
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
||||
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (personalization.length > 0) {
|
||||
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
||||
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (externalMedia.length > 0) {
|
||||
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
||||
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
||||
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 9: Aenderungen
|
||||
*/
|
||||
function generateChangesSection(
|
||||
version: string,
|
||||
date: Date,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
||||
en: '9. Changes to this Privacy Policy',
|
||||
}
|
||||
|
||||
const formattedDate = formatDate(date, language)
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
||||
|
||||
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
||||
|
||||
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
||||
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
||||
|
||||
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
||||
|
||||
The new privacy policy will then apply for your next visit.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'changes',
|
||||
order: 9,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9)
|
||||
*/
|
||||
export function generatePrivacyPolicySections(
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
version: string = '1.0.0'
|
||||
): PrivacyPolicySection[] {
|
||||
const now = new Date()
|
||||
|
||||
const sections: PrivacyPolicySection[] = [
|
||||
generateControllerSection(companyInfo, language),
|
||||
generateDataCollectionSection(dataPoints, language),
|
||||
generatePurposesSection(dataPoints, language),
|
||||
generateLegalBasisSection(dataPoints, language),
|
||||
generateRecipientsSection(dataPoints, language),
|
||||
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
||||
]
|
||||
|
||||
// Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden
|
||||
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
||||
if (specialCategoriesSection) {
|
||||
sections.push(specialCategoriesSection)
|
||||
}
|
||||
|
||||
sections.push(
|
||||
generateRightsSection(language),
|
||||
generateCookiesSection(dataPoints, language),
|
||||
generateChangesSection(version, now, language)
|
||||
)
|
||||
|
||||
// Abschnittsnummern neu vergeben
|
||||
sections.forEach((section, index) => {
|
||||
section.order = index + 1
|
||||
// Titel-Nummer aktualisieren
|
||||
const titleDe = section.title.de
|
||||
const titleEn = section.title.en
|
||||
if (titleDe.match(/^\d+[a-z]?\./)) {
|
||||
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
if (titleEn.match(/^\d+[a-z]?\./)) {
|
||||
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die vollstaendige Privacy Policy
|
||||
*/
|
||||
export function generatePrivacyPolicy(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat = 'HTML'
|
||||
): GeneratedPrivacyPolicy {
|
||||
const version = '1.0.0'
|
||||
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
||||
|
||||
// Generiere den Inhalt
|
||||
const content = renderPrivacyPolicy(sections, language, format)
|
||||
|
||||
return {
|
||||
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
||||
tenantId,
|
||||
language,
|
||||
sections,
|
||||
companyInfo,
|
||||
generatedAt: new Date(),
|
||||
version,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Privacy Policy im gewuenschten Format
|
||||
*/
|
||||
function renderPrivacyPolicy(
|
||||
sections: PrivacyPolicySection[],
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'HTML':
|
||||
return renderAsHTML(sections, language)
|
||||
case 'MARKDOWN':
|
||||
return renderAsMarkdown(sections, language)
|
||||
default:
|
||||
return renderAsMarkdown(sections, language)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert als HTML
|
||||
*/
|
||||
function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsHTML = sections
|
||||
.map((section) => {
|
||||
const content = t(section.content, language)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/### (.+)/g, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
||||
|
||||
return `
|
||||
<section id="${section.id}">
|
||||
<h2>${t(section.title, language)}</h2>
|
||||
<p>${content}</p>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
||||
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
||||
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
||||
p { margin: 1rem 0; }
|
||||
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
||||
li { margin: 0.5rem 0; }
|
||||
strong { font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
${sectionsHTML}
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert als Markdown
|
||||
*/
|
||||
function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsMarkdown = sections
|
||||
.map((section) => {
|
||||
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
||||
})
|
||||
.join('\n\n---\n\n')
|
||||
|
||||
return `# ${title}\n\n${sectionsMarkdown}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
generateControllerSection,
|
||||
generateDataCollectionSection,
|
||||
@@ -958,8 +15,13 @@ export {
|
||||
generateRetentionSection,
|
||||
generateSpecialCategoriesSection,
|
||||
generateRightsSection,
|
||||
} from './privacy-policy-sections'
|
||||
|
||||
export {
|
||||
generateCookiesSection,
|
||||
generateChangesSection,
|
||||
generatePrivacyPolicySections,
|
||||
generatePrivacyPolicy,
|
||||
renderAsHTML,
|
||||
renderAsMarkdown,
|
||||
}
|
||||
} from './privacy-policy-renderers'
|
||||
|
||||
18
admin-compliance/lib/sdk/einwilligungen/hooks.tsx
Normal file
18
admin-compliance/lib/sdk/einwilligungen/hooks.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Einwilligungen Hook
|
||||
// Custom hook for consuming the Einwilligungen context
|
||||
// =============================================================================
|
||||
|
||||
import { useContext } from 'react'
|
||||
import { EinwilligungenContext } from './provider'
|
||||
import type { EinwilligungenContextValue } from './provider'
|
||||
|
||||
export function useEinwilligungen(): EinwilligungenContextValue {
|
||||
const context = useContext(EinwilligungenContext)
|
||||
if (!context) {
|
||||
throw new Error('useEinwilligungen must be used within EinwilligungenProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
384
admin-compliance/lib/sdk/einwilligungen/provider.tsx
Normal file
384
admin-compliance/lib/sdk/einwilligungen/provider.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Einwilligungen Provider
|
||||
*
|
||||
* React Context Provider fuer das Einwilligungen-Modul.
|
||||
* Stellt State, computed values und Actions bereit.
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useReducer,
|
||||
useCallback,
|
||||
useMemo,
|
||||
ReactNode,
|
||||
Dispatch,
|
||||
} from 'react'
|
||||
import {
|
||||
EinwilligungenState,
|
||||
EinwilligungenAction,
|
||||
EinwilligungenTab,
|
||||
DataPoint,
|
||||
CookieBannerConfig,
|
||||
CompanyInfo,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
RiskLevel,
|
||||
} from './types'
|
||||
import {
|
||||
PREDEFINED_DATA_POINTS,
|
||||
DEFAULT_COOKIE_CATEGORIES,
|
||||
createDefaultCatalog,
|
||||
} from './catalog/loader'
|
||||
import { einwilligungenReducer, initialState } from './reducer'
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
export interface EinwilligungenContextValue {
|
||||
state: EinwilligungenState
|
||||
dispatch: Dispatch<EinwilligungenAction>
|
||||
|
||||
// Computed Values
|
||||
allDataPoints: DataPoint[]
|
||||
selectedDataPointsData: DataPoint[]
|
||||
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
|
||||
categoryStats: Record<DataPointCategory, number>
|
||||
riskStats: Record<RiskLevel, number>
|
||||
legalBasisStats: Record<LegalBasis, number>
|
||||
|
||||
// Actions
|
||||
initializeCatalog: (tenantId: string) => void
|
||||
loadCatalog: (tenantId: string) => Promise<void>
|
||||
saveCatalog: () => Promise<void>
|
||||
toggleDataPoint: (id: string) => void
|
||||
addCustomDataPoint: (dataPoint: DataPoint) => void
|
||||
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
|
||||
deleteCustomDataPoint: (id: string) => void
|
||||
setActiveTab: (tab: EinwilligungenTab) => void
|
||||
setPreviewLanguage: (language: SupportedLanguage) => void
|
||||
setPreviewFormat: (format: ExportFormat) => void
|
||||
setCompanyInfo: (info: CompanyInfo) => void
|
||||
generatePrivacyPolicy: () => Promise<void>
|
||||
generateCookieBannerConfig: () => void
|
||||
}
|
||||
|
||||
export const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
|
||||
|
||||
// =============================================================================
|
||||
// PROVIDER
|
||||
// =============================================================================
|
||||
|
||||
interface EinwilligungenProviderProps {
|
||||
children: ReactNode
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
|
||||
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// COMPUTED VALUES
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const allDataPoints = useMemo(() => {
|
||||
if (!state.catalog) return PREDEFINED_DATA_POINTS
|
||||
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
|
||||
}, [state.catalog])
|
||||
|
||||
const selectedDataPointsData = useMemo(() => {
|
||||
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
|
||||
}, [allDataPoints, state.selectedDataPoints])
|
||||
|
||||
const dataPointsByCategory = useMemo(() => {
|
||||
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
|
||||
const categories: DataPointCategory[] = [
|
||||
'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT',
|
||||
'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION',
|
||||
'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA',
|
||||
'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA',
|
||||
'AI_DATA', 'SECURITY',
|
||||
]
|
||||
for (const cat of categories) {
|
||||
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
|
||||
}
|
||||
return result as Record<DataPointCategory, DataPoint[]>
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const categoryStats = useMemo(() => {
|
||||
const counts: Partial<Record<DataPointCategory, number>> = {}
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.category] = (counts[dp.category] || 0) + 1
|
||||
}
|
||||
return counts as Record<DataPointCategory, number>
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const riskStats = useMemo(() => {
|
||||
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.riskLevel]++
|
||||
}
|
||||
return counts
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const legalBasisStats = useMemo(() => {
|
||||
const counts: Record<LegalBasis, number> = {
|
||||
CONTRACT: 0, CONSENT: 0, EXPLICIT_CONSENT: 0,
|
||||
LEGITIMATE_INTEREST: 0, LEGAL_OBLIGATION: 0,
|
||||
VITAL_INTERESTS: 0, PUBLIC_INTEREST: 0,
|
||||
}
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.legalBasis]++
|
||||
}
|
||||
return counts
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACTIONS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const initializeCatalog = useCallback(
|
||||
(tid: string) => {
|
||||
const catalog = createDefaultCatalog(tid)
|
||||
dispatch({ type: 'SET_CATALOG', payload: catalog })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const loadCatalog = useCallback(
|
||||
async (tid: string) => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||
headers: { 'X-Tenant-ID': tid },
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
|
||||
if (data.companyInfo) {
|
||||
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
|
||||
}
|
||||
if (data.cookieBannerConfig) {
|
||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
initializeCatalog(tid)
|
||||
} else {
|
||||
throw new Error('Failed to load catalog')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading catalog:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
|
||||
initializeCatalog(tid)
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
},
|
||||
[dispatch, initializeCatalog]
|
||||
)
|
||||
|
||||
const saveCatalog = useCallback(async () => {
|
||||
if (!state.catalog) return
|
||||
|
||||
dispatch({ type: 'SET_SAVING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': state.catalog.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
catalog: state.catalog,
|
||||
companyInfo: state.companyInfo,
|
||||
cookieBannerConfig: state.cookieBannerConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save catalog')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving catalog:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
|
||||
} finally {
|
||||
dispatch({ type: 'SET_SAVING', payload: false })
|
||||
}
|
||||
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
|
||||
|
||||
const toggleDataPoint = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const addCustomDataPoint = useCallback(
|
||||
(dataPoint: DataPoint) => {
|
||||
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateDataPoint = useCallback(
|
||||
(id: string, data: Partial<DataPoint>) => {
|
||||
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteCustomDataPoint = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(tab: EinwilligungenTab) => {
|
||||
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setPreviewLanguage = useCallback(
|
||||
(language: SupportedLanguage) => {
|
||||
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setPreviewFormat = useCallback(
|
||||
(format: ExportFormat) => {
|
||||
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setCompanyInfo = useCallback(
|
||||
(info: CompanyInfo) => {
|
||||
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const generatePrivacyPolicy = useCallback(async () => {
|
||||
if (!state.catalog || !state.companyInfo) {
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': state.catalog.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
dataPointIds: state.selectedDataPoints,
|
||||
companyInfo: state.companyInfo,
|
||||
language: state.previewLanguage,
|
||||
format: state.previewFormat,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const policy = await response.json()
|
||||
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
|
||||
} else {
|
||||
throw new Error('Failed to generate privacy policy')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating privacy policy:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
}, [
|
||||
state.catalog,
|
||||
state.companyInfo,
|
||||
state.selectedDataPoints,
|
||||
state.previewLanguage,
|
||||
state.previewFormat,
|
||||
dispatch,
|
||||
])
|
||||
|
||||
const generateCookieBannerConfig = useCallback(() => {
|
||||
if (!state.catalog) return
|
||||
|
||||
const config: CookieBannerConfig = {
|
||||
id: `cookie-banner-${state.catalog.tenantId}`,
|
||||
tenantId: state.catalog.tenantId,
|
||||
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
|
||||
...cat,
|
||||
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
|
||||
})),
|
||||
styling: {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1',
|
||||
borderRadius: 12,
|
||||
},
|
||||
texts: {
|
||||
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website.',
|
||||
},
|
||||
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
||||
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
|
||||
customize: { de: 'Anpassen', en: 'Customize' },
|
||||
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
||||
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
|
||||
}, [state.catalog, state.selectedDataPoints, dispatch])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CONTEXT VALUE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const value: EinwilligungenContextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
allDataPoints,
|
||||
selectedDataPointsData,
|
||||
dataPointsByCategory,
|
||||
categoryStats,
|
||||
riskStats,
|
||||
legalBasisStats,
|
||||
initializeCatalog,
|
||||
loadCatalog,
|
||||
saveCatalog,
|
||||
toggleDataPoint,
|
||||
addCustomDataPoint,
|
||||
updateDataPoint,
|
||||
deleteCustomDataPoint,
|
||||
setActiveTab,
|
||||
setPreviewLanguage,
|
||||
setPreviewFormat,
|
||||
setCompanyInfo,
|
||||
generatePrivacyPolicy,
|
||||
generateCookieBannerConfig,
|
||||
}
|
||||
|
||||
return (
|
||||
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
|
||||
)
|
||||
}
|
||||
237
admin-compliance/lib/sdk/einwilligungen/reducer.ts
Normal file
237
admin-compliance/lib/sdk/einwilligungen/reducer.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Einwilligungen Reducer
|
||||
*
|
||||
* Action-Handling und State-Uebergaenge fuer das Einwilligungen-Modul.
|
||||
*/
|
||||
|
||||
import {
|
||||
EinwilligungenState,
|
||||
EinwilligungenAction,
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE
|
||||
// =============================================================================
|
||||
|
||||
export const initialState: EinwilligungenState = {
|
||||
// Data
|
||||
catalog: null,
|
||||
selectedDataPoints: [],
|
||||
privacyPolicy: null,
|
||||
cookieBannerConfig: null,
|
||||
companyInfo: null,
|
||||
consentStatistics: null,
|
||||
|
||||
// UI State
|
||||
activeTab: 'catalog',
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: null,
|
||||
|
||||
// Editor State
|
||||
editingDataPoint: null,
|
||||
editingSection: null,
|
||||
|
||||
// Preview
|
||||
previewLanguage: 'de',
|
||||
previewFormat: 'HTML',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REDUCER
|
||||
// =============================================================================
|
||||
|
||||
export function einwilligungenReducer(
|
||||
state: EinwilligungenState,
|
||||
action: EinwilligungenAction
|
||||
): EinwilligungenState {
|
||||
switch (action.type) {
|
||||
case 'SET_CATALOG':
|
||||
return {
|
||||
...state,
|
||||
catalog: action.payload,
|
||||
selectedDataPoints: [
|
||||
...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||
...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||
],
|
||||
}
|
||||
|
||||
case 'SET_SELECTED_DATA_POINTS':
|
||||
return {
|
||||
...state,
|
||||
selectedDataPoints: action.payload,
|
||||
}
|
||||
|
||||
case 'TOGGLE_DATA_POINT': {
|
||||
const id = action.payload
|
||||
const isSelected = state.selectedDataPoints.includes(id)
|
||||
return {
|
||||
...state,
|
||||
selectedDataPoints: isSelected
|
||||
? state.selectedDataPoints.filter((dpId) => dpId !== id)
|
||||
: [...state.selectedDataPoints, id],
|
||||
}
|
||||
}
|
||||
|
||||
case 'ADD_CUSTOM_DATA_POINT':
|
||||
if (!state.catalog) return state
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: [...state.catalog.customDataPoints, action.payload],
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
selectedDataPoints: [...state.selectedDataPoints, action.payload.id],
|
||||
}
|
||||
|
||||
case 'UPDATE_DATA_POINT': {
|
||||
if (!state.catalog) return state
|
||||
const { id, data } = action.payload
|
||||
|
||||
const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id)
|
||||
|
||||
if (isCustom) {
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: state.catalog.customDataPoints.map((dp) =>
|
||||
dp.id === id ? { ...dp, ...data } : dp
|
||||
),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
dataPoints: state.catalog.dataPoints.map((dp) =>
|
||||
dp.id === id ? { ...dp, ...data } : dp
|
||||
),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'DELETE_CUSTOM_DATA_POINT':
|
||||
if (!state.catalog) return state
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload),
|
||||
}
|
||||
|
||||
case 'SET_PRIVACY_POLICY':
|
||||
return {
|
||||
...state,
|
||||
privacyPolicy: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_COOKIE_BANNER_CONFIG':
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: action.payload,
|
||||
}
|
||||
|
||||
case 'UPDATE_COOKIE_BANNER_STYLING':
|
||||
if (!state.cookieBannerConfig) return state
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: {
|
||||
...state.cookieBannerConfig,
|
||||
styling: {
|
||||
...state.cookieBannerConfig.styling,
|
||||
...action.payload,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
case 'UPDATE_COOKIE_BANNER_TEXTS':
|
||||
if (!state.cookieBannerConfig) return state
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: {
|
||||
...state.cookieBannerConfig,
|
||||
texts: {
|
||||
...state.cookieBannerConfig.texts,
|
||||
...action.payload,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
case 'SET_COMPANY_INFO':
|
||||
return {
|
||||
...state,
|
||||
companyInfo: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_CONSENT_STATISTICS':
|
||||
return {
|
||||
...state,
|
||||
consentStatistics: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_ACTIVE_TAB':
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_SAVING':
|
||||
return {
|
||||
...state,
|
||||
isSaving: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_EDITING_DATA_POINT':
|
||||
return {
|
||||
...state,
|
||||
editingDataPoint: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_EDITING_SECTION':
|
||||
return {
|
||||
...state,
|
||||
editingSection: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_PREVIEW_LANGUAGE':
|
||||
return {
|
||||
...state,
|
||||
previewLanguage: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_PREVIEW_FORMAT':
|
||||
return {
|
||||
...state,
|
||||
previewFormat: action.payload,
|
||||
}
|
||||
|
||||
case 'RESET_STATE':
|
||||
return initialState
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -1,838 +0,0 @@
|
||||
/**
|
||||
* Datenpunktkatalog & Datenschutzinformationen-Generator
|
||||
* TypeScript Interfaces
|
||||
*
|
||||
* Dieses Modul definiert alle Typen für:
|
||||
* - Datenpunktkatalog (32 vordefinierte + kundenspezifische)
|
||||
* - Privacy Policy Generator
|
||||
* - Cookie Banner Configuration
|
||||
* - Retention Matrix
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kategorien für Datenpunkte (18 Kategorien: A-R)
|
||||
*/
|
||||
export type DataPointCategory =
|
||||
| 'MASTER_DATA' // A: Stammdaten
|
||||
| 'CONTACT_DATA' // B: Kontaktdaten
|
||||
| 'AUTHENTICATION' // C: Authentifizierungsdaten
|
||||
| 'CONSENT' // D: Einwilligungsdaten
|
||||
| 'COMMUNICATION' // E: Kommunikationsdaten
|
||||
| 'PAYMENT' // F: Zahlungsdaten
|
||||
| 'USAGE_DATA' // G: Nutzungsdaten
|
||||
| 'LOCATION' // H: Standortdaten
|
||||
| 'DEVICE_DATA' // I: Gerätedaten
|
||||
| 'MARKETING' // J: Marketingdaten
|
||||
| 'ANALYTICS' // K: Analysedaten
|
||||
| 'SOCIAL_MEDIA' // L: Social-Media-Daten
|
||||
| 'HEALTH_DATA' // M: Gesundheitsdaten (Art. 9 DSGVO)
|
||||
| 'EMPLOYEE_DATA' // N: Beschäftigtendaten
|
||||
| 'CONTRACT_DATA' // O: Vertragsdaten
|
||||
| 'LOG_DATA' // P: Protokolldaten
|
||||
| 'AI_DATA' // Q: KI-Daten
|
||||
| 'SECURITY' // R: Sicherheitsdaten
|
||||
|
||||
/**
|
||||
* Risikoniveau für Datenpunkte
|
||||
*/
|
||||
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
|
||||
/**
|
||||
* Rechtsgrundlagen nach DSGVO Art. 6 und Art. 9
|
||||
*/
|
||||
export type LegalBasis =
|
||||
| 'CONTRACT' // Art. 6 Abs. 1 lit. b DSGVO
|
||||
| 'CONSENT' // Art. 6 Abs. 1 lit. a DSGVO
|
||||
| 'EXPLICIT_CONSENT' // Art. 9 Abs. 2 lit. a DSGVO (für Art. 9 Daten)
|
||||
| 'LEGITIMATE_INTEREST' // Art. 6 Abs. 1 lit. f DSGVO
|
||||
| 'LEGAL_OBLIGATION' // Art. 6 Abs. 1 lit. c DSGVO
|
||||
| 'VITAL_INTERESTS' // Art. 6 Abs. 1 lit. d DSGVO
|
||||
| 'PUBLIC_INTEREST' // Art. 6 Abs. 1 lit. e DSGVO
|
||||
|
||||
/**
|
||||
* Aufbewahrungsfristen
|
||||
*/
|
||||
export type RetentionPeriod =
|
||||
| '24_HOURS'
|
||||
| '30_DAYS'
|
||||
| '90_DAYS'
|
||||
| '12_MONTHS'
|
||||
| '24_MONTHS'
|
||||
| '26_MONTHS' // Google Analytics Standard
|
||||
| '36_MONTHS'
|
||||
| '48_MONTHS'
|
||||
| '6_YEARS'
|
||||
| '10_YEARS'
|
||||
| 'UNTIL_REVOCATION'
|
||||
| 'UNTIL_PURPOSE_FULFILLED'
|
||||
| 'UNTIL_ACCOUNT_DELETION'
|
||||
|
||||
/**
|
||||
* Cookie-Kategorien für Cookie-Banner
|
||||
*/
|
||||
export type CookieCategory =
|
||||
| 'ESSENTIAL' // Technisch notwendig
|
||||
| 'PERFORMANCE' // Analyse & Performance
|
||||
| 'PERSONALIZATION' // Personalisierung
|
||||
| 'EXTERNAL_MEDIA' // Externe Medien
|
||||
|
||||
/**
|
||||
* Export-Formate für Privacy Policy
|
||||
*/
|
||||
export type ExportFormat = 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX'
|
||||
|
||||
/**
|
||||
* Sprachen
|
||||
*/
|
||||
export type SupportedLanguage = 'de' | 'en'
|
||||
|
||||
// =============================================================================
|
||||
// DATA POINT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Lokalisierter Text (DE/EN)
|
||||
*/
|
||||
export interface LocalizedText {
|
||||
de: string
|
||||
en: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelner Datenpunkt im Katalog
|
||||
*/
|
||||
export interface DataPoint {
|
||||
id: string
|
||||
code: string // z.B. "A1", "B2", "C3"
|
||||
category: DataPointCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
purpose: LocalizedText
|
||||
riskLevel: RiskLevel
|
||||
legalBasis: LegalBasis
|
||||
legalBasisJustification: LocalizedText
|
||||
retentionPeriod: RetentionPeriod
|
||||
retentionJustification: LocalizedText
|
||||
cookieCategory: CookieCategory | null // null = kein Cookie
|
||||
isSpecialCategory: boolean // Art. 9 DSGVO (sensible Daten)
|
||||
requiresExplicitConsent: boolean
|
||||
thirdPartyRecipients: string[]
|
||||
technicalMeasures: string[]
|
||||
tags: string[]
|
||||
isCustom?: boolean // Kundenspezifischer Datenpunkt
|
||||
isActive?: boolean // Aktiviert fuer diesen Tenant
|
||||
}
|
||||
|
||||
/**
|
||||
* YAML-Struktur fuer Datenpunkte (fuer Loader)
|
||||
*/
|
||||
export interface DataPointYAML {
|
||||
id: string
|
||||
code: string
|
||||
category: string
|
||||
name_de: string
|
||||
name_en: string
|
||||
description_de: string
|
||||
description_en: string
|
||||
purpose_de: string
|
||||
purpose_en: string
|
||||
risk_level: string
|
||||
legal_basis: string
|
||||
legal_basis_justification_de: string
|
||||
legal_basis_justification_en: string
|
||||
retention_period: string
|
||||
retention_justification_de: string
|
||||
retention_justification_en: string
|
||||
cookie_category: string | null
|
||||
is_special_category: boolean
|
||||
requires_explicit_consent: boolean
|
||||
third_party_recipients: string[]
|
||||
technical_measures: string[]
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CATALOG & RETENTION MATRIX
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Gesamter Datenpunktkatalog eines Tenants
|
||||
*/
|
||||
export interface DataPointCatalog {
|
||||
id: string
|
||||
tenantId: string
|
||||
version: string
|
||||
dataPoints: DataPoint[] // Vordefinierte (32)
|
||||
customDataPoints: DataPoint[] // Kundenspezifische
|
||||
retentionMatrix: RetentionMatrixEntry[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Eintrag in der Retention Matrix
|
||||
*/
|
||||
export interface RetentionMatrixEntry {
|
||||
category: DataPointCategory
|
||||
categoryName: LocalizedText
|
||||
standardPeriod: RetentionPeriod
|
||||
legalBasis: string
|
||||
exceptions: RetentionException[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausnahme von der Standard-Loeschfrist
|
||||
*/
|
||||
export interface RetentionException {
|
||||
condition: LocalizedText
|
||||
period: RetentionPeriod
|
||||
reason: LocalizedText
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PRIVACY POLICY GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Abschnitt in der Privacy Policy
|
||||
*/
|
||||
export interface PrivacyPolicySection {
|
||||
id: string
|
||||
order: number
|
||||
title: LocalizedText
|
||||
content: LocalizedText
|
||||
dataPointIds: string[]
|
||||
isRequired: boolean
|
||||
isGenerated: boolean // true = aus Datenpunkten generiert
|
||||
}
|
||||
|
||||
/**
|
||||
* Unternehmensinfo fuer Privacy Policy
|
||||
*/
|
||||
export interface CompanyInfo {
|
||||
name: string
|
||||
address: string
|
||||
city: string
|
||||
postalCode: string
|
||||
country: string
|
||||
email: string
|
||||
phone?: string
|
||||
website?: string
|
||||
dpoName?: string // Datenschutzbeauftragter
|
||||
dpoEmail?: string
|
||||
dpoPhone?: string
|
||||
registrationNumber?: string // Handelsregister
|
||||
vatId?: string // USt-IdNr
|
||||
}
|
||||
|
||||
/**
|
||||
* Generierte Privacy Policy
|
||||
*/
|
||||
export interface GeneratedPrivacyPolicy {
|
||||
id: string
|
||||
tenantId: string
|
||||
language: SupportedLanguage
|
||||
sections: PrivacyPolicySection[]
|
||||
companyInfo: CompanyInfo
|
||||
generatedAt: Date
|
||||
version: string
|
||||
format: ExportFormat
|
||||
content?: string // Rendered content (HTML/MD)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionen fuer Privacy Policy Generierung
|
||||
*/
|
||||
export interface PrivacyPolicyGenerationOptions {
|
||||
language: SupportedLanguage
|
||||
format: ExportFormat
|
||||
includeDataPoints: string[] // Welche Datenpunkte einschliessen
|
||||
customSections?: PrivacyPolicySection[] // Zusaetzliche Abschnitte
|
||||
styling?: PrivacyPolicyStyling
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling-Optionen fuer PDF/HTML Export
|
||||
*/
|
||||
export interface PrivacyPolicyStyling {
|
||||
primaryColor?: string
|
||||
fontFamily?: string
|
||||
fontSize?: number
|
||||
headerFontSize?: number
|
||||
includeTableOfContents?: boolean
|
||||
includeDateFooter?: boolean
|
||||
logoUrl?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COOKIE BANNER CONFIG
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Einzelner Cookie in einer Kategorie
|
||||
*/
|
||||
export interface CookieInfo {
|
||||
name: string
|
||||
provider: string
|
||||
purpose: LocalizedText
|
||||
expiry: string
|
||||
type: 'FIRST_PARTY' | 'THIRD_PARTY'
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie-Banner Kategorie
|
||||
*/
|
||||
export interface CookieBannerCategory {
|
||||
id: CookieCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
isRequired: boolean // Essentiell = required
|
||||
defaultEnabled: boolean
|
||||
dataPointIds: string[] // Verknuepfte Datenpunkte
|
||||
cookies: CookieInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerStyling {
|
||||
position: 'BOTTOM' | 'TOP' | 'CENTER'
|
||||
theme: 'LIGHT' | 'DARK' | 'CUSTOM'
|
||||
primaryColor?: string
|
||||
secondaryColor?: string
|
||||
textColor?: string
|
||||
backgroundColor?: string
|
||||
borderRadius?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Texte fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerTexts {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
acceptAll: LocalizedText
|
||||
rejectAll: LocalizedText
|
||||
customize: LocalizedText
|
||||
save: LocalizedText
|
||||
privacyPolicyLink: LocalizedText
|
||||
}
|
||||
|
||||
/**
|
||||
* Generierter Code fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerEmbedCode {
|
||||
html: string
|
||||
css: string
|
||||
js: string
|
||||
scriptTag: string // Fertiger Script-Tag zum Einbinden
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollstaendige Cookie Banner Konfiguration
|
||||
*/
|
||||
export interface CookieBannerConfig {
|
||||
id: string
|
||||
tenantId: string
|
||||
categories: CookieBannerCategory[]
|
||||
styling: CookieBannerStyling
|
||||
texts: CookieBannerTexts
|
||||
embedCode?: CookieBannerEmbedCode
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONSENT MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Einzelne Einwilligung eines Nutzers
|
||||
*/
|
||||
export interface ConsentEntry {
|
||||
id: string
|
||||
userId: string
|
||||
dataPointId: string
|
||||
granted: boolean
|
||||
grantedAt: Date
|
||||
revokedAt?: Date
|
||||
ipAddress?: string
|
||||
userAgent?: string
|
||||
consentVersion: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregierte Consent-Statistiken
|
||||
*/
|
||||
export interface ConsentStatistics {
|
||||
totalConsents: number
|
||||
activeConsents: number
|
||||
revokedConsents: number
|
||||
byCategory: Record<DataPointCategory, {
|
||||
total: number
|
||||
active: number
|
||||
revoked: number
|
||||
}>
|
||||
byLegalBasis: Record<LegalBasis, {
|
||||
total: number
|
||||
active: number
|
||||
}>
|
||||
conversionRate: number // Prozent der Nutzer mit Consent
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EINWILLIGUNGEN STATE & ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Aktiver Tab in der Einwilligungen-Ansicht
|
||||
*/
|
||||
export type EinwilligungenTab =
|
||||
| 'catalog'
|
||||
| 'privacy-policy'
|
||||
| 'cookie-banner'
|
||||
| 'retention'
|
||||
| 'consents'
|
||||
|
||||
/**
|
||||
* State fuer Einwilligungen-Modul
|
||||
*/
|
||||
export interface EinwilligungenState {
|
||||
// Data
|
||||
catalog: DataPointCatalog | null
|
||||
selectedDataPoints: string[]
|
||||
privacyPolicy: GeneratedPrivacyPolicy | null
|
||||
cookieBannerConfig: CookieBannerConfig | null
|
||||
companyInfo: CompanyInfo | null
|
||||
consentStatistics: ConsentStatistics | null
|
||||
|
||||
// UI State
|
||||
activeTab: EinwilligungenTab
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
error: string | null
|
||||
|
||||
// Editor State
|
||||
editingDataPoint: DataPoint | null
|
||||
editingSection: PrivacyPolicySection | null
|
||||
|
||||
// Preview
|
||||
previewLanguage: SupportedLanguage
|
||||
previewFormat: ExportFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions fuer Einwilligungen-Reducer
|
||||
*/
|
||||
export type EinwilligungenAction =
|
||||
| { type: 'SET_CATALOG'; payload: DataPointCatalog }
|
||||
| { type: 'SET_SELECTED_DATA_POINTS'; payload: string[] }
|
||||
| { type: 'TOGGLE_DATA_POINT'; payload: string }
|
||||
| { type: 'ADD_CUSTOM_DATA_POINT'; payload: DataPoint }
|
||||
| { type: 'UPDATE_DATA_POINT'; payload: { id: string; data: Partial<DataPoint> } }
|
||||
| { type: 'DELETE_CUSTOM_DATA_POINT'; payload: string }
|
||||
| { type: 'SET_PRIVACY_POLICY'; payload: GeneratedPrivacyPolicy }
|
||||
| { type: 'SET_COOKIE_BANNER_CONFIG'; payload: CookieBannerConfig }
|
||||
| { type: 'UPDATE_COOKIE_BANNER_STYLING'; payload: Partial<CookieBannerStyling> }
|
||||
| { type: 'UPDATE_COOKIE_BANNER_TEXTS'; payload: Partial<CookieBannerTexts> }
|
||||
| { type: 'SET_COMPANY_INFO'; payload: CompanyInfo }
|
||||
| { type: 'SET_CONSENT_STATISTICS'; payload: ConsentStatistics }
|
||||
| { type: 'SET_ACTIVE_TAB'; payload: EinwilligungenTab }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_SAVING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_EDITING_DATA_POINT'; payload: DataPoint | null }
|
||||
| { type: 'SET_EDITING_SECTION'; payload: PrivacyPolicySection | null }
|
||||
| { type: 'SET_PREVIEW_LANGUAGE'; payload: SupportedLanguage }
|
||||
| { type: 'SET_PREVIEW_FORMAT'; payload: ExportFormat }
|
||||
| { type: 'RESET_STATE' }
|
||||
|
||||
// =============================================================================
|
||||
// HELPER TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kategorie-Metadaten
|
||||
*/
|
||||
export interface CategoryMetadata {
|
||||
id: DataPointCategory
|
||||
code: string // A, B, C, etc.
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
icon: string // Icon name
|
||||
color: string // Tailwind color class
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Kategorie zu Metadaten (18 Kategorien)
|
||||
*/
|
||||
export const CATEGORY_METADATA: Record<DataPointCategory, CategoryMetadata> = {
|
||||
MASTER_DATA: {
|
||||
id: 'MASTER_DATA',
|
||||
code: 'A',
|
||||
name: { de: 'Stammdaten', en: 'Master Data' },
|
||||
description: { de: 'Grundlegende personenbezogene Daten', en: 'Basic personal data' },
|
||||
icon: 'User',
|
||||
color: 'blue'
|
||||
},
|
||||
CONTACT_DATA: {
|
||||
id: 'CONTACT_DATA',
|
||||
code: 'B',
|
||||
name: { de: 'Kontaktdaten', en: 'Contact Data' },
|
||||
description: { de: 'Kontaktinformationen und Erreichbarkeit', en: 'Contact information and availability' },
|
||||
icon: 'Mail',
|
||||
color: 'sky'
|
||||
},
|
||||
AUTHENTICATION: {
|
||||
id: 'AUTHENTICATION',
|
||||
code: 'C',
|
||||
name: { de: 'Authentifizierungsdaten', en: 'Authentication Data' },
|
||||
description: { de: 'Daten zur Benutzeranmeldung und Session-Verwaltung', en: 'Data for user login and session management' },
|
||||
icon: 'Key',
|
||||
color: 'slate'
|
||||
},
|
||||
CONSENT: {
|
||||
id: 'CONSENT',
|
||||
code: 'D',
|
||||
name: { de: 'Einwilligungsdaten', en: 'Consent Data' },
|
||||
description: { de: 'Einwilligungen und Datenschutzpraeferenzen', en: 'Consents and privacy preferences' },
|
||||
icon: 'CheckCircle',
|
||||
color: 'green'
|
||||
},
|
||||
COMMUNICATION: {
|
||||
id: 'COMMUNICATION',
|
||||
code: 'E',
|
||||
name: { de: 'Kommunikationsdaten', en: 'Communication Data' },
|
||||
description: { de: 'Kundenservice und Kommunikationsdaten', en: 'Customer service and communication data' },
|
||||
icon: 'MessageSquare',
|
||||
color: 'cyan'
|
||||
},
|
||||
PAYMENT: {
|
||||
id: 'PAYMENT',
|
||||
code: 'F',
|
||||
name: { de: 'Zahlungsdaten', en: 'Payment Data' },
|
||||
description: { de: 'Rechnungs- und Zahlungsinformationen', en: 'Billing and payment information' },
|
||||
icon: 'CreditCard',
|
||||
color: 'amber'
|
||||
},
|
||||
USAGE_DATA: {
|
||||
id: 'USAGE_DATA',
|
||||
code: 'G',
|
||||
name: { de: 'Nutzungsdaten', en: 'Usage Data' },
|
||||
description: { de: 'Daten zur Nutzung des Dienstes', en: 'Data about service usage' },
|
||||
icon: 'Activity',
|
||||
color: 'violet'
|
||||
},
|
||||
LOCATION: {
|
||||
id: 'LOCATION',
|
||||
code: 'H',
|
||||
name: { de: 'Standortdaten', en: 'Location Data' },
|
||||
description: { de: 'Geografische Standortinformationen', en: 'Geographic location information' },
|
||||
icon: 'MapPin',
|
||||
color: 'emerald'
|
||||
},
|
||||
DEVICE_DATA: {
|
||||
id: 'DEVICE_DATA',
|
||||
code: 'I',
|
||||
name: { de: 'Geraetedaten', en: 'Device Data' },
|
||||
description: { de: 'Technische Geraete- und Browserinformationen', en: 'Technical device and browser information' },
|
||||
icon: 'Smartphone',
|
||||
color: 'zinc'
|
||||
},
|
||||
MARKETING: {
|
||||
id: 'MARKETING',
|
||||
code: 'J',
|
||||
name: { de: 'Marketingdaten', en: 'Marketing Data' },
|
||||
description: { de: 'Marketing- und Werbedaten', en: 'Marketing and advertising data' },
|
||||
icon: 'Megaphone',
|
||||
color: 'purple'
|
||||
},
|
||||
ANALYTICS: {
|
||||
id: 'ANALYTICS',
|
||||
code: 'K',
|
||||
name: { de: 'Analysedaten', en: 'Analytics Data' },
|
||||
description: { de: 'Web-Analyse und Nutzungsstatistiken', en: 'Web analytics and usage statistics' },
|
||||
icon: 'BarChart3',
|
||||
color: 'indigo'
|
||||
},
|
||||
SOCIAL_MEDIA: {
|
||||
id: 'SOCIAL_MEDIA',
|
||||
code: 'L',
|
||||
name: { de: 'Social-Media-Daten', en: 'Social Media Data' },
|
||||
description: { de: 'Daten aus sozialen Netzwerken', en: 'Data from social networks' },
|
||||
icon: 'Share2',
|
||||
color: 'pink'
|
||||
},
|
||||
HEALTH_DATA: {
|
||||
id: 'HEALTH_DATA',
|
||||
code: 'M',
|
||||
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
|
||||
description: { de: 'Besondere Kategorie nach Art. 9 DSGVO - Gesundheitsbezogene Daten', en: 'Special category under Art. 9 GDPR - Health-related data' },
|
||||
icon: 'Heart',
|
||||
color: 'rose'
|
||||
},
|
||||
EMPLOYEE_DATA: {
|
||||
id: 'EMPLOYEE_DATA',
|
||||
code: 'N',
|
||||
name: { de: 'Beschaeftigtendaten', en: 'Employee Data' },
|
||||
description: { de: 'Personalverwaltung und Arbeitnehmerinformationen (BDSG § 26)', en: 'HR management and employee information' },
|
||||
icon: 'Briefcase',
|
||||
color: 'orange'
|
||||
},
|
||||
CONTRACT_DATA: {
|
||||
id: 'CONTRACT_DATA',
|
||||
code: 'O',
|
||||
name: { de: 'Vertragsdaten', en: 'Contract Data' },
|
||||
description: { de: 'Vertragsinformationen und -dokumente', en: 'Contract information and documents' },
|
||||
icon: 'FileText',
|
||||
color: 'teal'
|
||||
},
|
||||
LOG_DATA: {
|
||||
id: 'LOG_DATA',
|
||||
code: 'P',
|
||||
name: { de: 'Protokolldaten', en: 'Log Data' },
|
||||
description: { de: 'System- und Zugriffsprotokolle', en: 'System and access logs' },
|
||||
icon: 'FileCode',
|
||||
color: 'gray'
|
||||
},
|
||||
AI_DATA: {
|
||||
id: 'AI_DATA',
|
||||
code: 'Q',
|
||||
name: { de: 'KI-Daten', en: 'AI Data' },
|
||||
description: { de: 'KI-Interaktionen, Prompts und generierte Inhalte (AI Act)', en: 'AI interactions, prompts and generated content (AI Act)' },
|
||||
icon: 'Bot',
|
||||
color: 'fuchsia'
|
||||
},
|
||||
SECURITY: {
|
||||
id: 'SECURITY',
|
||||
code: 'R',
|
||||
name: { de: 'Sicherheitsdaten', en: 'Security Data' },
|
||||
description: { de: 'Sicherheitsrelevante Daten und Vorfallberichte', en: 'Security-relevant data and incident reports' },
|
||||
icon: 'Shield',
|
||||
color: 'red'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Rechtsgrundlage zu Beschreibung
|
||||
*/
|
||||
export const LEGAL_BASIS_INFO: Record<LegalBasis, { article: string; name: LocalizedText; description: LocalizedText }> = {
|
||||
CONTRACT: {
|
||||
article: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||
name: { de: 'Vertragserfuellung', en: 'Contract Performance' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich fuer die Erfuellung eines Vertrags oder zur Durchfuehrung vorvertraglicher Massnahmen.',
|
||||
en: 'Processing is necessary for the performance of a contract or pre-contractual measures.'
|
||||
}
|
||||
},
|
||||
CONSENT: {
|
||||
article: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
name: { de: 'Einwilligung', en: 'Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ihre Einwilligung zu der Verarbeitung gegeben.',
|
||||
en: 'The data subject has given consent to the processing.'
|
||||
}
|
||||
},
|
||||
EXPLICIT_CONSENT: {
|
||||
article: 'Art. 9 Abs. 2 lit. a DSGVO',
|
||||
name: { de: 'Ausdrueckliche Einwilligung', en: 'Explicit Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ausdruecklich in die Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) eingewilligt. Dies betrifft Gesundheitsdaten, biometrische Daten, Daten zur ethnischen Herkunft, politische Meinungen, religiöse Überzeugungen etc.',
|
||||
en: 'The data subject has given explicit consent to the processing of special categories of personal data (Art. 9 GDPR). This includes health data, biometric data, racial or ethnic origin, political opinions, religious beliefs, etc.'
|
||||
}
|
||||
},
|
||||
LEGITIMATE_INTEREST: {
|
||||
article: 'Art. 6 Abs. 1 lit. f DSGVO',
|
||||
name: { de: 'Berechtigtes Interesse', en: 'Legitimate Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Wahrung berechtigter Interessen des Verantwortlichen erforderlich.',
|
||||
en: 'Processing is necessary for legitimate interests pursued by the controller.'
|
||||
}
|
||||
},
|
||||
LEGAL_OBLIGATION: {
|
||||
article: 'Art. 6 Abs. 1 lit. c DSGVO',
|
||||
name: { de: 'Rechtliche Verpflichtung', en: 'Legal Obligation' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Erfuellung einer rechtlichen Verpflichtung erforderlich.',
|
||||
en: 'Processing is necessary for compliance with a legal obligation.'
|
||||
}
|
||||
},
|
||||
VITAL_INTERESTS: {
|
||||
article: 'Art. 6 Abs. 1 lit. d DSGVO',
|
||||
name: { de: 'Lebenswichtige Interessen', en: 'Vital Interests' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich, um lebenswichtige Interessen der betroffenen Person oder einer anderen natuerlichen Person zu schuetzen.',
|
||||
en: 'Processing is necessary to protect the vital interests of the data subject or another natural person.'
|
||||
}
|
||||
},
|
||||
PUBLIC_INTEREST: {
|
||||
article: 'Art. 6 Abs. 1 lit. e DSGVO',
|
||||
name: { de: 'Oeffentliches Interesse', en: 'Public Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist fuer die Wahrnehmung einer Aufgabe erforderlich, die im oeffentlichen Interesse liegt oder in Ausuebung oeffentlicher Gewalt erfolgt.',
|
||||
en: 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Aufbewahrungsfrist zu Beschreibung
|
||||
*/
|
||||
export const RETENTION_PERIOD_INFO: Record<RetentionPeriod, { label: LocalizedText; days: number | null }> = {
|
||||
'24_HOURS': { label: { de: '24 Stunden', en: '24 Hours' }, days: 1 },
|
||||
'30_DAYS': { label: { de: '30 Tage', en: '30 Days' }, days: 30 },
|
||||
'90_DAYS': { label: { de: '90 Tage', en: '90 Days' }, days: 90 },
|
||||
'12_MONTHS': { label: { de: '12 Monate', en: '12 Months' }, days: 365 },
|
||||
'24_MONTHS': { label: { de: '24 Monate', en: '24 Months' }, days: 730 },
|
||||
'26_MONTHS': { label: { de: '26 Monate (Google Analytics)', en: '26 Months (Google Analytics)' }, days: 790 },
|
||||
'36_MONTHS': { label: { de: '36 Monate', en: '36 Months' }, days: 1095 },
|
||||
'48_MONTHS': { label: { de: '48 Monate', en: '48 Months' }, days: 1460 },
|
||||
'6_YEARS': { label: { de: '6 Jahre', en: '6 Years' }, days: 2190 },
|
||||
'10_YEARS': { label: { de: '10 Jahre', en: '10 Years' }, days: 3650 },
|
||||
'UNTIL_REVOCATION': { label: { de: 'Bis Widerruf', en: 'Until Revocation' }, days: null },
|
||||
'UNTIL_PURPOSE_FULFILLED': { label: { de: 'Bis Zweckerfuellung', en: 'Until Purpose Fulfilled' }, days: null },
|
||||
'UNTIL_ACCOUNT_DELETION': { label: { de: 'Bis Kontoschliessung', en: 'Until Account Deletion' }, days: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise für Art. 9 DSGVO Kategorien
|
||||
*/
|
||||
export interface Article9Warning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const ARTICLE_9_WARNING: Article9Warning = {
|
||||
title: {
|
||||
de: 'Besondere Kategorie personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: 'Special Category of Personal Data (Art. 9 GDPR)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung dieser Daten unterliegt besonderen Anforderungen nach Art. 9 DSGVO. Diese Daten sind besonders schuetzenswert.',
|
||||
en: 'Processing of this data is subject to special requirements under Art. 9 GDPR. This data requires special protection.'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Ausdrueckliche Einwilligung erforderlich (Art. 9 Abs. 2 lit. a DSGVO)',
|
||||
en: 'Explicit consent required (Art. 9(2)(a) GDPR)'
|
||||
},
|
||||
{
|
||||
de: 'Separate Einwilligungserklaerung im UI notwendig',
|
||||
en: 'Separate consent declaration required in UI'
|
||||
},
|
||||
{
|
||||
de: 'Hoehere Dokumentationspflichten',
|
||||
en: 'Higher documentation requirements'
|
||||
},
|
||||
{
|
||||
de: 'Spezielle Loeschverfahren erforderlich',
|
||||
en: 'Special deletion procedures required'
|
||||
},
|
||||
{
|
||||
de: 'Datenschutz-Folgenabschaetzung (DSFA) empfohlen',
|
||||
en: 'Data Protection Impact Assessment (DPIA) recommended'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise für Beschäftigtendaten (BDSG § 26)
|
||||
*/
|
||||
export interface EmployeeDataWarning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const EMPLOYEE_DATA_WARNING: EmployeeDataWarning = {
|
||||
title: {
|
||||
de: 'Beschaeftigtendaten (BDSG § 26)',
|
||||
en: 'Employee Data (BDSG § 26)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung von Beschaeftigtendaten unterliegt besonderen Anforderungen nach § 26 BDSG.',
|
||||
en: 'Processing of employee data is subject to special requirements under § 26 BDSG (German Federal Data Protection Act).'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Aufbewahrungspflichten fuer Lohnunterlagen (6-10 Jahre)',
|
||||
en: 'Retention obligations for payroll records (6-10 years)'
|
||||
},
|
||||
{
|
||||
de: 'Betriebsrat-Beteiligung ggf. erforderlich',
|
||||
en: 'Works council involvement may be required'
|
||||
},
|
||||
{
|
||||
de: 'Verarbeitung nur fuer Zwecke des Beschaeftigungsverhaeltnisses',
|
||||
en: 'Processing only for employment purposes'
|
||||
},
|
||||
{
|
||||
de: 'Besondere Vertraulichkeit bei Gesundheitsdaten',
|
||||
en: 'Special confidentiality for health data'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise für KI-Daten (AI Act)
|
||||
*/
|
||||
export interface AIDataWarning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const AI_DATA_WARNING: AIDataWarning = {
|
||||
title: {
|
||||
de: 'KI-Daten (AI Act)',
|
||||
en: 'AI Data (AI Act)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.',
|
||||
en: 'Processing of AI-related data is subject to AI Act transparency requirements.'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Transparenzpflichten bei KI-Verarbeitung',
|
||||
en: 'Transparency obligations for AI processing'
|
||||
},
|
||||
{
|
||||
de: 'Kennzeichnung von KI-generierten Inhalten',
|
||||
en: 'Labeling of AI-generated content'
|
||||
},
|
||||
{
|
||||
de: 'Dokumentation der KI-Modell-Nutzung',
|
||||
en: 'Documentation of AI model usage'
|
||||
},
|
||||
{
|
||||
de: 'Keine Verwendung fuer unerlaubtes Training ohne Einwilligung',
|
||||
en: 'No use for unauthorized training without consent'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk Level Styling
|
||||
*/
|
||||
export const RISK_LEVEL_STYLING: Record<RiskLevel, { label: LocalizedText; color: string; bgColor: string }> = {
|
||||
LOW: {
|
||||
label: { de: 'Niedrig', en: 'Low' },
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
MEDIUM: {
|
||||
label: { de: 'Mittel', en: 'Medium' },
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
HIGH: {
|
||||
label: { de: 'Hoch', en: 'High' },
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// =============================================================================
|
||||
// CATALOG & RETENTION MATRIX
|
||||
// =============================================================================
|
||||
|
||||
import type { DataPointCategory, RetentionPeriod } from './enums'
|
||||
import type { LocalizedText, DataPoint } from './data-point'
|
||||
|
||||
/**
|
||||
* Gesamter Datenpunktkatalog eines Tenants
|
||||
*/
|
||||
export interface DataPointCatalog {
|
||||
id: string
|
||||
tenantId: string
|
||||
version: string
|
||||
dataPoints: DataPoint[] // Vordefinierte (32)
|
||||
customDataPoints: DataPoint[] // Kundenspezifische
|
||||
retentionMatrix: RetentionMatrixEntry[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Eintrag in der Retention Matrix
|
||||
*/
|
||||
export interface RetentionMatrixEntry {
|
||||
category: DataPointCategory
|
||||
categoryName: LocalizedText
|
||||
standardPeriod: RetentionPeriod
|
||||
legalBasis: string
|
||||
exceptions: RetentionException[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausnahme von der Standard-Loeschfrist
|
||||
*/
|
||||
export interface RetentionException {
|
||||
condition: LocalizedText
|
||||
period: RetentionPeriod
|
||||
reason: LocalizedText
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// =============================================================================
|
||||
// CONSENT MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
import type { DataPointCategory, LegalBasis } from './enums'
|
||||
|
||||
/**
|
||||
* Einzelne Einwilligung eines Nutzers
|
||||
*/
|
||||
export interface ConsentEntry {
|
||||
id: string
|
||||
userId: string
|
||||
dataPointId: string
|
||||
granted: boolean
|
||||
grantedAt: Date
|
||||
revokedAt?: Date
|
||||
ipAddress?: string
|
||||
userAgent?: string
|
||||
consentVersion: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregierte Consent-Statistiken
|
||||
*/
|
||||
export interface ConsentStatistics {
|
||||
totalConsents: number
|
||||
activeConsents: number
|
||||
revokedConsents: number
|
||||
byCategory: Record<DataPointCategory, {
|
||||
total: number
|
||||
active: number
|
||||
revoked: number
|
||||
}>
|
||||
byLegalBasis: Record<LegalBasis, {
|
||||
total: number
|
||||
active: number
|
||||
}>
|
||||
conversionRate: number // Prozent der Nutzer mit Consent
|
||||
}
|
||||
259
admin-compliance/lib/sdk/einwilligungen/types/constants.ts
Normal file
259
admin-compliance/lib/sdk/einwilligungen/types/constants.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
// =============================================================================
|
||||
// CONSTANTS - CATEGORY METADATA & LEGAL BASIS INFO
|
||||
// =============================================================================
|
||||
|
||||
import type { DataPointCategory, LegalBasis, RetentionPeriod, RiskLevel } from './enums'
|
||||
import type { LocalizedText } from './data-point'
|
||||
import type { CategoryMetadata } from './helpers'
|
||||
|
||||
/**
|
||||
* Mapping von Kategorie zu Metadaten (18 Kategorien)
|
||||
*/
|
||||
export const CATEGORY_METADATA: Record<DataPointCategory, CategoryMetadata> = {
|
||||
MASTER_DATA: {
|
||||
id: 'MASTER_DATA',
|
||||
code: 'A',
|
||||
name: { de: 'Stammdaten', en: 'Master Data' },
|
||||
description: { de: 'Grundlegende personenbezogene Daten', en: 'Basic personal data' },
|
||||
icon: 'User',
|
||||
color: 'blue'
|
||||
},
|
||||
CONTACT_DATA: {
|
||||
id: 'CONTACT_DATA',
|
||||
code: 'B',
|
||||
name: { de: 'Kontaktdaten', en: 'Contact Data' },
|
||||
description: { de: 'Kontaktinformationen und Erreichbarkeit', en: 'Contact information and availability' },
|
||||
icon: 'Mail',
|
||||
color: 'sky'
|
||||
},
|
||||
AUTHENTICATION: {
|
||||
id: 'AUTHENTICATION',
|
||||
code: 'C',
|
||||
name: { de: 'Authentifizierungsdaten', en: 'Authentication Data' },
|
||||
description: { de: 'Daten zur Benutzeranmeldung und Session-Verwaltung', en: 'Data for user login and session management' },
|
||||
icon: 'Key',
|
||||
color: 'slate'
|
||||
},
|
||||
CONSENT: {
|
||||
id: 'CONSENT',
|
||||
code: 'D',
|
||||
name: { de: 'Einwilligungsdaten', en: 'Consent Data' },
|
||||
description: { de: 'Einwilligungen und Datenschutzpraeferenzen', en: 'Consents and privacy preferences' },
|
||||
icon: 'CheckCircle',
|
||||
color: 'green'
|
||||
},
|
||||
COMMUNICATION: {
|
||||
id: 'COMMUNICATION',
|
||||
code: 'E',
|
||||
name: { de: 'Kommunikationsdaten', en: 'Communication Data' },
|
||||
description: { de: 'Kundenservice und Kommunikationsdaten', en: 'Customer service and communication data' },
|
||||
icon: 'MessageSquare',
|
||||
color: 'cyan'
|
||||
},
|
||||
PAYMENT: {
|
||||
id: 'PAYMENT',
|
||||
code: 'F',
|
||||
name: { de: 'Zahlungsdaten', en: 'Payment Data' },
|
||||
description: { de: 'Rechnungs- und Zahlungsinformationen', en: 'Billing and payment information' },
|
||||
icon: 'CreditCard',
|
||||
color: 'amber'
|
||||
},
|
||||
USAGE_DATA: {
|
||||
id: 'USAGE_DATA',
|
||||
code: 'G',
|
||||
name: { de: 'Nutzungsdaten', en: 'Usage Data' },
|
||||
description: { de: 'Daten zur Nutzung des Dienstes', en: 'Data about service usage' },
|
||||
icon: 'Activity',
|
||||
color: 'violet'
|
||||
},
|
||||
LOCATION: {
|
||||
id: 'LOCATION',
|
||||
code: 'H',
|
||||
name: { de: 'Standortdaten', en: 'Location Data' },
|
||||
description: { de: 'Geografische Standortinformationen', en: 'Geographic location information' },
|
||||
icon: 'MapPin',
|
||||
color: 'emerald'
|
||||
},
|
||||
DEVICE_DATA: {
|
||||
id: 'DEVICE_DATA',
|
||||
code: 'I',
|
||||
name: { de: 'Geraetedaten', en: 'Device Data' },
|
||||
description: { de: 'Technische Geraete- und Browserinformationen', en: 'Technical device and browser information' },
|
||||
icon: 'Smartphone',
|
||||
color: 'zinc'
|
||||
},
|
||||
MARKETING: {
|
||||
id: 'MARKETING',
|
||||
code: 'J',
|
||||
name: { de: 'Marketingdaten', en: 'Marketing Data' },
|
||||
description: { de: 'Marketing- und Werbedaten', en: 'Marketing and advertising data' },
|
||||
icon: 'Megaphone',
|
||||
color: 'purple'
|
||||
},
|
||||
ANALYTICS: {
|
||||
id: 'ANALYTICS',
|
||||
code: 'K',
|
||||
name: { de: 'Analysedaten', en: 'Analytics Data' },
|
||||
description: { de: 'Web-Analyse und Nutzungsstatistiken', en: 'Web analytics and usage statistics' },
|
||||
icon: 'BarChart3',
|
||||
color: 'indigo'
|
||||
},
|
||||
SOCIAL_MEDIA: {
|
||||
id: 'SOCIAL_MEDIA',
|
||||
code: 'L',
|
||||
name: { de: 'Social-Media-Daten', en: 'Social Media Data' },
|
||||
description: { de: 'Daten aus sozialen Netzwerken', en: 'Data from social networks' },
|
||||
icon: 'Share2',
|
||||
color: 'pink'
|
||||
},
|
||||
HEALTH_DATA: {
|
||||
id: 'HEALTH_DATA',
|
||||
code: 'M',
|
||||
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
|
||||
description: { de: 'Besondere Kategorie nach Art. 9 DSGVO - Gesundheitsbezogene Daten', en: 'Special category under Art. 9 GDPR - Health-related data' },
|
||||
icon: 'Heart',
|
||||
color: 'rose'
|
||||
},
|
||||
EMPLOYEE_DATA: {
|
||||
id: 'EMPLOYEE_DATA',
|
||||
code: 'N',
|
||||
name: { de: 'Beschaeftigtendaten', en: 'Employee Data' },
|
||||
description: { de: 'Personalverwaltung und Arbeitnehmerinformationen (BDSG § 26)', en: 'HR management and employee information' },
|
||||
icon: 'Briefcase',
|
||||
color: 'orange'
|
||||
},
|
||||
CONTRACT_DATA: {
|
||||
id: 'CONTRACT_DATA',
|
||||
code: 'O',
|
||||
name: { de: 'Vertragsdaten', en: 'Contract Data' },
|
||||
description: { de: 'Vertragsinformationen und -dokumente', en: 'Contract information and documents' },
|
||||
icon: 'FileText',
|
||||
color: 'teal'
|
||||
},
|
||||
LOG_DATA: {
|
||||
id: 'LOG_DATA',
|
||||
code: 'P',
|
||||
name: { de: 'Protokolldaten', en: 'Log Data' },
|
||||
description: { de: 'System- und Zugriffsprotokolle', en: 'System and access logs' },
|
||||
icon: 'FileCode',
|
||||
color: 'gray'
|
||||
},
|
||||
AI_DATA: {
|
||||
id: 'AI_DATA',
|
||||
code: 'Q',
|
||||
name: { de: 'KI-Daten', en: 'AI Data' },
|
||||
description: { de: 'KI-Interaktionen, Prompts und generierte Inhalte (AI Act)', en: 'AI interactions, prompts and generated content (AI Act)' },
|
||||
icon: 'Bot',
|
||||
color: 'fuchsia'
|
||||
},
|
||||
SECURITY: {
|
||||
id: 'SECURITY',
|
||||
code: 'R',
|
||||
name: { de: 'Sicherheitsdaten', en: 'Security Data' },
|
||||
description: { de: 'Sicherheitsrelevante Daten und Vorfallberichte', en: 'Security-relevant data and incident reports' },
|
||||
icon: 'Shield',
|
||||
color: 'red'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Rechtsgrundlage zu Beschreibung
|
||||
*/
|
||||
export const LEGAL_BASIS_INFO: Record<LegalBasis, { article: string; name: LocalizedText; description: LocalizedText }> = {
|
||||
CONTRACT: {
|
||||
article: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||
name: { de: 'Vertragserfuellung', en: 'Contract Performance' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich fuer die Erfuellung eines Vertrags oder zur Durchfuehrung vorvertraglicher Massnahmen.',
|
||||
en: 'Processing is necessary for the performance of a contract or pre-contractual measures.'
|
||||
}
|
||||
},
|
||||
CONSENT: {
|
||||
article: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
name: { de: 'Einwilligung', en: 'Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ihre Einwilligung zu der Verarbeitung gegeben.',
|
||||
en: 'The data subject has given consent to the processing.'
|
||||
}
|
||||
},
|
||||
EXPLICIT_CONSENT: {
|
||||
article: 'Art. 9 Abs. 2 lit. a DSGVO',
|
||||
name: { de: 'Ausdrueckliche Einwilligung', en: 'Explicit Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ausdruecklich in die Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) eingewilligt. Dies betrifft Gesundheitsdaten, biometrische Daten, Daten zur ethnischen Herkunft, politische Meinungen, religiöse Überzeugungen etc.',
|
||||
en: 'The data subject has given explicit consent to the processing of special categories of personal data (Art. 9 GDPR). This includes health data, biometric data, racial or ethnic origin, political opinions, religious beliefs, etc.'
|
||||
}
|
||||
},
|
||||
LEGITIMATE_INTEREST: {
|
||||
article: 'Art. 6 Abs. 1 lit. f DSGVO',
|
||||
name: { de: 'Berechtigtes Interesse', en: 'Legitimate Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Wahrung berechtigter Interessen des Verantwortlichen erforderlich.',
|
||||
en: 'Processing is necessary for legitimate interests pursued by the controller.'
|
||||
}
|
||||
},
|
||||
LEGAL_OBLIGATION: {
|
||||
article: 'Art. 6 Abs. 1 lit. c DSGVO',
|
||||
name: { de: 'Rechtliche Verpflichtung', en: 'Legal Obligation' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Erfuellung einer rechtlichen Verpflichtung erforderlich.',
|
||||
en: 'Processing is necessary for compliance with a legal obligation.'
|
||||
}
|
||||
},
|
||||
VITAL_INTERESTS: {
|
||||
article: 'Art. 6 Abs. 1 lit. d DSGVO',
|
||||
name: { de: 'Lebenswichtige Interessen', en: 'Vital Interests' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich, um lebenswichtige Interessen der betroffenen Person oder einer anderen natuerlichen Person zu schuetzen.',
|
||||
en: 'Processing is necessary to protect the vital interests of the data subject or another natural person.'
|
||||
}
|
||||
},
|
||||
PUBLIC_INTEREST: {
|
||||
article: 'Art. 6 Abs. 1 lit. e DSGVO',
|
||||
name: { de: 'Oeffentliches Interesse', en: 'Public Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist fuer die Wahrnehmung einer Aufgabe erforderlich, die im oeffentlichen Interesse liegt oder in Ausuebung oeffentlicher Gewalt erfolgt.',
|
||||
en: 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Aufbewahrungsfrist zu Beschreibung
|
||||
*/
|
||||
export const RETENTION_PERIOD_INFO: Record<RetentionPeriod, { label: LocalizedText; days: number | null }> = {
|
||||
'24_HOURS': { label: { de: '24 Stunden', en: '24 Hours' }, days: 1 },
|
||||
'30_DAYS': { label: { de: '30 Tage', en: '30 Days' }, days: 30 },
|
||||
'90_DAYS': { label: { de: '90 Tage', en: '90 Days' }, days: 90 },
|
||||
'12_MONTHS': { label: { de: '12 Monate', en: '12 Months' }, days: 365 },
|
||||
'24_MONTHS': { label: { de: '24 Monate', en: '24 Months' }, days: 730 },
|
||||
'26_MONTHS': { label: { de: '26 Monate (Google Analytics)', en: '26 Months (Google Analytics)' }, days: 790 },
|
||||
'36_MONTHS': { label: { de: '36 Monate', en: '36 Months' }, days: 1095 },
|
||||
'48_MONTHS': { label: { de: '48 Monate', en: '48 Months' }, days: 1460 },
|
||||
'6_YEARS': { label: { de: '6 Jahre', en: '6 Years' }, days: 2190 },
|
||||
'10_YEARS': { label: { de: '10 Jahre', en: '10 Years' }, days: 3650 },
|
||||
'UNTIL_REVOCATION': { label: { de: 'Bis Widerruf', en: 'Until Revocation' }, days: null },
|
||||
'UNTIL_PURPOSE_FULFILLED': { label: { de: 'Bis Zweckerfuellung', en: 'Until Purpose Fulfilled' }, days: null },
|
||||
'UNTIL_ACCOUNT_DELETION': { label: { de: 'Bis Kontoschliessung', en: 'Until Account Deletion' }, days: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk Level Styling
|
||||
*/
|
||||
export const RISK_LEVEL_STYLING: Record<RiskLevel, { label: LocalizedText; color: string; bgColor: string }> = {
|
||||
LOW: {
|
||||
label: { de: 'Niedrig', en: 'Low' },
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
MEDIUM: {
|
||||
label: { de: 'Mittel', en: 'Medium' },
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
HIGH: {
|
||||
label: { de: 'Hoch', en: 'High' },
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// =============================================================================
|
||||
// COOKIE BANNER CONFIG
|
||||
// =============================================================================
|
||||
|
||||
import type { CookieCategory } from './enums'
|
||||
import type { LocalizedText } from './data-point'
|
||||
|
||||
/**
|
||||
* Einzelner Cookie in einer Kategorie
|
||||
*/
|
||||
export interface CookieInfo {
|
||||
name: string
|
||||
provider: string
|
||||
purpose: LocalizedText
|
||||
expiry: string
|
||||
type: 'FIRST_PARTY' | 'THIRD_PARTY'
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie-Banner Kategorie
|
||||
*/
|
||||
export interface CookieBannerCategory {
|
||||
id: CookieCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
isRequired: boolean // Essentiell = required
|
||||
defaultEnabled: boolean
|
||||
dataPointIds: string[] // Verknuepfte Datenpunkte
|
||||
cookies: CookieInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerStyling {
|
||||
position: 'BOTTOM' | 'TOP' | 'CENTER'
|
||||
theme: 'LIGHT' | 'DARK' | 'CUSTOM'
|
||||
primaryColor?: string
|
||||
secondaryColor?: string
|
||||
textColor?: string
|
||||
backgroundColor?: string
|
||||
borderRadius?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Texte fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerTexts {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
acceptAll: LocalizedText
|
||||
rejectAll: LocalizedText
|
||||
customize: LocalizedText
|
||||
save: LocalizedText
|
||||
privacyPolicyLink: LocalizedText
|
||||
}
|
||||
|
||||
/**
|
||||
* Generierter Code fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerEmbedCode {
|
||||
html: string
|
||||
css: string
|
||||
js: string
|
||||
scriptTag: string // Fertiger Script-Tag zum Einbinden
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollstaendige Cookie Banner Konfiguration
|
||||
*/
|
||||
export interface CookieBannerConfig {
|
||||
id: string
|
||||
tenantId: string
|
||||
categories: CookieBannerCategory[]
|
||||
styling: CookieBannerStyling
|
||||
texts: CookieBannerTexts
|
||||
embedCode?: CookieBannerEmbedCode
|
||||
updatedAt: Date
|
||||
}
|
||||
72
admin-compliance/lib/sdk/einwilligungen/types/data-point.ts
Normal file
72
admin-compliance/lib/sdk/einwilligungen/types/data-point.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// =============================================================================
|
||||
// DATA POINT
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
DataPointCategory,
|
||||
RiskLevel,
|
||||
LegalBasis,
|
||||
RetentionPeriod,
|
||||
CookieCategory,
|
||||
} from './enums'
|
||||
|
||||
/**
|
||||
* Lokalisierter Text (DE/EN)
|
||||
*/
|
||||
export interface LocalizedText {
|
||||
de: string
|
||||
en: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelner Datenpunkt im Katalog
|
||||
*/
|
||||
export interface DataPoint {
|
||||
id: string
|
||||
code: string // z.B. "A1", "B2", "C3"
|
||||
category: DataPointCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
purpose: LocalizedText
|
||||
riskLevel: RiskLevel
|
||||
legalBasis: LegalBasis
|
||||
legalBasisJustification: LocalizedText
|
||||
retentionPeriod: RetentionPeriod
|
||||
retentionJustification: LocalizedText
|
||||
cookieCategory: CookieCategory | null // null = kein Cookie
|
||||
isSpecialCategory: boolean // Art. 9 DSGVO (sensible Daten)
|
||||
requiresExplicitConsent: boolean
|
||||
thirdPartyRecipients: string[]
|
||||
technicalMeasures: string[]
|
||||
tags: string[]
|
||||
isCustom?: boolean // Kundenspezifischer Datenpunkt
|
||||
isActive?: boolean // Aktiviert fuer diesen Tenant
|
||||
}
|
||||
|
||||
/**
|
||||
* YAML-Struktur fuer Datenpunkte (fuer Loader)
|
||||
*/
|
||||
export interface DataPointYAML {
|
||||
id: string
|
||||
code: string
|
||||
category: string
|
||||
name_de: string
|
||||
name_en: string
|
||||
description_de: string
|
||||
description_en: string
|
||||
purpose_de: string
|
||||
purpose_en: string
|
||||
risk_level: string
|
||||
legal_basis: string
|
||||
legal_basis_justification_de: string
|
||||
legal_basis_justification_en: string
|
||||
retention_period: string
|
||||
retention_justification_de: string
|
||||
retention_justification_en: string
|
||||
cookie_category: string | null
|
||||
is_special_category: boolean
|
||||
requires_explicit_consent: boolean
|
||||
third_party_recipients: string[]
|
||||
technical_measures: string[]
|
||||
tags: string[]
|
||||
}
|
||||
85
admin-compliance/lib/sdk/einwilligungen/types/enums.ts
Normal file
85
admin-compliance/lib/sdk/einwilligungen/types/enums.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Datenpunktkatalog & Datenschutzinformationen-Generator
|
||||
* Enums & Literal Types
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kategorien fuer Datenpunkte (18 Kategorien: A-R)
|
||||
*/
|
||||
export type DataPointCategory =
|
||||
| 'MASTER_DATA' // A: Stammdaten
|
||||
| 'CONTACT_DATA' // B: Kontaktdaten
|
||||
| 'AUTHENTICATION' // C: Authentifizierungsdaten
|
||||
| 'CONSENT' // D: Einwilligungsdaten
|
||||
| 'COMMUNICATION' // E: Kommunikationsdaten
|
||||
| 'PAYMENT' // F: Zahlungsdaten
|
||||
| 'USAGE_DATA' // G: Nutzungsdaten
|
||||
| 'LOCATION' // H: Standortdaten
|
||||
| 'DEVICE_DATA' // I: Gerätedaten
|
||||
| 'MARKETING' // J: Marketingdaten
|
||||
| 'ANALYTICS' // K: Analysedaten
|
||||
| 'SOCIAL_MEDIA' // L: Social-Media-Daten
|
||||
| 'HEALTH_DATA' // M: Gesundheitsdaten (Art. 9 DSGVO)
|
||||
| 'EMPLOYEE_DATA' // N: Beschäftigtendaten
|
||||
| 'CONTRACT_DATA' // O: Vertragsdaten
|
||||
| 'LOG_DATA' // P: Protokolldaten
|
||||
| 'AI_DATA' // Q: KI-Daten
|
||||
| 'SECURITY' // R: Sicherheitsdaten
|
||||
|
||||
/**
|
||||
* Risikoniveau fuer Datenpunkte
|
||||
*/
|
||||
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
|
||||
/**
|
||||
* Rechtsgrundlagen nach DSGVO Art. 6 und Art. 9
|
||||
*/
|
||||
export type LegalBasis =
|
||||
| 'CONTRACT' // Art. 6 Abs. 1 lit. b DSGVO
|
||||
| 'CONSENT' // Art. 6 Abs. 1 lit. a DSGVO
|
||||
| 'EXPLICIT_CONSENT' // Art. 9 Abs. 2 lit. a DSGVO (fuer Art. 9 Daten)
|
||||
| 'LEGITIMATE_INTEREST' // Art. 6 Abs. 1 lit. f DSGVO
|
||||
| 'LEGAL_OBLIGATION' // Art. 6 Abs. 1 lit. c DSGVO
|
||||
| 'VITAL_INTERESTS' // Art. 6 Abs. 1 lit. d DSGVO
|
||||
| 'PUBLIC_INTEREST' // Art. 6 Abs. 1 lit. e DSGVO
|
||||
|
||||
/**
|
||||
* Aufbewahrungsfristen
|
||||
*/
|
||||
export type RetentionPeriod =
|
||||
| '24_HOURS'
|
||||
| '30_DAYS'
|
||||
| '90_DAYS'
|
||||
| '12_MONTHS'
|
||||
| '24_MONTHS'
|
||||
| '26_MONTHS' // Google Analytics Standard
|
||||
| '36_MONTHS'
|
||||
| '48_MONTHS'
|
||||
| '6_YEARS'
|
||||
| '10_YEARS'
|
||||
| 'UNTIL_REVOCATION'
|
||||
| 'UNTIL_PURPOSE_FULFILLED'
|
||||
| 'UNTIL_ACCOUNT_DELETION'
|
||||
|
||||
/**
|
||||
* Cookie-Kategorien fuer Cookie-Banner
|
||||
*/
|
||||
export type CookieCategory =
|
||||
| 'ESSENTIAL' // Technisch notwendig
|
||||
| 'PERFORMANCE' // Analyse & Performance
|
||||
| 'PERSONALIZATION' // Personalisierung
|
||||
| 'EXTERNAL_MEDIA' // Externe Medien
|
||||
|
||||
/**
|
||||
* Export-Formate fuer Privacy Policy
|
||||
*/
|
||||
export type ExportFormat = 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX'
|
||||
|
||||
/**
|
||||
* Sprachen
|
||||
*/
|
||||
export type SupportedLanguage = 'de' | 'en'
|
||||
18
admin-compliance/lib/sdk/einwilligungen/types/helpers.ts
Normal file
18
admin-compliance/lib/sdk/einwilligungen/types/helpers.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// =============================================================================
|
||||
// HELPER TYPES
|
||||
// =============================================================================
|
||||
|
||||
import type { DataPointCategory } from './enums'
|
||||
import type { LocalizedText } from './data-point'
|
||||
|
||||
/**
|
||||
* Kategorie-Metadaten
|
||||
*/
|
||||
export interface CategoryMetadata {
|
||||
id: DataPointCategory
|
||||
code: string // A, B, C, etc.
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
icon: string // Icon name
|
||||
color: string // Tailwind color class
|
||||
}
|
||||
17
admin-compliance/lib/sdk/einwilligungen/types/index.ts
Normal file
17
admin-compliance/lib/sdk/einwilligungen/types/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Datenpunktkatalog & Datenschutzinformationen-Generator
|
||||
* TypeScript Interfaces
|
||||
*
|
||||
* Barrel re-export of all domain modules.
|
||||
*/
|
||||
|
||||
export * from './enums'
|
||||
export * from './data-point'
|
||||
export * from './catalog-retention'
|
||||
export * from './privacy-policy'
|
||||
export * from './cookie-banner'
|
||||
export * from './consent-management'
|
||||
export * from './state-actions'
|
||||
export * from './helpers'
|
||||
export * from './constants'
|
||||
export * from './warnings'
|
||||
@@ -0,0 +1,77 @@
|
||||
// =============================================================================
|
||||
// PRIVACY POLICY GENERATION
|
||||
// =============================================================================
|
||||
|
||||
import type { SupportedLanguage, ExportFormat } from './enums'
|
||||
import type { LocalizedText } from './data-point'
|
||||
|
||||
/**
|
||||
* Abschnitt in der Privacy Policy
|
||||
*/
|
||||
export interface PrivacyPolicySection {
|
||||
id: string
|
||||
order: number
|
||||
title: LocalizedText
|
||||
content: LocalizedText
|
||||
dataPointIds: string[]
|
||||
isRequired: boolean
|
||||
isGenerated: boolean // true = aus Datenpunkten generiert
|
||||
}
|
||||
|
||||
/**
|
||||
* Unternehmensinfo fuer Privacy Policy
|
||||
*/
|
||||
export interface CompanyInfo {
|
||||
name: string
|
||||
address: string
|
||||
city: string
|
||||
postalCode: string
|
||||
country: string
|
||||
email: string
|
||||
phone?: string
|
||||
website?: string
|
||||
dpoName?: string // Datenschutzbeauftragter
|
||||
dpoEmail?: string
|
||||
dpoPhone?: string
|
||||
registrationNumber?: string // Handelsregister
|
||||
vatId?: string // USt-IdNr
|
||||
}
|
||||
|
||||
/**
|
||||
* Generierte Privacy Policy
|
||||
*/
|
||||
export interface GeneratedPrivacyPolicy {
|
||||
id: string
|
||||
tenantId: string
|
||||
language: SupportedLanguage
|
||||
sections: PrivacyPolicySection[]
|
||||
companyInfo: CompanyInfo
|
||||
generatedAt: Date
|
||||
version: string
|
||||
format: ExportFormat
|
||||
content?: string // Rendered content (HTML/MD)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionen fuer Privacy Policy Generierung
|
||||
*/
|
||||
export interface PrivacyPolicyGenerationOptions {
|
||||
language: SupportedLanguage
|
||||
format: ExportFormat
|
||||
includeDataPoints: string[] // Welche Datenpunkte einschliessen
|
||||
customSections?: PrivacyPolicySection[] // Zusaetzliche Abschnitte
|
||||
styling?: PrivacyPolicyStyling
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling-Optionen fuer PDF/HTML Export
|
||||
*/
|
||||
export interface PrivacyPolicyStyling {
|
||||
primaryColor?: string
|
||||
fontFamily?: string
|
||||
fontSize?: number
|
||||
headerFontSize?: number
|
||||
includeTableOfContents?: boolean
|
||||
includeDateFooter?: boolean
|
||||
logoUrl?: string
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// =============================================================================
|
||||
// EINWILLIGUNGEN STATE & ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
import type { SupportedLanguage, ExportFormat } from './enums'
|
||||
import type { DataPoint } from './data-point'
|
||||
import type { DataPointCatalog } from './catalog-retention'
|
||||
import type { PrivacyPolicySection, GeneratedPrivacyPolicy, CompanyInfo } from './privacy-policy'
|
||||
import type { CookieBannerConfig, CookieBannerStyling, CookieBannerTexts } from './cookie-banner'
|
||||
import type { ConsentStatistics } from './consent-management'
|
||||
|
||||
/**
|
||||
* Aktiver Tab in der Einwilligungen-Ansicht
|
||||
*/
|
||||
export type EinwilligungenTab =
|
||||
| 'catalog'
|
||||
| 'privacy-policy'
|
||||
| 'cookie-banner'
|
||||
| 'retention'
|
||||
| 'consents'
|
||||
|
||||
/**
|
||||
* State fuer Einwilligungen-Modul
|
||||
*/
|
||||
export interface EinwilligungenState {
|
||||
// Data
|
||||
catalog: DataPointCatalog | null
|
||||
selectedDataPoints: string[]
|
||||
privacyPolicy: GeneratedPrivacyPolicy | null
|
||||
cookieBannerConfig: CookieBannerConfig | null
|
||||
companyInfo: CompanyInfo | null
|
||||
consentStatistics: ConsentStatistics | null
|
||||
|
||||
// UI State
|
||||
activeTab: EinwilligungenTab
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
error: string | null
|
||||
|
||||
// Editor State
|
||||
editingDataPoint: DataPoint | null
|
||||
editingSection: PrivacyPolicySection | null
|
||||
|
||||
// Preview
|
||||
previewLanguage: SupportedLanguage
|
||||
previewFormat: ExportFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions fuer Einwilligungen-Reducer
|
||||
*/
|
||||
export type EinwilligungenAction =
|
||||
| { type: 'SET_CATALOG'; payload: DataPointCatalog }
|
||||
| { type: 'SET_SELECTED_DATA_POINTS'; payload: string[] }
|
||||
| { type: 'TOGGLE_DATA_POINT'; payload: string }
|
||||
| { type: 'ADD_CUSTOM_DATA_POINT'; payload: DataPoint }
|
||||
| { type: 'UPDATE_DATA_POINT'; payload: { id: string; data: Partial<DataPoint> } }
|
||||
| { type: 'DELETE_CUSTOM_DATA_POINT'; payload: string }
|
||||
| { type: 'SET_PRIVACY_POLICY'; payload: GeneratedPrivacyPolicy }
|
||||
| { type: 'SET_COOKIE_BANNER_CONFIG'; payload: CookieBannerConfig }
|
||||
| { type: 'UPDATE_COOKIE_BANNER_STYLING'; payload: Partial<CookieBannerStyling> }
|
||||
| { type: 'UPDATE_COOKIE_BANNER_TEXTS'; payload: Partial<CookieBannerTexts> }
|
||||
| { type: 'SET_COMPANY_INFO'; payload: CompanyInfo }
|
||||
| { type: 'SET_CONSENT_STATISTICS'; payload: ConsentStatistics }
|
||||
| { type: 'SET_ACTIVE_TAB'; payload: EinwilligungenTab }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_SAVING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_EDITING_DATA_POINT'; payload: DataPoint | null }
|
||||
| { type: 'SET_EDITING_SECTION'; payload: PrivacyPolicySection | null }
|
||||
| { type: 'SET_PREVIEW_LANGUAGE'; payload: SupportedLanguage }
|
||||
| { type: 'SET_PREVIEW_FORMAT'; payload: ExportFormat }
|
||||
| { type: 'RESET_STATE' }
|
||||
123
admin-compliance/lib/sdk/einwilligungen/types/warnings.ts
Normal file
123
admin-compliance/lib/sdk/einwilligungen/types/warnings.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// =============================================================================
|
||||
// SPECIAL DATA CATEGORY WARNINGS
|
||||
// =============================================================================
|
||||
|
||||
import type { LocalizedText } from './data-point'
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise fuer Art. 9 DSGVO Kategorien
|
||||
*/
|
||||
export interface Article9Warning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const ARTICLE_9_WARNING: Article9Warning = {
|
||||
title: {
|
||||
de: 'Besondere Kategorie personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: 'Special Category of Personal Data (Art. 9 GDPR)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung dieser Daten unterliegt besonderen Anforderungen nach Art. 9 DSGVO. Diese Daten sind besonders schuetzenswert.',
|
||||
en: 'Processing of this data is subject to special requirements under Art. 9 GDPR. This data requires special protection.'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Ausdrueckliche Einwilligung erforderlich (Art. 9 Abs. 2 lit. a DSGVO)',
|
||||
en: 'Explicit consent required (Art. 9(2)(a) GDPR)'
|
||||
},
|
||||
{
|
||||
de: 'Separate Einwilligungserklaerung im UI notwendig',
|
||||
en: 'Separate consent declaration required in UI'
|
||||
},
|
||||
{
|
||||
de: 'Hoehere Dokumentationspflichten',
|
||||
en: 'Higher documentation requirements'
|
||||
},
|
||||
{
|
||||
de: 'Spezielle Loeschverfahren erforderlich',
|
||||
en: 'Special deletion procedures required'
|
||||
},
|
||||
{
|
||||
de: 'Datenschutz-Folgenabschaetzung (DSFA) empfohlen',
|
||||
en: 'Data Protection Impact Assessment (DPIA) recommended'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise fuer Beschaeftigtendaten (BDSG § 26)
|
||||
*/
|
||||
export interface EmployeeDataWarning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const EMPLOYEE_DATA_WARNING: EmployeeDataWarning = {
|
||||
title: {
|
||||
de: 'Beschaeftigtendaten (BDSG § 26)',
|
||||
en: 'Employee Data (BDSG § 26)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung von Beschaeftigtendaten unterliegt besonderen Anforderungen nach § 26 BDSG.',
|
||||
en: 'Processing of employee data is subject to special requirements under § 26 BDSG (German Federal Data Protection Act).'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Aufbewahrungspflichten fuer Lohnunterlagen (6-10 Jahre)',
|
||||
en: 'Retention obligations for payroll records (6-10 years)'
|
||||
},
|
||||
{
|
||||
de: 'Betriebsrat-Beteiligung ggf. erforderlich',
|
||||
en: 'Works council involvement may be required'
|
||||
},
|
||||
{
|
||||
de: 'Verarbeitung nur fuer Zwecke des Beschaeftigungsverhaeltnisses',
|
||||
en: 'Processing only for employment purposes'
|
||||
},
|
||||
{
|
||||
de: 'Besondere Vertraulichkeit bei Gesundheitsdaten',
|
||||
en: 'Special confidentiality for health data'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise fuer KI-Daten (AI Act)
|
||||
*/
|
||||
export interface AIDataWarning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const AI_DATA_WARNING: AIDataWarning = {
|
||||
title: {
|
||||
de: 'KI-Daten (AI Act)',
|
||||
en: 'AI Data (AI Act)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.',
|
||||
en: 'Processing of AI-related data is subject to AI Act transparency requirements.'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Transparenzpflichten bei KI-Verarbeitung',
|
||||
en: 'Transparency obligations for AI processing'
|
||||
},
|
||||
{
|
||||
de: 'Kennzeichnung von KI-generierten Inhalten',
|
||||
en: 'Labeling of AI-generated content'
|
||||
},
|
||||
{
|
||||
de: 'Dokumentation der KI-Modell-Nutzung',
|
||||
en: 'Documentation of AI model usage'
|
||||
},
|
||||
{
|
||||
de: 'Keine Verwendung fuer unerlaubtes Training ohne Einwilligung',
|
||||
en: 'No use for unauthorized training without consent'
|
||||
}
|
||||
]
|
||||
}
|
||||
361
admin-compliance/lib/sdk/export-pdf.ts
Normal file
361
admin-compliance/lib/sdk/export-pdf.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* SDK PDF Export
|
||||
* Generates PDF compliance reports from SDK state
|
||||
*/
|
||||
|
||||
import jsPDF from 'jspdf'
|
||||
import { SDKState, SDK_STEPS } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ExportOptions {
|
||||
includeEvidence?: boolean
|
||||
includeDocuments?: boolean
|
||||
includeRawData?: boolean
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
export const DEFAULT_OPTIONS: ExportOptions = {
|
||||
includeEvidence: true,
|
||||
includeDocuments: true,
|
||||
includeRawData: true,
|
||||
language: 'de',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LABELS (German)
|
||||
// =============================================================================
|
||||
|
||||
export const LABELS_DE = {
|
||||
title: 'AI Compliance SDK - Export',
|
||||
subtitle: 'Compliance-Dokumentation',
|
||||
generatedAt: 'Generiert am',
|
||||
page: 'Seite',
|
||||
summary: 'Zusammenfassung',
|
||||
progress: 'Fortschritt',
|
||||
phase1: 'Phase 1: Automatisches Compliance Assessment',
|
||||
phase2: 'Phase 2: Dokumentengenerierung',
|
||||
useCases: 'Use Cases',
|
||||
risks: 'Risiken',
|
||||
controls: 'Controls',
|
||||
requirements: 'Anforderungen',
|
||||
modules: 'Compliance-Module',
|
||||
evidence: 'Nachweise',
|
||||
checkpoints: 'Checkpoints',
|
||||
noData: 'Keine Daten vorhanden',
|
||||
status: 'Status',
|
||||
completed: 'Abgeschlossen',
|
||||
pending: 'Ausstehend',
|
||||
inProgress: 'In Bearbeitung',
|
||||
severity: 'Schweregrad',
|
||||
mitigation: 'Mitigation',
|
||||
description: 'Beschreibung',
|
||||
category: 'Kategorie',
|
||||
implementation: 'Implementierung',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function formatDate(date: Date | string | undefined): string {
|
||||
if (!date) return '-'
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
doc.setDrawColor(147, 51, 234)
|
||||
doc.setLineWidth(0.5)
|
||||
doc.line(20, 15, pageWidth - 20, 15)
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(100)
|
||||
doc.text(title, 20, 12)
|
||||
doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12)
|
||||
}
|
||||
|
||||
function addFooter(doc: jsPDF, state: SDKState): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
const pageHeight = doc.internal.pageSize.getHeight()
|
||||
doc.setDrawColor(200)
|
||||
doc.setLineWidth(0.3)
|
||||
doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15)
|
||||
doc.setFontSize(8)
|
||||
doc.setTextColor(150)
|
||||
doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10)
|
||||
}
|
||||
|
||||
function addSectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||
doc.setFontSize(14)
|
||||
doc.setTextColor(147, 51, 234)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(title, 20, y)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
return y + 10
|
||||
}
|
||||
|
||||
function addSubsectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(60)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(title, 25, y)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
return y + 7
|
||||
}
|
||||
|
||||
function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number {
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
const lines = doc.splitTextToSize(text, maxWidth)
|
||||
doc.text(lines, x, y)
|
||||
return y + lines.length * 5
|
||||
}
|
||||
|
||||
function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number {
|
||||
const pageHeight = doc.internal.pageSize.getHeight()
|
||||
if (y + requiredSpace > pageHeight - 25) {
|
||||
doc.addPage()
|
||||
return 30
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF EXPORT
|
||||
// =============================================================================
|
||||
|
||||
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||
const doc = new jsPDF()
|
||||
|
||||
let y = 30
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
|
||||
// Title Page
|
||||
doc.setFillColor(147, 51, 234)
|
||||
doc.rect(0, 0, pageWidth, 60, 'F')
|
||||
doc.setFontSize(24)
|
||||
doc.setTextColor(255)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(LABELS_DE.title, 20, 35)
|
||||
doc.setFontSize(14)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(LABELS_DE.subtitle, 20, 48)
|
||||
|
||||
y = 80
|
||||
doc.setDrawColor(200)
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
|
||||
|
||||
y += 15
|
||||
doc.setFontSize(12)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
|
||||
y += 10
|
||||
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
|
||||
y += 10
|
||||
doc.text(`Version: ${state.version}`, 30, y)
|
||||
y += 10
|
||||
const completedSteps = state.completedSteps.length
|
||||
const totalSteps = SDK_STEPS.length
|
||||
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
|
||||
|
||||
y += 30
|
||||
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
|
||||
|
||||
const tocItems = [
|
||||
{ title: 'Zusammenfassung', page: 2 },
|
||||
{ title: 'Phase 1: Compliance Assessment', page: 3 },
|
||||
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
|
||||
{ title: 'Risiken & Controls', page: 5 },
|
||||
{ title: 'Checkpoints', page: 6 },
|
||||
]
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(80)
|
||||
tocItems.forEach((item, idx) => {
|
||||
doc.text(`${idx + 1}. ${item.title}`, 25, y)
|
||||
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
|
||||
y += 7
|
||||
})
|
||||
|
||||
// Summary Page
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.summary, y)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
|
||||
y += 15
|
||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
||||
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
y += 8
|
||||
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
y += 25
|
||||
|
||||
y = addSubsectionTitle(doc, 'Kennzahlen', y)
|
||||
const metrics = [
|
||||
{ label: 'Use Cases', value: state.useCases.length },
|
||||
{ label: 'Risiken identifiziert', value: state.risks.length },
|
||||
{ label: 'Controls definiert', value: state.controls.length },
|
||||
{ label: 'Anforderungen', value: state.requirements.length },
|
||||
{ label: 'Nachweise', value: state.evidence.length },
|
||||
]
|
||||
metrics.forEach(metric => {
|
||||
doc.text(`${metric.label}: ${metric.value}`, 30, y)
|
||||
y += 7
|
||||
})
|
||||
|
||||
// Use Cases
|
||||
y += 10
|
||||
y = checkPageBreak(doc, y)
|
||||
y = addSectionTitle(doc, LABELS_DE.useCases, y)
|
||||
|
||||
if (state.useCases.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.useCases.forEach((uc, idx) => {
|
||||
y = checkPageBreak(doc, y, 50)
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
|
||||
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
|
||||
if (uc.description) {
|
||||
y = addText(doc, uc.description, 25, y + 21, 160)
|
||||
}
|
||||
y += 40
|
||||
})
|
||||
}
|
||||
|
||||
// Risks
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.risks, y)
|
||||
|
||||
if (state.risks.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
const sortedRisks = [...state.risks].sort((a, b) => {
|
||||
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
return (order[a.severity] || 4) - (order[b.severity] || 4)
|
||||
})
|
||||
sortedRisks.forEach((risk, idx) => {
|
||||
y = checkPageBreak(doc, y, 45)
|
||||
const severityColors: Record<string, [number, number, number]> = {
|
||||
CRITICAL: [220, 38, 38], HIGH: [234, 88, 12],
|
||||
MEDIUM: [234, 179, 8], LOW: [34, 197, 94],
|
||||
}
|
||||
const color = severityColors[risk.severity] || [100, 100, 100]
|
||||
doc.setFillColor(color[0], color[1], color[2])
|
||||
doc.rect(20, y - 3, 3, 30, 'F')
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
|
||||
if (risk.description) {
|
||||
y = addText(doc, risk.description, 28, y + 21, 155)
|
||||
}
|
||||
if (risk.mitigation && risk.mitigation.length > 0) {
|
||||
y += 5
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(34, 197, 94)
|
||||
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
|
||||
}
|
||||
y += 15
|
||||
})
|
||||
}
|
||||
|
||||
// Controls
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.controls, y)
|
||||
|
||||
if (state.controls.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.controls.forEach((ctrl, idx) => {
|
||||
y = checkPageBreak(doc, y, 35)
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
|
||||
if (ctrl.description) {
|
||||
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
|
||||
}
|
||||
y += 35
|
||||
})
|
||||
}
|
||||
|
||||
// Checkpoints
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
|
||||
|
||||
const checkpointIds = Object.keys(state.checkpoints)
|
||||
if (checkpointIds.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
checkpointIds.forEach((cpId) => {
|
||||
const cp = state.checkpoints[cpId]
|
||||
y = checkPageBreak(doc, y, 25)
|
||||
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
|
||||
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
|
||||
doc.circle(25, y + 2, 3, 'F')
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.text(cpId, 35, y + 5)
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
|
||||
if (cp.errors && cp.errors.length > 0) {
|
||||
doc.setTextColor(220, 38, 38)
|
||||
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
|
||||
y += 7
|
||||
}
|
||||
y += 20
|
||||
})
|
||||
}
|
||||
|
||||
// Add page numbers
|
||||
const pageCount = doc.getNumberOfPages()
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i)
|
||||
if (i > 1) {
|
||||
addHeader(doc, LABELS_DE.title, i, pageCount)
|
||||
}
|
||||
addFooter(doc, state)
|
||||
}
|
||||
|
||||
return doc.output('blob')
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user