MeinVLN Beteiligungsportal

Token-basiertes Beteiligungsportal des VLN Manager. Behörden, Träger öffentlicher Belange (TöB) und Einzelpersonen erhalten über einen persönlichen Zugriffscode ohne Login Einsicht in bereitgestellte Dokumente und können einen Fragenkatalog beantworten. Jeder Seitenaufruf und jeder Dateidownload wird revisionssicher protokolliert.

Erreichbar unter https://beteiligung.vlnsachsen.de (eigener nginx-Container nginx-beteiligung). Angelegt, gepflegt und ausgewertet werden Verfahren im geschützten Self-Service-Portal mein.vlnsachsen.de sowie über die REST-API v2.

Hinweis: Dieses Repository dokumentiert das Beteiligungsmodul. Der produktive Code lebt im Monorepo des VLN Manager und verteilt sich auf mehrere Verzeichnisse (siehe Codebestandteile).


Inhalt


Was es ist & wofür

Im Flurbereinigungsverfahren müssen regelmäßig Behörden und Beteiligte angehört werden. Das Beteiligungsportal löst dafür den klassischen Postweg ab:

  • Eine Teilnehmergemeinschaft (TG) oder ein Verbandsmitarbeiter legt ein Beteiligungsverfahren an (Titel, Frist, VKZ).
  • Pro Empfänger wird ein eindeutiger, kryptographisch sicherer Zugriffscode erzeugt (uniqidReal(), 13 Zeichen, Großbuchstaben-Hex).
  • Der Empfänger öffnet seinen Link …/?code=<CODE>, lädt die freigegebenen Dokumente (einzeln oder als ZIP) herunter und beantwortet den Fragenkatalog.
  • Die TG wertet Rückläufe und Zugriffe aus.

Kein Empfänger-Account, kein Passwort der Code ist das Token. Codes werden niemals geraten (Zufallsquelle random_bytes/openssl_random_pseudo_bytes).


Rollen & Ablauf

Rolle Ort Aktion
Verband / TG-Vorsitz / TG-Sachbearbeiter mein.vlnsachsen.de Verfahren anlegen, Empfänger/Fragen/Dateien pflegen, auswerten
Empfänger (Behörde, TöB, Person) beteiligung.vlnsachsen.de/?code=… Dokumente einsehen/herunterladen, Fragen beantworten
System / Integration api.…/v2/beteiligung Verfahren per API anlegen, Public-Read/Submit per Token
Anlegen (meinVLN)            Beteiligung (öffentlich)        Auswertung (meinVLN)
─────────────────           ─────────────────────────       ────────────────────
beteiligung_create.php  ──►  index.php?code=<TOKEN>     ──►  beteiligung_auswertung.php
beteiligung_edit.php         ├─ Dateien (Einzel/ZIP)         ├─ Antworten + Aggregate
 ├─ Stammdaten               └─ antwort.php (Fragen)         └─ Zugriffsverlauf (Audit)
 ├─ Empfänger (→ Code)
 ├─ Fragen
 └─ Dateien

Codebestandteile

Pfad Zweck
0_beteiligung/index.php Public-Entry: Datei-/ZIP-Download, Seite mit Fragenformular
0_beteiligung/antwort.php Public-Submit: speichert Antworten (Pflichtfeld-Validierung, Typ-Normalisierung)
0_beteiligung/templates/ Mustache-Templates des Public-Portals (login, main, header, footer)
0_beteiligung/set.php Hilfsskript: listet Upload-Verzeichnis (Debug)
require/require-beteiligungsverfahren.php Domänenklasse BETEILIGUNGSVERFAHREN + Factory get_BETEILIGUNGSVERFAHREN()
0_meinvln/beteiligung_create.php Verfahren anlegen (JSON-Endpoint)
0_meinvln/beteiligung_edit.php CRUD-Dispatcher (?action=meta|empfaenger_*|frage_save|frage_del|datei_upload|datei_del|verfahren_del)
0_meinvln/beteiligung_auswertung.php Antworten-Auswertung + Zugriffsverlauf
0_meinvln/templates/beteiligung_{edit,auswertung}.mustache Admin-Oberflächen
0_api/v2/plugins/beteiligung.php REST-API v2 (List/Create + Public Read/Submit)

