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( erinnerung_gesendet = models.BooleanField(
default=False, verbose_name="Erinnerung gesendet" 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: class Meta:
verbose_name = "Upload-Token" 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 django.urls import path
from stiftung.views.portal import ( from stiftung.views.portal import (
datenschutzerklaerung,
onboarding_danke, onboarding_danke,
onboarding_schritt, onboarding_schritt,
upload_danke, upload_danke,
@@ -15,6 +16,12 @@ from stiftung.views.portal import (
app_name = "portal" app_name = "portal"
urlpatterns = [ urlpatterns = [
# Datenschutzerklärung (öffentlich, kein Token erforderlich)
path(
"datenschutz/",
datenschutzerklaerung,
name="datenschutzerklaerung",
),
# Upload-Portal (bestehende Destinatäre Token-basiert) # Upload-Portal (bestehende Destinatäre Token-basiert)
path( path(
"upload/<str:token>/", "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, "gueltig_bis": gueltig_bis,
"halbjahr_label": halbjahr_label, "halbjahr_label": halbjahr_label,
"quartal_label": quartal_label, "quartal_label": quartal_label,
"datenschutz_url": f"{base_url}/portal/datenschutz/",
} }
subject = f"Nachweis-Aufforderung: {quartal_label} ({halbjahr_label}) vHTV-Stiftung" 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, "gueltig_bis": upload_token.gueltig_bis,
"halbjahr_label": halbjahr_label, "halbjahr_label": halbjahr_label,
"ist_erinnerung": True, "ist_erinnerung": True,
"datenschutz_url": f"{base_url}/portal/datenschutz/",
} }
subject = f"Erinnerung: Nachweis-Upload noch ausstehend {halbjahr_label}" 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 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__) logger = logging.getLogger(__name__)
# Erlaubte Dateitypen für Uploads # Erlaubte Dateitypen für Uploads
@@ -105,6 +110,19 @@ def upload_formular(request, token):
if request.method == "GET": if request.method == "GET":
return render(request, "portal/upload_formular.html", base_context) 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 # POST: Kategorisierte Dateien und Texte verarbeiten
# Kategorien mit ihren DMS-Kontext-Werten und FK-Feldern auf VierteljahresNachweis # Kategorien mit ihren DMS-Kontext-Werten und FK-Feldern auf VierteljahresNachweis
KATEGORIEN = [ KATEGORIEN = [
@@ -228,6 +246,10 @@ def upload_formular(request, token):
if nachweis_update_fields: if nachweis_update_fields:
nachweis.save(update_fields=list(set(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 # Token einlösen
ip = _get_client_ip(request) ip = _get_client_ip(request)
upload_token.einloesen(ip_address=ip) upload_token.einloesen(ip_address=ip)

View File

@@ -71,7 +71,8 @@
</div> </div>
<div class="footer"> <div class="footer">
van Hees-Theyssen-Vogel'sche Stiftung &bull; Raesfelder Str. 3 &bull; 46499 Hamminkeln &bull; Tel. 02858/836780<br> 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>
</div> </div>
</body> </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. 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;"> <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> <i class="fas fa-info-circle me-1 text-warning"></i>
<strong>Aktuelle Grenzwerte gemäß § 53 Nr. 2 AO (Stand 01/2024):</strong> <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. Bei Haushaltsangehörigen erhöhen sich die Grenzen entsprechend.
Maßgeblich sind die jeweils gültigen Werte zum Zeitpunkt der Prüfung. Maßgeblich sind die jeweils gültigen Werte zum Zeitpunkt der Prüfung.
</div> </div>

View File

@@ -41,6 +41,10 @@
.deadline { font-size: 13px; color: #888; margin-top: 16px; text-align: center; } .deadline { font-size: 13px; color: #888; margin-top: 16px; text-align: center; }
.footer { text-align: center; margin-top: 24px; font-size: 12px; color: #aaa; } .footer { text-align: center; margin-top: 24px; font-size: 12px; color: #aaa; }
.pflicht-hinweis { font-size: 12px; color: #888; margin-bottom: 16px; } .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> </style>
</head> </head>
<body> <body>
@@ -128,6 +132,17 @@
<textarea name="weitere_dokumente_text" placeholder="Optionale Anmerkungen oder Beschreibung">{{ weitere_dokumente_text|default:"" }}</textarea> <textarea name="weitere_dokumente_text" placeholder="Optionale Anmerkungen oder Beschreibung">{{ weitere_dokumente_text|default:"" }}</textarea>
</div> </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> <button type="submit" class="submit-btn">Unterlagen jetzt einreichen</button>
</form> </form>