DSGVO-Compliance: Einwilligung, Datenschutzerklärung & Consent-Logging im Upload-Portal (STI-89)
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled

- Datenschutzerklärung unter /portal/datenschutz/ öffentlich erreichbar
- Link zur Datenschutzerklärung in Nachweis-Aufforderungs-E-Mails (HTML + TXT)
- Einwilligungs-Checkbox vor Upload mit Server-Side-Validierung
- Consent-Logging: einwilligung_erteilt_am auf UploadToken (Art. 7 Abs. 1 DSGVO)
- Regelsatz-Korrektur: 449€→563€ in Onboarding-Template (Stand 01/2024)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-21 22:43:01 +00:00
parent f7c122515f
commit 4d751d861d
9 changed files with 72 additions and 2 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2026-03-21 22:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0063_add_anrede_to_destinataer'),
]
operations = [
migrations.AddField(
model_name='uploadtoken',
name='einwilligung_erteilt_am',
field=models.DateTimeField(blank=True, help_text='Zeitpunkt der DSGVO-Einwilligung beim Upload (Art. 7 Abs. 1 DSGVO)', null=True, verbose_name='Einwilligung erteilt am'),
),
]

View File

@@ -1362,6 +1362,10 @@ class UploadToken(models.Model):
erinnerung_gesendet = models.BooleanField(
default=False, verbose_name="Erinnerung gesendet"
)
einwilligung_erteilt_am = models.DateTimeField(
null=True, blank=True, verbose_name="Einwilligung erteilt am",
help_text="Zeitpunkt der DSGVO-Einwilligung beim Upload (Art. 7 Abs. 1 DSGVO)"
)
class Meta:
verbose_name = "Upload-Token"

View File

