Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) — E-Mail-Login, Enum-Status, Members-Flag #2

Open
opened 2026-08-02 16:39:41 +02:00 by Simon · 6 comments
Owner

Plan: Mitglieder-Backend — OData V4 Service + SQLite (LXC 10.20.10.10)

Update 01.09.2026 (v4 — final):

  • Visibility-Enum: Public | Members (früher „Community“) — als Members geflagte Seiten sehen alle bestätigten Mitglieder, können sie ansehen und herunterladen
  • Member-Status ist ein Enum: Status = Active / Deleted (Soft-Delete, kein Bool)
  • Kein pro-Mitglied-per-Seite-Grant: Zugriff = Rolle × Sichtbarkeit (Enum-Flag am Content)
  • Bestätigtes Mitglied = Status Active + Role ∈ {Mitglied, Admin} → sieht und lädt alle Members-Seiten
  • Login-ID = E-Mail (kein separates Username-Feld)

Entscheidungen (Stand 01.09.2026, alle bestätigt):

  • DB: SQLite direkt auf LXC 10.20.10.10 (/var/lib/ejun-auth/members.db)
  • API: OData V4 = .NET 8 + Microsoft.AspNetCore.OData
  • Passwort: C#-Standard PasswordHasher (PBKDF2-SHA256 + random Salt)
  • Login-ID: E-Mail
  • Frontend: Vanilla-HTML/JS-Admin-Panel gegen /odata/ (kein Build-Step, Design-Sprache wie login.html)
  • Transport: HTTPS appdev30.berleb.net → OPNsense 10.10.10.1 (TLS) → Nginx auth_request → OData-Service

Warum .NET 8: OData-V4-Referenz ($filter/$expand/$orderby/Actions/CSDL/EnumType). VM-Fakten (live 01.09): kein .NET/Node, Python 3.12.3; .NET 8 SDK per apt verfügbar (dotnet-sdk-8.0 8.0.130). RAM knapp: ~400 MB frei, kein Swap → Workstation-GC, MemoryMax=350M via systemd.

Ziel:

  • Admin legt Mitglieder an (Vorname, Nachname, Gemeinde, Rolle, E-Mail, Handy, Bemerkung), bestätigt sie, weicht sie soft aus
  • Als Members geflagte Seiten (z. B. uvp-hayforst-report.html) sehen alle bestätigten Mitglieder — ansehen und herunterladen
  • Nicht die ganze Seite gesperrt, nur die geflagten Inhalte; Rest bleibt öffentlich

OData-V4-Modell (CSDL, Edm.EnumType):

Enums:

  • MemberRole: Admin | Mitglied | Unverifiziert
  • MemberStatus: Active | Deleted (Soft-Delete)
  • ContentVisibility: Public | Members

EntitySets:

  • Members (Key: Email)
  • Contents (Key: Slug)
  • Grants → weg (Zugriff = Rolle × Sichtbarkeit, keine Einzelzuordnung)