Anlegen erfolgt zusätzlich über ein Modal in 0_meinvln/templates/meinvln_main.mustache, das auf beteiligung_create.php postet.


Datenmodell

Zwei Tabellen, keine ORM rohes MySQLi über den DB-Wrapper.

BETEILIGUNGSVERFAHREN

Spalte Typ Bedeutung
id INT, PK, auto Verfahrens-ID
VKZ INT Verfahrenskennzeichen (TG-Zuordnung)
von INT (Unix-TS) Beginn des Beteiligungszeitraums
bis INT (Unix-TS) Frist (Zugriff/Antwort enden 23:59:59 dieses Tages)
CODES JSON Empfänger inkl. Code, Stammdaten, Antworten
DATEN JSON Array der freigegebenen Datei-IDs
SETTING JSON Titel, Ersteller, Fragenkatalog

Indizes: idx_vkz, idx_bis, idx_vkz_bis.

CODES Map Code → Empfänger:

{
  "A1B2C3D4E5F60": {
    "name":   "Landratsamt Musterkreis",
    "anrede": "Damen und Herren",
    "email":  "post@lra-muster.de",
    "z":      [],
    "antworten":  { "f1": "Ja", "f2": ["A", "C"], "f3": 4 },
    "antwort_ts": 1718524800
  }
}

SETTING:

{
  "titel":      "Anhörung Wegebau Gewann Süd",
  "created_by": 62,
  "created_at": 1718438400,
  "fragen":     [ /* siehe Fragenkatalog */ ]
}

DATEN: ["10231", "10232", "10240"] IDs aus dem zentralen Dateisystem (DATEI/ORDNER). Physische Ablage im Ordnerbaum unter <VKZ>/Beteiligung/<verfahren_id>.

BETEILIGUNGSVERFAHREN_ZUGRIFF (Audit-Log)

Append-only, race-condition-frei ein INSERT pro Zugriff. Ersetzt das frühere JSON-Logging in CODES[code].{a,z} (seit 2026-05-23; nie wieder Zugriffslogs in die JSON-Spalte schreiben).

Spalte Typ Bedeutung
verfahren_id INT FK auf BETEILIGUNGSVERFAHREN.id
code VARCHAR Empfänger-Code
typ ENUM(page,file,zip) Art des Zugriffs
file_id INT NULL bei typ=file die Datei-ID
ts INT (Unix-TS) Zeitpunkt
ip VARBINARY via INET6_ATON() (IPv4/IPv6)
user_agent VARCHAR(255) gekürzter UA-String

Aggregation für die Auswertung über BETEILIGUNGSVERFAHREN::get_zugriffsstats() (GROUP BY code, typ), Detailverlauf per JOIN auf DATA_FILE in beteiligung_auswertung.php.


Fragenkatalog

Fragen liegen in SETTING.fragen. Jede Frage hat eine stabile ID (f1, f2, …), einen Typ und ein pflicht-Flag. Erlaubte Typen:

Typ Bedeutung Zusatzfelder Antwortformat
single Einfachauswahl optionen (≥ 2) String (eine Option)
multi Mehrfachauswahl optionen (≥ 2) String-Array
text Freitext String
zahl Zahl Number
datum Datum parsebarer Datums-String
skala Skala min, max (max > min) Integer in [min, max]
[
  { "id": "f1", "typ": "single", "frage": "Bestehen Einwände?",
    "pflicht": true, "optionen": ["Ja", "Nein"] },
  { "id": "f2", "typ": "multi", "frage": "Betroffene Belange",
    "optionen": ["Natur", "Wasser", "Verkehr"] },
  { "id": "f3", "typ": "skala", "frage": "Dringlichkeit", "min": 1, "max": 5 }
]

Eingehende Antworten werden serverseitig gegen den Katalog validiert (Optionen müssen existieren, Skala im Bereich, Pflichtfragen vorhanden) sowohl in antwort.php als auch im API-Endpoint.


REST-API v2