@@ -6,6 +6,7 @@ Diese URLs sind ohne Login zugänglich (tokenbasierte Authentifizierung).
from django.urls import path
from stiftung.views.portal import (
datenschutzerklaerung,
onboarding_danke,
onboarding_schritt,
upload_danke,
@@ -15,6 +16,12 @@ from stiftung.views.portal import (
app_name = "portal"
urlpatterns = [
# Datenschutzerklärung (öffentlich, kein Token erforderlich)
path(
"datenschutz/",
datenschutzerklaerung,
name="datenschutzerklaerung",
),
# Upload-Portal (bestehende Destinatäre Token-basiert)
path(
"upload/<str:token>/",

View File

@@ -547,6 +547,7 @@ def send_nachweis_aufforderung(self, destinataer_id, nachweis_id, base_url=None)
"gueltig_bis": gueltig_bis,
"halbjahr_label": halbjahr_label,
"quartal_label": quartal_label,
"datenschutz_url": f"{base_url}/portal/datenschutz/",
}
subject = f"Nachweis-Aufforderung: {quartal_label} ({halbjahr_label}) vHTV-Stiftung"
@@ -618,6 +619,7 @@ def send_nachweis_erinnerung(self, token_id, base_url=None):
"gueltig_bis": upload_token.gueltig_bis,
"halbjahr_label": halbjahr_label,
"ist_erinnerung": True,
"datenschutz_url": f"{base_url}/portal/datenschutz/",
}
subject = f"Erinnerung: Nachweis-Upload noch ausstehend {halbjahr_label}"

View File

@@ -33,6 +33,11 @@ from django.views.decorators.http import require_http_methods
from stiftung.models import DokumentDatei, OnboardingEinladung, UploadToken, VierteljahresNachweis
def datenschutzerklaerung(request):
"""Datenschutzerklärung für das öffentliche Portal."""
return render(request, "portal/datenschutzerklaerung.html")
logger = logging.getLogger(__name__)
# Erlaubte Dateitypen für Uploads
@@ -105,6 +110,19 @@ def upload_formular(request, token):
if request.method == "GET":
return render(request, "portal/upload_formular.html", base_context)
# POST: Einwilligung prüfen
einwilligung = request.POST.get("einwilligung")
if not einwilligung:
ctx = {
**base_context,
"einwilligung_fehler": "Bitte erteilen Sie Ihre Einwilligung zur Datenverarbeitung, um fortzufahren.",
}
for kat in [
"studiennachweis", "einkommenssituation", "vermogenssituation", "weitere_dokumente"
]:
ctx[f"{kat}_text"] = request.POST.get(f"{kat}_text", "")
return render(request, "portal/upload_formular.html", ctx)
# POST: Kategorisierte Dateien und Texte verarbeiten
# Kategorien mit ihren DMS-Kontext-Werten und FK-Feldern auf VierteljahresNachweis
KATEGORIEN = [
@@ -228,6 +246,10 @@ def upload_formular(request, token):
if nachweis_update_fields:
nachweis.save(update_fields=list(set(nachweis_update_fields)))
# DSGVO-Einwilligung protokollieren (Art. 7 Abs. 1 DSGVO)
upload_token.einwilligung_erteilt_am = timezone.now()
upload_token.save(update_fields=["einwilligung_erteilt_am"])
# Token einlösen
ip = _get_client_ip(request)
upload_token.einloesen(ip_address=ip)

View File

@@ -71,7 +71,8 @@
</div>
<div class="footer">
van Hees-Theyssen-Vogel'sche Stiftung &bull; Raesfelder Str. 3 &bull; 46499 Hamminkeln &bull; Tel. 02858/836780<br>
Diese E-Mail wurde automatisch erzeugt. Bitte antworten Sie nicht direkt auf diese E-Mail.
Diese E-Mail wurde automatisch erzeugt. Bitte antworten Sie nicht direkt auf diese E-Mail.<br>
<a href="{{ datenschutz_url }}" style="color:#999;">Datenschutzerklärung</a>
</div>
</div>
</body>

View File

@@ -35,3 +35,4 @@ Tel. 02858/836780
---
Diese E-Mail wurde automatisch erzeugt. Bitte antworten Sie nicht direkt auf diese E-Mail.
Datenschutzerklärung: {{ datenschutz_url }}

View File

@@ -108,7 +108,7 @@
<div class="mt-3 p-2 rounded" style="background: #fff8e1; border: 1px solid #ffc107; font-size: 0.82rem;">
<i class="fas fa-info-circle me-1 text-warning"></i>
<strong>Aktuelle Grenzwerte gemäß § 53 Nr. 2 AO (Stand 01/2024):</strong>
Bezüge max. 2.245 € monatlich (5× Regelsatz 449 €); Vermögen max. 15.500 €.
Bezüge max. 2.815 € monatlich (5× Regelsatz 563 €); Vermögen max. 15.500 €.
Bei Haushaltsangehörigen erhöhen sich die Grenzen entsprechend.
Maßgeblich sind die jeweils gültigen Werte zum Zeitpunkt der Prüfung.
</div>

View File

@@ -41,6 +41,10 @@
.deadline { font-size: 13px; color: #888; margin-top: 16px; text-align: center; }
.footer { text-align: center; margin-top: 24px; font-size: 12px; color: #aaa; }
.pflicht-hinweis { font-size: 12px; color: #888; margin-bottom: 16px; }
.einwilligung-box { background: #f0f6ff; border: 1px solid #b0cce8; border-radius: 6px; padding: 14px 16px; margin: 20px 0; font-size: 14px; }
.einwilligung-box label { font-weight: normal; display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.einwilligung-box input[type="checkbox"] { margin-top: 2px; flex-shrink: 0; width: 16px; height: 16px; cursor: pointer; }
.einwilligung-box .einwilligung-fehler { color: #c00; font-size: 13px; margin-top: 6px; }
</style>
</head>
<body>
@@ -128,6 +132,17 @@
<textarea name="weitere_dokumente_text" placeholder="Optionale Anmerkungen oder Beschreibung">{{ weitere_dokumente_text|default:"" }}</textarea>
</div>
<div class="einwilligung-box">
<label>
<input type="checkbox" name="einwilligung" id="einwilligung" required {% if einwilligung_erteilt %}checked{% endif %}>
<span>Ich willige ein, dass die van Hees-Theyssen-Vogel'sche Stiftung die von mir hochgeladenen Dokumente und eingegebenen Daten zum Zweck der Förderprüfung verarbeitet und speichert. Ich habe die <a href="{% url 'portal:datenschutzerklaerung' %}" target="_blank">Datenschutzerklärung</a> gelesen und stimme ihr zu. Die Einwilligung kann ich jederzeit widerrufen (stiftung@vhtv-stiftung.de).
</span>
</label>
{% if einwilligung_fehler %}
<p class="einwilligung-fehler">{{ einwilligung_fehler }}</p>
{% endif %}
</div>
<button type="submit" class="submit-btn">Unterlagen jetzt einreichen</button>
</form>