Member (Key = Email, Login-ID):

  • Email (string, unique, Login-ID)
  • FirstName, LastName (string)
  • Gemeinde (string)
  • Role (MemberRole)
  • Status (MemberStatus) — Active/Deleted
  • Phone (string, nullable)
  • RegisteredAt (datetime)
  • ConfirmedAt (datetime, nullable) — wenn Simon bestätigt (Unverifiziert → Mitglied)
  • InternalNote (string, nur Admin)
  • PasswordHash (string, C#-PasswordHasher, nur intern)

Content (Key = Slug):

  • Slug — z. B. uvp-hayforst
  • Path — /dev/uvp-hayforst-report.html
  • Title (string)
  • Visibility (ContentVisibility) — Public | Members

Zugriffsregeln (fnAuthz(path)):

  • Public → alle, ohne Login
  • Members → Status Active && Role ∈ {Mitglied, Admin} (bestätigtes Mitglied) → ansehen + herunterladen
  • Angemeldet aber Unverifiziert/Deleted → 403 (fehlende Bestätigung)
  • Keine Credentials → 401 → Login-Card
  • /admin/ → Role Admin && Status Active

Mitglieder-Lebenszyklus (Admin im Panel):

  1. Anlegen → Role Unverifiziert, Status Active, generiertes Passwort
  2. Bestätigen (von Simon) → Role Mitglied, ConfirmedAt = jetzt → Members-Zugriff freigeschaltet
  3. Auswählen → Status Deleted (Soft-Delete, jederzeit reaktivierbar; Rolle bleibt gespeichert)

Custom Actions (OData-V4):

  • POST /odata/ejun.verify(Email,Password) → {valid, role, status}
  • POST /odata/ejun.confirm(Email) → Unverifiziert → Mitglied, setzt ConfirmedAt
  • POST /odata/ejun.setStatus(Email, Status) → Active/Deleted (Soft-Delete/Reaktivierung)
  • POST /odata/ejun.resetPassword(Email) → temporäres Passwort
  • POST /odata/ejun.register(Email, FirstName, LastName, Gemeinde, Phone) → legt Mitglied als Unverifiziert/Active mit generiertem Passwort an; 409 wenn E-Mail schon existiert (überschreibt NICHT wie der rohe PUT)
  • (keine ejun.grant/ejun.revoke mehr — Sichtbarkeit lebt am Content)

Admin-Panel (Vanilla-HTML/JS gegen OData, kein Build-Step):

  • /admin/members — Liste (filterbar nach Rolle/Status), anlegen, bestätigen, soft-delete, Passwort-Reset
  • /admin/contents — Seiten als Members flaggen
  • Gleiche Design-Sprache wie login.html (Creme/Peach/Grün)

Beispiel-Queries (bekanntes OData-V4-Vokabular):

  • GET /odata/Members?$filter=Role eq ejun.MemberRole/'Mitglied' and Status eq ejun.MemberStatus/'Active'
  • GET /odata/Members?$filter=contains(Email,'@berleb.net')&$orderby=ConfirmedAt desc
  • GET /odata/Contents?$filter=Visibility eq ejun.ContentVisibility/'Members'
  • GET /odata/$metadata (CSDL), GET /odata/ (Service-Doc)

Architektur:

Browser ── HTTPS ── OPNsense (10.10.10.1, TLS) ── HTTP ── Nginx (10.20.10.10)
                                              /dev/uvp-hayforst-report.html (Visibility: Members)
                                              auth_request → /authz
                                              OData V4 Service (Kestrel, .NET 8, 127.0.0.1:8081)
                                              - EntitySets: Members, Contents (+ 3 Enums)
                                              - Function fnAuthz(path) → 200/401/403
                                              - Actions: ejun.verify / confirm / setStatus / resetPassword
                                              EF Core → SQLite /var/lib/ejun-auth/members.db

Umsetzung (Phasen):

  • Phase 1 — OData-Service + DB + Nginx:
    • .NET 8-Projekt ejun-oapi (Kestrel, 127.0.0.1:8081), OData-Modell (2 EntitySets + 3 Enums) + EF Core/SQLite
    • fnAuthz mit Zugriffsregeln (Public/Members/Admin)
    • systemd ejun-oapi.service (Restart=on-failure, Workstation-GC, MemoryMax=350M)
    • Nginx: auth_request auf Members-Seiten + /authz-Location
    • Seeds: simon@berleb.net (Admin, Active), Content uvp-hayforst (Members)
  • Phase 2 — Admin-Panel: /admin/members (anlegen/bestätigen/soft-delete/reset) + /admin/contents (Flag)
  • Phase 3 — Login-UX + HTTPS-Fix: login.html E-Mail-Feld (statt Username), JS-HTTPS-Regex fix, Nginx-GATE-302 scheme-aware (X-Forwarded-Proto)
  • Phase 4 — E2E + Absicherung: Playwright (Admin → Members-Seite, Mitglied → 200, Unverifiziert → 403, ohne Credentials → 401); no-store, X-Auth-Email-Leak prüfen
  • Phase 5 — Registrierung (öffentlicher Endpunkt): ejun.register-Action + öffentlicher /odata/-Pfad in Nginx (nur verify + register öffentlich, rest intern) + Formular auf registrierung.html + DSGVO-Text in datenschutz.html (Details im Abschnitt „Phase 5“ unten)

Phase 5 — Registrierung (öffentlicher Endpunkt) — Details (Stand 03.09.2026):

Auslöser: registrierung.html ist aktuell ein toter Endpunkt (nur mailto: + WhatsApp-Button, Text „kein Formular“). Ziel: echte Selbst-Registrierung gegen das bestehende OData-Backend, ohne die Login-Mechanik anzufassen.

Backend (1 neue unbound Action):

  • ejun.register(Email, FirstName, LastName, Gemeinde, Phone) in ODataOperationImportController:
    • E-Mail normalisieren (lowercase), Duplikat-Check → bei Treffer 409 Conflict (bestehendes Mitglied bleibt unangetastet — kein PUT-Override)
    • Neuanlage: Role = Unverifiziert, Status = Active, generiertes Passwort (PasswordHasher), RegisteredAt = jetzt, ConfirmedAt = null
    • Rückgabe: { email, status, generatedPassword } (generiertes PW nur Admin/sichtbar bis resetPassword)
  • EDM-Modell: builder.Action("register") + Parameter ergänzen (PascalCase-Namen = EDM-Parameter-Namen, sonst 500 NRE)

Nginx (öffentliches /odata/ — heute fehlt):

  • Aktuelle Config hat nur den internen /authz_internal (Subrequest). Neu: location /odata/ → proxy_pass http://127.0.0.1:8081/odata/
  • Nur verify + register öffentlich, rest intern halten: entweder location = /odata/verify + location = /odata/register (öffentlich) und location /odata/ (Default → auth_request oder 403), oder ein map/Whitelist im Proxy
  • Rate-Limit auf /odata/register (z. B. limit_req 5 req/min pro IP) gegen Spam-Registrierungen
  • X-Auth-Email-Leak + no-store wie Phase 4

Frontend (registrierung.html):

  • Formular im EjUn-Design (Creme/Peach/Grün, wie login.html): Vorname, Nachname, E-Mail, Gemeinde, Handy (optional), DSGVO-Checkbox
  • JS: fetch('/odata/register', {method:'POST', body:{Email, FirstName, ...}}) → Erfolgs-Banner („Danke! Wir melden uns nach der Bestätigung“), Duplikat-Banner („Schon registriert — wir melden uns“)
  • Bestätigung bleibt manuell über Simon (ejun.confirm / Admin-Panel) — passt zum Bestätigungs-Lebenszyklus
  • datenschutz.html: „kein eigenes Formular“-Passus (Z. 54-55) anpassen → Daten fließen jetzt per Formular in members.db (Name, E-Mail, Gemeinde, Handy, Hash)

Abhängigkeiten:

  • Phase 2 (Admin-Panel) ist der Komfort-Pfad — Registrierung läuft ohne Admin-Panel (Bestätigung geht auch per rohem ejun.confirm)
  • Neue Mitglieder sehen nach Bestätigung automatisch alle Members-Seiten (Zugriff = Rolle × Sichtbarkeit, kein Einzel-Grant)

Verifikation (Phase 5):

  • curl POST /odata/register (neue E-Mail → 200, Duplikat → 409)
  • E2E: Formular abgeben → Mitglied in DB als Unverifiziert → confirm → sieht Members-Seite
  • Nginx: /odata/register öffentlich erreichbar, /odata/Members bleibt intern
  • Playwright: registrierung.html × 393/768/1024 px, Erfolgs-/Duplikat-Banner

Risiko:

  • RAM knapp (~400 MB frei, kein Swap) → schlanker Service, MemoryMax
  • Fällt ejun-oapi aus → Members-Seiten 503 (systemd Restart=on-failure)
  • Soft-Delete: gelöschte Mitglieder bleiben als Zeile (Tiny-DB, kein Problem; Email-Unique-Key bleibt reserviert → Reaktivierung ohne Kollision)

Status: Umsetzung läuft (Phase 2 + 3 + 5 live, News-Gate live, 03.09.2026).

  • Phase 1 (Backend): ✅ fertig & live
  • Phase 2 (Admin-Panel): ✅ fertig & live — members.html + contents.html + admin.js (Fetch-Creds-Fix: absolute API-Base + Auth-Header + sessionStorage), Browser-E2E 19/19 grün (Commit e0b02f8)
  • Phase 3 (Login-UX): ✅ fertig & live — E-Mail-Feld + absolute_redirect off (Gate-302 scheme-aware) + CRED-URL-Regex https?: (kein HTTP-Downgrade hinter OPNsense-TLS); Report-Gate war Rubber-Stamp (DB-Pfad /reports/ → /dev/), jetzt 401/401/200 wie designed (Commit 7f3eecf)
  • Phase 4 (E2E): ⚠️ grün, no-store/X-Auth-Email-Leak offen
  • Phase 5 (Registrierung): ✅ fertig & live — ejun.register-Action + öffentliches /odata/register + registrierung.html-Formular
  • News-Gate (Details nur für Mitglieder): ✅ live — die drei aktuellen News-Details (uvp-hayforst, umzingelungsgutachten, entlastung-franken-neuhofen) sind jetzt per UVP-Pattern geschützt: GATE 302 → login.html?next=…-report.html, REPORT auth_request + Contents-DB (Visibility=Mitglied); News-Teaser auf der Startseite bleiben public. Playwright-E2E 11/11 (Commit 7cf8b83)
# Plan: Mitglieder-Backend — OData V4 Service + SQLite (LXC 10.20.10.10) > **Update 01.09.2026 (v4 — final):** > - **Visibility-Enum:** `Public` | **`Members`** (früher „Community“) — als `Members` geflagte Seiten sehen **alle bestätigten Mitglieder**, können sie ansehen und herunterladen > - **Member-Status ist ein Enum:** `Status` = `Active` / `Deleted` (Soft-Delete, kein Bool) > - **Kein pro-Mitglied-per-Seite-Grant:** Zugriff = Rolle × Sichtbarkeit (Enum-Flag am Content) > - **Bestätigtes Mitglied** = `Status Active` + `Role ∈ {Mitglied, Admin}` → sieht und lädt alle Members-Seiten > - **Login-ID = E-Mail** (kein separates Username-Feld) **Entscheidungen (Stand 01.09.2026, alle bestätigt):** - **DB:** SQLite direkt auf LXC `10.20.10.10` (`/var/lib/ejun-auth/members.db`) - **API:** **OData V4 = .NET 8 + Microsoft.AspNetCore.OData** - **Passwort:** **C#-Standard** `PasswordHasher` (PBKDF2-SHA256 + random Salt) - **Login-ID:** **E-Mail** - **Frontend:** **Vanilla-HTML/JS-Admin-Panel** gegen `/odata/` (kein Build-Step, Design-Sprache wie login.html) - **Transport:** HTTPS `appdev30.berleb.net` → OPNsense 10.10.10.1 (TLS) → Nginx `auth_request` → OData-Service **Warum .NET 8:** OData-V4-Referenz ($filter/$expand/$orderby/Actions/CSDL/EnumType). VM-Fakten (live 01.09): kein .NET/Node, Python 3.12.3; .NET 8 SDK per apt verfügbar (dotnet-sdk-8.0 8.0.130). **RAM knapp: ~400 MB frei, kein Swap** → Workstation-GC, `MemoryMax=350M` via systemd. **Ziel:** - Admin legt Mitglieder an (Vorname, Nachname, Gemeinde, Rolle, E-Mail, Handy, Bemerkung), bestätigt sie, weicht sie soft aus - Als **`Members`** geflagte Seiten (z. B. `uvp-hayforst-report.html`) sehen **alle bestätigten Mitglieder** — ansehen und herunterladen - **Nicht die ganze Seite** gesperrt, nur die geflagten Inhalte; Rest bleibt öffentlich **OData-V4-Modell (CSDL, Edm.EnumType):** Enums: - `MemberRole`: `Admin` | `Mitglied` | `Unverifiziert` - `MemberStatus`: `Active` | `Deleted` (Soft-Delete) - `ContentVisibility`: `Public` | **`Members`** EntitySets: - `Members` (Key: `Email`) - `Contents` (Key: `Slug`) - ~~`Grants`~~ → **weg** (Zugriff = Rolle × Sichtbarkeit, keine Einzelzuordnung) `Member` (Key = `Email`, Login-ID): - `Email` (string, unique, **Login-ID**) - `FirstName`, `LastName` (string) - `Gemeinde` (string) - `Role` (MemberRole) - `Status` (MemberStatus) — `Active`/`Deleted` - `Phone` (string, nullable) - `RegisteredAt` (datetime) - `ConfirmedAt` (datetime, nullable) — wenn Simon bestätigt (Unverifiziert → Mitglied) - `InternalNote` (string, nur Admin) - `PasswordHash` (string, C#-PasswordHasher, nur intern) `Content` (Key = `Slug`): - `Slug` — z. B. `uvp-hayforst` - `Path` — `/dev/uvp-hayforst-report.html` - `Title` (string) - `Visibility` (ContentVisibility) — `Public` | **`Members`** **Zugriffsregeln (`fnAuthz(path)`):** - `Public` → alle, ohne Login - `Members` → `Status Active` && `Role ∈ {Mitglied, Admin}` (bestätigtes Mitglied) → **ansehen + herunterladen** - Angemeldet aber `Unverifiziert`/`Deleted` → `403` (fehlende Bestätigung) - Keine Credentials → `401` → Login-Card - `/admin/` → `Role Admin` && `Status Active` **Mitglieder-Lebenszyklus (Admin im Panel):** 1. Anlegen → `Role Unverifiziert`, `Status Active`, generiertes Passwort 2. Bestätigen (von Simon) → `Role Mitglied`, `ConfirmedAt = jetzt` → Members-Zugriff freigeschaltet 3. Auswählen → `Status Deleted` (Soft-Delete, jederzeit reaktivierbar; Rolle bleibt gespeichert) **Custom Actions (OData-V4):** - `POST /odata/ejun.verify(Email,Password)` → `{valid, role, status}` - `POST /odata/ejun.confirm(Email)` → `Unverifiziert → Mitglied`, setzt `ConfirmedAt` - `POST /odata/ejun.setStatus(Email, Status)` → `Active`/`Deleted` (Soft-Delete/Reaktivierung) - `POST /odata/ejun.resetPassword(Email)` → temporäres Passwort - `POST /odata/ejun.register(Email, FirstName, LastName, Gemeinde, Phone)` → legt Mitglied als `Unverifiziert`/`Active` mit generiertem Passwort an; `409` wenn E-Mail schon existiert (überschreibt NICHT wie der rohe PUT) - (keine `ejun.grant`/`ejun.revoke` mehr — Sichtbarkeit lebt am Content) **Admin-Panel (Vanilla-HTML/JS gegen OData, kein Build-Step):** - `/admin/members` — Liste (filterbar nach Rolle/Status), anlegen, bestätigen, soft-delete, Passwort-Reset - `/admin/contents` — Seiten als `Members` flaggen - Gleiche Design-Sprache wie login.html (Creme/Peach/Grün) **Beispiel-Queries (bekanntes OData-V4-Vokabular):** - `GET /odata/Members?$filter=Role eq ejun.MemberRole/'Mitglied' and Status eq ejun.MemberStatus/'Active'` - `GET /odata/Members?$filter=contains(Email,'@berleb.net')&$orderby=ConfirmedAt desc` - `GET /odata/Contents?$filter=Visibility eq ejun.ContentVisibility/'Members'` - `GET /odata/$metadata` (CSDL), `GET /odata/` (Service-Doc) **Architektur:** ``` Browser ── HTTPS ── OPNsense (10.10.10.1, TLS) ── HTTP ── Nginx (10.20.10.10) /dev/uvp-hayforst-report.html (Visibility: Members) auth_request → /authz OData V4 Service (Kestrel, .NET 8, 127.0.0.1:8081) - EntitySets: Members, Contents (+ 3 Enums) - Function fnAuthz(path) → 200/401/403 - Actions: ejun.verify / confirm / setStatus / resetPassword EF Core → SQLite /var/lib/ejun-auth/members.db ``` **Umsetzung (Phasen):** - **Phase 1 — OData-Service + DB + Nginx:** - `.NET 8`-Projekt `ejun-oapi` (Kestrel, `127.0.0.1:8081`), OData-Modell (2 EntitySets + 3 Enums) + EF Core/SQLite - `fnAuthz` mit Zugriffsregeln (Public/Members/Admin) - systemd `ejun-oapi.service` (Restart=on-failure, Workstation-GC, MemoryMax=350M) - Nginx: `auth_request` auf Members-Seiten + `/authz`-Location - Seeds: `simon@berleb.net` (Admin, Active), Content `uvp-hayforst` (Members) - **Phase 2 — Admin-Panel:** `/admin/members` (anlegen/bestätigen/soft-delete/reset) + `/admin/contents` (Flag) - **Phase 3 — Login-UX + HTTPS-Fix:** `login.html` E-Mail-Feld (statt Username), JS-HTTPS-Regex fix, Nginx-GATE-302 scheme-aware (`X-Forwarded-Proto`) - **Phase 4 — E2E + Absicherung:** Playwright (Admin → Members-Seite, Mitglied → 200, Unverifiziert → 403, ohne Credentials → 401); `no-store`, `X-Auth-Email`-Leak prüfen - **Phase 5 — Registrierung (öffentlicher Endpunkt):** `ejun.register`-Action + öffentlicher `/odata/`-Pfad in Nginx (nur `verify` + `register` öffentlich, rest intern) + Formular auf `registrierung.html` + DSGVO-Text in `datenschutz.html` (Details im Abschnitt „Phase 5“ unten) **Phase 5 — Registrierung (öffentlicher Endpunkt) — Details (Stand 03.09.2026):** > Auslöser: `registrierung.html` ist aktuell ein toter Endpunkt (nur `mailto:` + WhatsApp-Button, Text „kein Formular“). Ziel: echte Selbst-Registrierung gegen das bestehende OData-Backend, ohne die Login-Mechanik anzufassen. **Backend (1 neue unbound Action):** - `ejun.register(Email, FirstName, LastName, Gemeinde, Phone)` in `ODataOperationImportController`: - E-Mail normalisieren (lowercase), Duplikat-Check → bei Treffer `409 Conflict` (bestehendes Mitglied bleibt unangetastet — kein PUT-Override) - Neuanlage: `Role = Unverifiziert`, `Status = Active`, generiertes Passwort (`PasswordHasher`), `RegisteredAt = jetzt`, `ConfirmedAt = null` - Rückgabe: `{ email, status, generatedPassword }` (generiertes PW nur Admin/sichtbar bis `resetPassword`) - EDM-Modell: `builder.Action("register")` + Parameter ergänzen (PascalCase-Namen = EDM-Parameter-Namen, sonst 500 NRE) **Nginx (öffentliches `/odata/` — heute fehlt):** - Aktuelle Config hat nur den internen `/authz_internal` (Subrequest). Neu: `location /odata/` → `proxy_pass http://127.0.0.1:8081/odata/` - **Nur `verify` + `register` öffentlich**, rest intern halten: entweder `location = /odata/verify` + `location = /odata/register` (öffentlich) und `location /odata/` (Default → `auth_request` oder 403), oder ein `map`/Whitelist im Proxy - Rate-Limit auf `/odata/register` (z. B. `limit_req` 5 req/min pro IP) gegen Spam-Registrierungen - `X-Auth-Email`-Leak + `no-store` wie Phase 4 **Frontend (`registrierung.html`):** - Formular im EjUn-Design (Creme/Peach/Grün, wie `login.html`): Vorname, Nachname, E-Mail, Gemeinde, Handy (optional), DSGVO-Checkbox - JS: `fetch('/odata/register', {method:'POST', body:{Email, FirstName, ...}})` → Erfolgs-Banner („Danke! Wir melden uns nach der Bestätigung“), Duplikat-Banner („Schon registriert — wir melden uns“) - Bestätigung bleibt **manuell über Simon** (`ejun.confirm` / Admin-Panel) — passt zum Bestätigungs-Lebenszyklus - `datenschutz.html`: „kein eigenes Formular“-Passus (Z. 54-55) anpassen → Daten fließen jetzt per Formular in `members.db` (Name, E-Mail, Gemeinde, Handy, Hash) **Abhängigkeiten:** - Phase 2 (Admin-Panel) ist der Komfort-Pfad — Registrierung läuft **ohne** Admin-Panel (Bestätigung geht auch per rohem `ejun.confirm`) - Neue Mitglieder sehen nach Bestätigung automatisch alle `Members`-Seiten (Zugriff = Rolle × Sichtbarkeit, kein Einzel-Grant) **Verifikation (Phase 5):** - `curl POST /odata/register` (neue E-Mail → 200, Duplikat → 409) - E2E: Formular abgeben → Mitglied in DB als `Unverifiziert` → `confirm` → sieht `Members`-Seite - Nginx: `/odata/register` öffentlich erreichbar, `/odata/Members` bleibt intern - Playwright: `registrierung.html` × 393/768/1024 px, Erfolgs-/Duplikat-Banner --- **Risiko:** - RAM knapp (~400 MB frei, kein Swap) → schlanker Service, MemoryMax - Fällt `ejun-oapi` aus → Members-Seiten 503 (systemd `Restart=on-failure`) - Soft-Delete: gelöschte Mitglieder bleiben als Zeile (Tiny-DB, kein Problem; `Email`-Unique-Key bleibt reserviert → Reaktivierung ohne Kollision) **Status: Umsetzung läuft (Phase 2 + 3 + 5 live, News-Gate live, 03.09.2026).** - Phase 1 (Backend): ✅ fertig & live - Phase 2 (Admin-Panel): ✅ fertig & live — `members.html` + `contents.html` + `admin.js` (Fetch-Creds-Fix: absolute API-Base + Auth-Header + sessionStorage), Browser-E2E 19/19 grün (Commit `e0b02f8`) - Phase 3 (Login-UX): ✅ fertig & live — E-Mail-Feld + `absolute_redirect off` (Gate-302 scheme-aware) + CRED-URL-Regex `https?:` (kein HTTP-Downgrade hinter OPNsense-TLS); Report-Gate war Rubber-Stamp (DB-Pfad `/reports/` → `/dev/`), jetzt 401/401/200 wie designed (Commit `7f3eecf`) - Phase 4 (E2E): ⚠️ grün, `no-store`/`X-Auth-Email`-Leak offen - Phase 5 (Registrierung): ✅ fertig & live — `ejun.register`-Action + öffentliches `/odata/register` + `registrierung.html`-Formular - **News-Gate (Details nur für Mitglieder):** ✅ live — die drei aktuellen News-Details (`uvp-hayforst`, `umzingelungsgutachten`, `entlastung-franken-neuhofen`) sind jetzt per UVP-Pattern geschützt: GATE 302 → `login.html?next=…-report.html`, REPORT `auth_request` + `Contents`-DB (Visibility=Mitglied); News-Teaser auf der Startseite bleiben public. Playwright-E2E 11/11 (Commit `7cf8b83`)
Simon changed title from Registrierungs-System: Mini SQL + OData V4 API to Mitglieder-Backend: SQLite + ejun-authd (LXC 10.20.10.10) 2026-09-01 19:30:35 +02:00
Simon changed title from Mitglieder-Backend: SQLite + ejun-authd (LXC 10.20.10.10) to Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) 2026-09-01 22:06:22 +02:00
Simon changed title from Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) to Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) — E-Mail als Login-ID 2026-09-01 22:14:06 +02:00
Simon changed title from Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) — E-Mail als Login-ID to Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) — E-Mail-Login, Enum-Status, Community-Flag 2026-09-01 22:29:02 +02:00
Simon changed title from Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) — E-Mail-Login, Enum-Status, Community-Flag to Mitglieder-Backend: OData V4 Service (.NET 8) + SQLite (LXC 10.20.10.10) — E-Mail-Login, Enum-Status, Members-Flag 2026-09-01 22:44:44 +02:00
Author
Owner