Basis: https://api.…/v2/beteiligung. Admin-Routen erfordern einen API-Key (auth: true), Public-Routen nutzen den Empfänger-Token.

Methode Route Zugriff Zweck
GET /beteiligung Admin Verfahren listen (?vkz=, ?status=aktiv|abgelaufen|alle)
POST /beteiligung Admin Verfahren anlegen (inkl. Empfänger + Fragen)
GET /beteiligung/{token} Public Verfahren + Dateien + Fragen + bisherige Antwort
POST /beteiligung/{token}/antworten Public Antworten speichern (Frist + Validierung)
POST /beteiligung/{id}/empfaenger Admin Weiteren Empfänger anhängen

Verfahren anlegen:

curl -X POST https://api.…/v2/beteiligung \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
        "vkz": 1234,
        "bis": "2026-07-31",
        "empfaenger": [
          { "name": "Landratsamt Musterkreis", "anrede": "Damen und Herren", "email": "post@lra-muster.de" }
        ],
        "fragen": [
          { "typ": "single", "frage": "Einwände?", "pflicht": true, "optionen": ["Ja","Nein"] }
        ]
      }'

Antworten einreichen (öffentlich, per Token):

curl -X POST https://api.…/v2/beteiligung/A1B2C3D4E5F60/antworten \
  -H "Content-Type: application/json" \
  -d '{ "antworten": { "f1": "Nein" } }'

Fehlercodes: 400 (ungültige Eingabe), 404 (Token/Verfahren unbekannt), 410 (Frist abgelaufen), 422 (Pflichtfragen fehlen).


Berechtigungen

  • Admin-Seiten (mein.vlnsachsen.de): Modul-Gate MODUL_BETEILIGUNG ($RIGHT->darf_modul(...)) Verbandsmitarbeiter sowie freigeschaltete TG-Vorsitzende/Stellvertreter.
  • Pro Verfahren zusätzlich tg_access($VKZ): TG-Nutzer sehen nur Verfahren ihrer VKZ, der Verband sieht alle.
  • Public-Portal: keine Anmeldung; Autorisierung allein über den Code in CODES. Dateien werden nur über jumpfile()/jumpzip() ausgeliefert (kein direkter Pfadzugriff), Downloads vor der Auslieferung protokolliert.

Betrieb & Deployment

Eigener nginx-Container, getrennt von den anderen Subdomains.

  • docker-compose: /daten/dockervorlagen/manager/docker-compose.beteiligung.yaml (Service nginxbeteiligung, Container nginx-beteiligung, Routing via Traefik auf beteiligung.vlnsachsen.de, TLS über myresolver).
  • nginx-Conf: /daten/dockervorlagen/manager/nginx_beteiligung.conf (Webroot 0_beteiligung/, PHP-FPM phpworker:9000).
  • URL-Konstante: URL_BETEILIGUNG = "https://beteiligung.vlnsachsen.de" (in require/require-main.php).
# Container (neu) starten
cd /daten/dockervorlagen/manager
docker compose -f docker-compose.beteiligung.yaml up -d

# PHP-Syntaxcheck (PHP läuft nur im Container)
docker exec php-worker php -l /var/www/html/require/require-beteiligungsverfahren.php

Sicherheitshinweise

  • Codes sind Geheimnisse. Ein Code gewährt vollen Lese-/Antwortzugriff auf ein Verfahren nur über sichere Kanäle versenden.
  • Zugriffslogs gehören in die Audit-Tabelle BETEILIGUNGSVERFAHREN_ZUGRIFF, nie zurück in die CODES-JSON-Spalte.
  • Frist (bis) ist hart: Nach Ablauf verweigert die API das Speichern (410 Gone).
  • Eingaben werden serverseitig validiert Client-Werte (Optionen, Skala, Pflicht) nie ungeprüft übernehmen.
  • Dateiauslieferung nur indirekt über die Domänenklasse; keine Pfade aus Client-Input.

Teil des VLN Manager (Flurbereinigung Sachsen). Domänenklasse: require/require-beteiligungsverfahren.php.

S
Description
No description provided
Readme 30 KiB