Update 03.09.2026 — Phase 5 „Registrierung“ ergänzt:

registrierung.html soll von der Mailto-Seite zu einer echten Selbst-Registrierung werden. Kernpunkte (Details im Issue-Body):

  • Neue unbound Action ejun.register(Email, FirstName, LastName, Gemeinde, Phone) → Unverifiziert/Active + generiertes Passwort; 409 bei Duplikat
  • Nginx bekommt einen öffentlichen /odata/-Pfad (heute nur internes /authz_internal); nur verify + register öffentlich, rest intern, Rate-Limit auf register
  • Formular im EjUn-Design auf registrierung.html + DSGVO-Text in datenschutz.html
  • Bestätigung bleibt manuell (Simon per ejun.confirm / Admin-Panel)

Phasen-Status oben im Issue aktualisiert: Phase 1 live, Phase 2 (Admin-Panel) ausstehend, Phase 3/4 teilweise, Phase 5 geplant.

**Update 03.09.2026 — Phase 5 „Registrierung“ ergänzt:** `registrierung.html` soll von der Mailto-Seite zu einer echten Selbst-Registrierung werden. Kernpunkte (Details im Issue-Body): - Neue unbound Action `ejun.register(Email, FirstName, LastName, Gemeinde, Phone)` → `Unverifiziert`/`Active` + generiertes Passwort; `409` bei Duplikat - Nginx bekommt einen öffentlichen `/odata/`-Pfad (heute nur internes `/authz_internal`); nur `verify` + `register` öffentlich, rest intern, Rate-Limit auf `register` - Formular im EjUn-Design auf `registrierung.html` + DSGVO-Text in `datenschutz.html` - Bestätigung bleibt manuell (Simon per `ejun.confirm` / Admin-Panel) Phasen-Status oben im Issue aktualisiert: Phase 1 live, Phase 2 (Admin-Panel) ausstehend, Phase 3/4 teilweise, Phase 5 geplant.
Author
Owner

Phase 5 Registrierung - umgesetzt und live auf dev

Commits: c4da4fc + 9baa6b6 (dev)

Was drin ist:

  1. Backend - neue unbound Action ejun.register(Email, FirstName, LastName, Gemeinde, Phone). Legt Unverifiziert/Active mit generiertem 12-stelligen Einmalpasswort an. HTTP-Semantik: 200 created / 409 exists / 400 invalid-email. E-Mail case-insensitiv, kein PUT-Override.
  2. Nginx - offentlich wird jetzt NUR /odata/register (Zone ejunreg 5/min) und /odata/verify (Zone ejunodata 2/s). Alles andere unter /odata/ (Members, Contents, confirm, setStatus, resetPassword) faellt auf 404 - kein Admin-Verbum nach aussen.
  3. registrierung.html - Mailto-Seite ersetzt durch echtes Formular im EjUn-Design (Creme, Peach/Gruen-Blobs, Barlow). Erfolgs-, Duplikat-, Fehler- und Rate-Limit-Banner. E-Mail + WhatsApp bleiben als Sekundaerweg.
  4. datenschutz.html - "kein Formular"-Passus durch "Registrierungsformular" ersetzt (Name, E-Mail, Gemeinde, Telefon, IP 28 Tage).

Live-E2E auf 10.20.10.10 (7/7 gruen):

  • register neu -> 200 created
  • register Duplikat -> 409 exists
  • register ungueltig -> 400 invalid-email
  • verify simon -> 200 valid (admin)
  • GET /odata/Members -> 404 (intern)
  • POST /odata/confirm -> 404 (intern)
  • Report-Gate ohne Auth -> 401 (Zwei-Wechsel-Login intakt)

Test-Mitglieder aus der DB aufgeraeumt (simon@berleb.net als Seed bleibt).

Noch offen: Phase 2 Admin-Panel, Phase 3 Login-UX (http-Hardcode + GATE-302 scheme-aware), Phase 4 no-store / X-Auth-Email-Leak. Produktion-Deploy (/var/www/ejun-page) erst mit dem naechen Release.

Phase 5 Registrierung - umgesetzt und live auf dev Commits: c4da4fc + 9baa6b6 (dev) Was drin ist: 1. Backend - neue unbound Action ejun.register(Email, FirstName, LastName, Gemeinde, Phone). Legt Unverifiziert/Active mit generiertem 12-stelligen Einmalpasswort an. HTTP-Semantik: 200 created / 409 exists / 400 invalid-email. E-Mail case-insensitiv, kein PUT-Override. 2. Nginx - offentlich wird jetzt NUR /odata/register (Zone ejunreg 5/min) und /odata/verify (Zone ejunodata 2/s). Alles andere unter /odata/ (Members, Contents, confirm, setStatus, resetPassword) faellt auf 404 - kein Admin-Verbum nach aussen. 3. registrierung.html - Mailto-Seite ersetzt durch echtes Formular im EjUn-Design (Creme, Peach/Gruen-Blobs, Barlow). Erfolgs-, Duplikat-, Fehler- und Rate-Limit-Banner. E-Mail + WhatsApp bleiben als Sekundaerweg. 4. datenschutz.html - "kein Formular"-Passus durch "Registrierungsformular" ersetzt (Name, E-Mail, Gemeinde, Telefon, IP 28 Tage). Live-E2E auf 10.20.10.10 (7/7 gruen): - register neu -> 200 created - register Duplikat -> 409 exists - register ungueltig -> 400 invalid-email - verify simon -> 200 valid (admin) - GET /odata/Members -> 404 (intern) - POST /odata/confirm -> 404 (intern) - Report-Gate ohne Auth -> 401 (Zwei-Wechsel-Login intakt) Test-Mitglieder aus der DB aufgeraeumt (simon@berleb.net als Seed bleibt). Noch offen: Phase 2 Admin-Panel, Phase 3 Login-UX (http-Hardcode + GATE-302 scheme-aware), Phase 4 no-store / X-Auth-Email-Leak. Produktion-Deploy (/var/www/ejun-page) erst mit dem naechen Release.
Author
Owner

Fortschritt Phase 2 + 5 (03.09.2026)

Phase 2 — Admin-Panel: ✅ live & E2E-grün (19/19)

  • admin/members.html + admin/contents.html + admin.js im EjUn-Design
  • Kern-Bug gefixt: relative fetch('/dev/adminapi/...') erbt die user:pass@-Creds aus location.href → Fetch-Spec wirft „Request cannot be constructed from a URL that includes credentials". Fix: absolute API-Base (location.origin) + expliziter Authorization: Basic-Header (aus sessionStorage, Fallback location.username) + HTML-Erkennung in parse().
  • Nginx: Gate = /dev/admin/ → Login-Card; Panel + API via auth_request (Admin+Active); @admin403_dev → login.html?fail=2
  • Browser-E2E (e2e-admin-browser.mjs, Playwright/Chromium): Gate → Login → Liste → create → confirm → resetPassword → setStatus → PUT → Visibility-Toggle → DELETE → Tab-Wechsel → falsches PW (fail=1) → Nicht-Admin (fail=2) → 0 Console-Fehler
  • Commit e0b02f8 auf dev, VM dev-e0b02f8 live

Phase 5 — Registrierung: ✅ live

  • ejun.register-Action (unbound, PascalCase-Parameter), 409 bei Duplikat
  • Nginx: /odata/register + /odata/verify öffentlich, rest intern
  • registrierung.html-Formular im EjUn-Design, Erfolgs-/Duplikat-Banner

Offen (Phase 3 + 4): scheme-aware-GATE (X-Forwarded-Proto), no-store-Cache-Control, X-Auth-Email-Leak prüfen.

## Fortschritt Phase 2 + 5 (03.09.2026) **Phase 2 — Admin-Panel: ✅ live & E2E-grün (19/19)** - `admin/members.html` + `admin/contents.html` + `admin.js` im EjUn-Design - **Kern-Bug gefixt:** relative `fetch('/dev/adminapi/...')` erbt die `user:pass@`-Creds aus `location.href` → Fetch-Spec wirft „Request cannot be constructed from a URL that includes credentials". Fix: absolute API-Base (`location.origin`) + expliziter `Authorization: Basic`-Header (aus `sessionStorage`, Fallback `location.username`) + HTML-Erkennung in `parse()`. - Nginx: Gate `= /dev/admin/` → Login-Card; Panel + API via `auth_request` (Admin+Active); `@admin403_dev` → `login.html?fail=2` - Browser-E2E (`e2e-admin-browser.mjs`, Playwright/Chromium): Gate → Login → Liste → create → confirm → resetPassword → setStatus → PUT → Visibility-Toggle → DELETE → Tab-Wechsel → falsches PW (fail=1) → Nicht-Admin (fail=2) → 0 Console-Fehler - Commit `e0b02f8` auf `dev`, VM `dev-e0b02f8` live **Phase 5 — Registrierung: ✅ live** - `ejun.register`-Action (unbound, PascalCase-Parameter), 409 bei Duplikat - Nginx: `/odata/register` + `/odata/verify` öffentlich, rest intern - `registrierung.html`-Formular im EjUn-Design, Erfolgs-/Duplikat-Banner **Offen (Phase 3 + 4):** scheme-aware-GATE (`X-Forwarded-Proto`), `no-store`-Cache-Control, `X-Auth-Email`-Leak prüfen.
Author
Owner

Phase 3 (Login-UX) live — 2026-09-03, Commit 7f3eecf.

  • Gate-302 scheme-aware: Nginx absolute_redirect off — alle 302s (Gate, fail=1/2, Admin) liefern relative Location: /dev/login.html?…; der Browser löst gegen die echte Client-Scheme auf. Hinter OPNsense-TLS kein HTTP-Downgrade mehr (vorher: http://appdev30.berleb.net/… trotz HTTPS-Eingang).
  • CRED-URL-Regex: Login-Card baut https?:// mit Backreference statt harter http:// — der user:pass@-Kick funktioniert auf beiden Schemes.
  • Report-Gate-Rubber-Stamp gefixt (Datafix): DB-Contents.Path stand auf /reports/uvp-hayforst.html, Nginx + Seed auf /dev/uvp-hayforst-report.html — unbekannter Pfad galt als public und lieferte immer 200. Eine Zeile konsistent gesetzt. Danach: ohne Auth 401 + WWW-Authenticate, falsch 401 → fail=1, richtig 200.
  • Verifikation: ad-hoc 9/9 (relative 302s TLS + LAN, /authz-Direktcheck) + Browser-E2E 19/19.

Offen in Phase 4: no-store-Cachepolicy + X-Auth-Email-Leak-Header.

**Phase 3 (Login-UX) live — 2026-09-03, Commit `7f3eecf`.** - **Gate-302 scheme-aware:** Nginx `absolute_redirect off` — alle 302s (Gate, `fail=1/2`, Admin) liefern relative `Location: /dev/login.html?…`; der Browser löst gegen die echte Client-Scheme auf. Hinter OPNsense-TLS kein HTTP-Downgrade mehr (vorher: `http://appdev30.berleb.net/…` trotz HTTPS-Eingang). - **CRED-URL-Regex:** Login-Card baut `https?://` mit Backreference statt harter `http://` — der `user:pass@`-Kick funktioniert auf beiden Schemes. - **Report-Gate-Rubber-Stamp gefixt (Datafix):** DB-`Contents.Path` stand auf `/reports/uvp-hayforst.html`, Nginx + Seed auf `/dev/uvp-hayforst-report.html` — unbekannter Pfad galt als public und lieferte immer 200. Eine Zeile konsistent gesetzt. Danach: ohne Auth **401** + `WWW-Authenticate`, falsch **401 → fail=1**, richtig **200**. - Verifikation: ad-hoc 9/9 (relative 302s TLS + LAN, `/authz`-Direktcheck) + Browser-E2E 19/19. Offen in Phase 4: `no-store`-Cachepolicy + `X-Auth-Email`-Leak-Header.
Author
Owner

Phase 5b (Registrierung-UX) live — 2026-09-03, Commits 85fdbc7 + fb067ff.

  • Submit-Button war unsichtbar: --accent-2 wurde im Button-Gradient referenziert, war aber auf der Seite nie definiert → die background-Declaration war invalid (CSS-Error Recovery) → transparenter Button, weißer Text. Fix: :root { --accent-2: #1c7c43; } (identisch zu login.html, members.html, contents.html). Dazu: Submit-SVG hatte kein width/height → fiel auf ~150 px Default-Box zurück und streckte den Button (256×154 → 256×52). .register-submit svg { width/height: 1.15rem; flex: 0 0 auto; }.
  • E-Mail zuerst + Fokus: rf-mail ist jetzt das erste Feld im Formular, autofocus beim Laden.
  • Zwei Alternativ-Buttons weg: .register-alt-Block (E-Mail + WhatsApp-Gruppe) + dazugehörige CSS entfernt — nur noch das Formular mit einem klaren Senden-Button.

Verifikation: Live-Browser-Check (Playwright/Chromium) auf dev-fb067ff — Button-Gradient + Weiß-Text, activeElement = rf-mail, Feldreihenfolge korrekt, Alt-Block weg, Screenshot geprüft.

**Phase 5b (Registrierung-UX) live — 2026-09-03, Commits `85fdbc7` + `fb067ff`.** - **Submit-Button war unsichtbar:** `--accent-2` wurde im Button-Gradient referenziert, war aber auf der Seite nie definiert → die `background`-Declaration war invalid (CSS-Error Recovery) → transparenter Button, weißer Text. Fix: `:root { --accent-2: #1c7c43; }` (identisch zu `login.html`, `members.html`, `contents.html`). Dazu: Submit-SVG hatte kein `width/height` → fiel auf ~150 px Default-Box zurück und streckte den Button (256×154 → 256×52). `.register-submit svg { width/height: 1.15rem; flex: 0 0 auto; }`. - **E-Mail zuerst + Fokus:** `rf-mail` ist jetzt das erste Feld im Formular, `autofocus` beim Laden. - **Zwei Alternativ-Buttons weg:** `.register-alt`-Block (E-Mail + WhatsApp-Gruppe) + dazugehörige CSS entfernt — nur noch das Formular mit einem klaren Senden-Button. Verifikation: Live-Browser-Check (Playwright/Chromium) auf `dev-fb067ff` — Button-Gradient + Weiß-Text, `activeElement = rf-mail`, Feldreihenfolge korrekt, Alt-Block weg, Screenshot geprüft.
Author
Owner

News-Gate: die drei aktuellen News-Details jetzt nur für Mitglieder

Auslöser: UVP-Bericht (13.08.) war schon geschützt, aber Umzingelungsgutachten (15.08.) und Stellungnahme II (10.08.) waren auf der Detailseite öffentlich — nur der Teaser auf der Startseite war der Einstieg.

Was jetzt live ist (dev):

News Detail-GATE REPORT (geschützt) Contents-Vis
UVP-Bericht (13.08.) /dev/uvp-hayforst.html /dev/uvp-hayforst-report.html Mitglied
Umzingelungsgutachten (15.08.) /dev/umzingelungsgutachten.html /dev/umzingelungsgutachten-report.html Mitglied
Stellungnahme II (10.08.) /dev/entlastung-franken-neuhofen.html /dev/entlastung-franken-neuhofen-report.html Mitglied

Mechanik: exakt das bewährte UVP-Pattern (Nginx, keine Backend-Änderung):

  • GATE: location = …html → 302 /dev/login.html?next=/dev/<name>-report.html + noindex/no-store
  • REPORT: location = …-report.html mit auth_request auf eigene Subrequests (/authz_news_umzingelung, /authz_news_entlastung), X-Authz-Path zeigt auf die …-report.html-Pfaden in der Contents-DB, alias auf die echte physikalische Datei, 401 → @report401 (Zwei-Wechsel)
  • DB: 2 neue Zeilen in Contents (Visibility=1/Mitglied)
  • news.js: keine Änderung — Teaser-Links zeigen auf den GATE-Pfad, der automatisch umleitet

Verifikation (live, 03.09.):

  • curl-Matrix: GATE 302 (alle drei), REPORT ohne Auth 302, falsch 401→fail=1, korrekt 200
  • Playwright 11/11: Teaser public → Gate → Login → Report rendert (Title korrekt), keine fail-Box
  • Bestehende Admin-E2E 19/19 (PASS jetzt per EJUN_PW-Umgebungsvariable, Commit 5c9c4e1)

Beobachtung am Rande: während der E2E-Läufe war das Seed-Passwort EjUn-Dev-7Q4mZx nicht mehr gültig (rotiert per ejun.resetPassword — Journal zeigt POSTs auf :8081/odata/resetPassword). E2E-Suite liest jetzt EJUN_PW aus der Umgebung.

Open: Produktion (buergerinitiative-ejun.de) hat keine der drei News geschützt (auch die UVP nicht) — folgt mit Release.

## News-Gate: die drei aktuellen News-Details jetzt nur für Mitglieder **Auslöser:** UVP-Bericht (13.08.) war schon geschützt, aber **Umzingelungsgutachten (15.08.)** und **Stellungnahme II (10.08.)** waren auf der Detailseite öffentlich — nur der Teaser auf der Startseite war der Einstieg. **Was jetzt live ist (dev):** | News | Detail-GATE | REPORT (geschützt) | Contents-Vis | |---|---|---|---| | UVP-Bericht (13.08.) | `/dev/uvp-hayforst.html` | `/dev/uvp-hayforst-report.html` | Mitglied | | Umzingelungsgutachten (15.08.) | `/dev/umzingelungsgutachten.html` | `/dev/umzingelungsgutachten-report.html` | Mitglied | | Stellungnahme II (10.08.) | `/dev/entlastung-franken-neuhofen.html` | `/dev/entlastung-franken-neuhofen-report.html` | Mitglied | **Mechanik:** exakt das bewährte UVP-Pattern (Nginx, keine Backend-Änderung): - **GATE:** `location = …html` → `302 /dev/login.html?next=/dev/<name>-report.html` + noindex/no-store - **REPORT:** `location = …-report.html` mit `auth_request` auf eigene Subrequests (`/authz_news_umzingelung`, `/authz_news_entlastung`), `X-Authz-Path` zeigt auf die `…-report.html`-Pfaden in der Contents-DB, `alias` auf die echte physikalische Datei, 401 → `@report401` (Zwei-Wechsel) - **DB:** 2 neue Zeilen in `Contents` (Visibility=1/Mitglied) - **news.js: keine Änderung** — Teaser-Links zeigen auf den GATE-Pfad, der automatisch umleitet **Verifikation (live, 03.09.):** - curl-Matrix: GATE 302 (alle drei), REPORT ohne Auth 302, falsch 401→fail=1, korrekt 200 - Playwright 11/11: Teaser public → Gate → Login → Report rendert (Title korrekt), keine fail-Box - Bestehende Admin-E2E **19/19** (PASS jetzt per `EJUN_PW`-Umgebungsvariable, Commit `5c9c4e1`) **Beobachtung am Rande:** während der E2E-Läufe war das Seed-Passwort `EjUn-Dev-7Q4mZx` nicht mehr gültig (rotiert per `ejun.resetPassword` — Journal zeigt POSTs auf `:8081/odata/resetPassword`). E2E-Suite liest jetzt `EJUN_PW` aus der Umgebung. **Open:** Produktion (`buergerinitiative-ejun.de`) hat **keine** der drei News geschützt (auch die UVP nicht) — folgt mit Release.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Simon/ejun-page#2
No description provided.