JPRISE Blazor Server app with infrastructure landing page
Find a file
Edmond Gebara 911310e985 added: new button link to Matrix/Element in "Dienste"
added: new button link to Matrix/Element downloads in "Clients"
added: new group "Entwicklung" with link buttons to git.jprise.info and KNIME download
added: new group "Administration" with link button to oauth.jprise.info (Zitadel)
2026-06-12 03:52:01 +02:00
deploy Initial commit: JPRISE Blazor Server app with infrastructure landing page" 2026-06-02 20:52:49 +02:00
JPRISE added: new button link to Matrix/Element in "Dienste" 2026-06-12 03:52:01 +02:00
.gitignore Initial commit: JPRISE Blazor Server app with infrastructure landing page" 2026-06-02 20:52:49 +02:00
CLAUDE.md added: new button link to Matrix/Element in "Dienste" 2026-06-12 03:52:01 +02:00
CLAUDE.SoulfulShack.md added: columns "Administrator", "AdministratorStellvertreter" 2026-06-11 10:45:25 +02:00
JPRISE.sln Initial commit: JPRISE Blazor Server app with infrastructure landing page" 2026-06-02 20:52:49 +02:00
README.md added: new button link to Matrix/Element in "Dienste" 2026-06-12 03:52:01 +02:00

JPRISE Landing Page

Blazor-Server-App, die die statische index.html auf jprise.info ersetzt und vor den Inhalt einen OIDC-Login (Zitadel auf oauth.jprise.info) schaltet.

Stack

  • .NET 10 / ASP.NET Core, Blazor Web App mit Server-Interaktivität
  • OIDC via Microsoft.AspNetCore.Authentication.OpenIdConnect (Authorization Code + PKCE, client_secret_basic)
  • Serilog (Console + tägliche rotierende Datei in Logs/jprise-YYYYMMDD.log)
  • Deployment: Windows IIS (In-Process) oder Linux/Kestrel, identischer Publish-Output

Landing-Page-Inhalt

Die Startseite (Components/Pages/Home.razor) besteht aus fünf Kachel-Sektionen. Externe Links öffnen in einem neuen Tab (target="_blank" rel="noopener noreferrer"), interne Seiten bleiben im selben Tab.

Sektion Kachel Ziel CTA
Dienste ownCloud https://ocis.jprise.info ÖFFNEN
Galène https://galene.jprise.info BETRETEN
Matrix-Chat https://chat.jprise.info BETRETEN
Clients RustDesk https://rustdesk.com/ HERUNTERLADEN
Syncthing https://syncthing.net/downloads/ HERUNTERLADEN
KeePassXC https://keepassxc.org/download/ HERUNTERLADEN
Element https://element.io/download HERUNTERLADEN
Infrastruktur jprise.de /infrastruktur/jprise.de ANZEIGEN
jprise.info /infrastruktur/jprise.info ANZEIGEN
Anwendungen /anwendungen VERWALTEN
Entwicklung Forgejo https://git.jprise.info ÖFFNEN
KNIME https://www.knime.com/downloads HERUNTERLADEN
Administration Zitadel https://oauth.jprise.info ÖFFNEN

Es gibt keinen .hero-Block mehr; den Abstand zur fixierten Kopfzeile (Wordmark + User-Chip) trägt die erste Sektion über main > .section:first-of-type in wwwroot/app.css. Das Fade-in-Stagger der Kacheln ist auf .service:nth-of-type(5) begrenzt — eine Sektion mit mehr als fünf Kacheln ließe die zusätzlichen ohne Verzögerung einblenden. Die einzelne Administration-Kachel läuft (Grid repeat(auto-fit, minmax(260px, 1fr))) über die volle Breite; bei Bedarf zentriert ein .services--single-Modifier sie auf normale Kachelbreite.

Erstmaliges Setup (Dev)

1. Client-Secret in user-secrets ablegen

Niemals in appsettings.json einchecken.

cd JPRISE
dotnet user-secrets set "Oidc:ClientSecret" "<SECRET-AUS-ZITADEL>"

Speicherort macOS: ~/.microsoft/usersecrets/1e65e766-ff77-4dd6-a525-0834bed98b8b/secrets.json.

2. Lokal starten

dotnet run --launch-profile https
  • HTTPS auf https://localhost:5001, HTTP auf http://localhost:5000.
  • Beim ersten Aufruf wird automatisch zur Zitadel-Anmeldung umgeleitet.
  • Nach erfolgreichem Login Redirect zurück nach /signin-oidc → Cookie wird gesetzt → Index-Seite ist sichtbar.

3. Logout testen

Button „Abmelden" oben links → POST /logout → SignOut Cookie + Redirect an Zitadel end_session_endpoint → Rück-Redirect nach /signout-callback-oidc/.

Zitadel-Konfiguration (Referenz)

Feld Wert
Authority / Issuer https://oauth.jprise.info
Discovery https://oauth.jprise.info/.well-known/openid-configuration
Project / App JPRISE / LandingPage
Client-ID 374059785240182787
Client-Type Web (Confidential)
Response Type Code
Grant Type Authorization Code
Auth Method Basic (client_secret_basic)
Scopes openid profile email

Redirect-URIs (müssen in Zitadel hinterlegt sein)

Login-Callback:

  • https://jprise.info/signin-oidc
  • https://localhost:5001/signin-oidc
  • http://localhost:5000/signin-oidc

Post-Logout-Redirect:

  • https://jprise.info/signout-callback-oidc
  • https://localhost:5001/signout-callback-oidc
  • http://localhost:5000/signout-callback-oidc

Build & Publish

Windows-Ziel (IIS auf vmd157055)

dotnet publish JPRISE/JPRISE.csproj \
    -c Release \
    -r win-x64 \
    --self-contained false \
    -o publish/win

Output: publish/win/ mit JPRISE.dll, web.config, wwwroot/, Konfigs.

Stolperstein (Publish von macOS aus): dotnet publish räumt das Zielverzeichnis nicht selbst auf — alte Artefakte (umbenannte DLLs, entfernte wwwroot-Dateien) bleiben liegen und können den Eindruck erwecken, der Build habe „nichts geändert". Wer das Verzeichnis vorher mit rm -rf publish/win löscht, läuft auf macOS → win-x64 aber in einen zweiten Fehler: die TransformWebConfig-Task bricht mit Could not find a part of the path '.../publish/win/web.config' ab, weil der Elternpfad zum Schreibzeitpunkt fehlt. Lösung — Verzeichnis nach dem Löschen wieder anlegen:

rm -rf publish/win && mkdir -p publish/win
dotnet publish JPRISE/JPRISE.csproj -c Release -r win-x64 --self-contained false -o publish/win

Robuster im csproj verankert (greift aus Terminal und Rider): ein BeforeTargets="PrepareForPublish"-Target, das RemoveDir + MakeDir auf $(PublishDir) ausführt. Nicht BeforeTargets="Publish" verwenden — das löscht das Verzeichnis zu spät und triggert exakt den TransformWebConfig-Fehler. Rider selbst hat keinen „Clean before publish"- Haken, der das Output-Verzeichnis leert (Build → Clean putzt nur bin/obj).

Linux/Kestrel-Ziel

dotnet publish JPRISE/JPRISE.csproj \
    -c Release \
    -r linux-x64 \
    --self-contained false \
    -o publish/linux

Auf dem Linux-Host genügt dotnet JPRISE.dll hinter einem Reverse Proxy (oder Kestrel direkt mit ASPNETCORE_URLS).

Deployment nach jprise.info (IIS)

  1. Client-Secret als System-Environment-Variable auf dem Windows-Server setzen. Doppel-Underscore (__) = ASP.NET-Core-Konfigurationspfad-Separator, mappt auf Oidc:ClientSecret.

    Variante A — PowerShell (als Administrator):

    [System.Environment]::SetEnvironmentVariable(
        "Oidc__ClientSecret",
        "<SECRET-AUS-ZITADEL>",
        [System.EnvironmentVariableTarget]::Machine)
    

    Variante B — cmd (elevated):

    setx Oidc__ClientSecret "<SECRET-AUS-ZITADEL>" /M
    

    Variante C — GUI: sysdm.cpl → Erweitert → Umgebungsvariablen → Systemvariablen → Neu.

    IIS sieht neue System-Env-Vars erst nach einem Neustart des Application Pools (oder iisreset):

    Import-Module WebAdministration
    Restart-WebAppPool -Name "jprise.info"
    # oder hart:
    iisreset
    

    Die WebAdministration-Cmdlets (Get-Website, Restart-WebAppPool, …) und appcmd lesen die zentrale applicationHost.config und brauchen eine als Administrator gestartete Shell — sonst „Die Konfigurationsdatei kann aufgrund unzureichender Berechtigungen nicht gelesen werden". In PowerShell 7 ist das Modul zusätzlich explizit zu laden: Import-Module WebAdministration -UseWindowsPowerShell. appcmd liegt nicht im PATH und wird in pwsh mit vollem Pfad aufgerufen: & "$env:windir\system32\inetsrv\appcmd.exe" list sites.

    Prüfen, dass die Variable gesetzt ist (neue Shell aufmachen):

    [System.Environment]::GetEnvironmentVariable("Oidc__ClientSecret", "Machine")
    
  2. App-Pool stoppen in IIS-Manager (Site jprise.info → Stopp).

  3. SFTP-Upload des publish/win/-Inhalts nach C:\inetpub\jprise.info\ (gleicher Pfad wie SoulfulShack). Mountainduck-Mount ist verfügbar unter:

    /Users/edmond/Library/CloudStorage/MountainDuck-vmd157055.contaboserver.netSFTP/C/inetpub/jprise.info/
    
  4. App-Pool starten. Erster Request triggert OIDC-Discovery → bei Erfolg wird zur Anmeldung weitergeleitet.

  5. Logs prüfen: C:\inetpub\jprise.info\Logs\jprise-YYYYMMDD.log (Serilog rolling file).

Alternative zum Pool-Stopp/-Start (Schritte 2 + 4): Eine app_offline.htm ins Content-Root C:\inetpub\jprise.info\ legen — das ASP.NET Core Module fährt die App beim Erscheinen der Datei kontrolliert herunter (vermeidet DLL-File-Locks während des Uploads) und beim Löschen wieder hoch. Das braucht keine IIS-Rechte, nur Schreibzugriff auf den Ordner (über den MountainDuck-Mount möglich) — also weder elevated Shell noch RDP:

"<h1>Wartung</h1>" | Set-Content "C:\inetpub\jprise.info\app_offline.htm"
# … publish/win/ hochladen …
Remove-Item   "C:\inetpub\jprise.info\app_offline.htm"

Troubleshooting

Symptom Ursache Lösung
unauthorized_client von Zitadel Client-Secret fehlt/falsch dotnet user-secrets list bzw. IIS-Env-Var prüfen
Endlos-Loop nach Login Cookie wird nicht akzeptiert (SameSite/Secure) HTTPS verwenden; Cookie SameSite=Lax, Secure=Always ist gesetzt
redirect_uri_mismatch URI nicht in Zitadel hinterlegt exakte URL (Schema + Host + Port + Pfad) eintragen
invalid_request / „requested redirect_uri is missing" lokale Callback-URL (localhost:5001) nicht in der LandingPage-Client-Config lokale Redirect-URIs in Zitadel nachtragen (siehe Abschnitt „Redirect-URIs")
502.5 / Worker startet nicht Hosting Bundle Version mismatch dotnet --info auf Server prüfen, ≥ 10.0.8 erforderlich
id_token fehlt beim Logout SaveTokens nicht aktiv bereits true gesetzt — Cookie nach Re-Login prüfen

Anwendungs-Verwaltung (/anwendungen)

Authentifizierte CRUD-Seite für das Firmen-Anwendungsverzeichnis (DevExpress DxGrid, SQLite-Backend).

  • Spalten: Typ, Anwendung, Hersteller, Link, Ansprechpartner, Melder, Administrator, Administrator (Stellvertreter).
  • Export: Über die Grid-Toolbar (Button „Export") in vier Formaten — XLSX, XLS, CSV, PDF. Der Download wird direkt im Browser ausgelöst. CSV exportiert mit EncodeExecutableContent = true (Schutz vor CSV-Injection, da Admin-Namen frei eingegeben werden).
  • Anlegen/Bearbeiten/Löschen über das Popup-Edit-Formular bzw. die Command-Spalte.

Schema-Änderung (neue Spalte) — Ablauf

Die Anwendung ruft beim Start db.Database.Migrate() auf; Migrationen werden also automatisch und idempotent beim ersten Request nach dem Deploy angewendet. Für eine neue Spalte:

  1. Property in Data/Anwendung.cs ergänzen (z. B. public string Administrator { get; set; } = string.Empty; — der Default sorgt für leeren String statt NULL).
  2. Migration generieren:
    dotnet ef migrations add AddAdministratorColumns --project JPRISE/JPRISE.csproj --output-dir Data/Migrations
    
  3. Generierte Migration prüfen — für SQLite mit bereits befüllter Tabelle muss jede AddColumn-Operation nullable: false, defaultValue: "" haben, sonst schlägt die Migration auf den Bestandszeilen fehl.
  4. Vor dem Deploy das rohe SQL gegenlesen (ändert nichts, gibt nur aus):
    dotnet ef migrations script --project JPRISE/JPRISE.csproj --idempotent
    
  5. Spalte an den UI-Stellen pflegen (Grid + Edit-Formular) — siehe CLAUDE.md, Abschnitt „Anwendungs-Verwaltung", für die drei Code-Orte, die zusammen gepflegt werden müssen.

Backup vor dem ersten echten Migrate: Migrate() ändert die Live-jprise.db in-place. Da deploy/deploy.sh ohne --delete rsynct, bleibt die DB zwar erhalten — aber zieh dir vor dem Deploy einmal die jprise.db über den MountainDuck-Mount, bevor eine Migration sie anfasst.

Architektur-Hinweise für spätere Erweiterungen

  • User-spezifische Sub-Pages: Eine neue Razor-Page unter Components/Pages/ anlegen; die globale FallbackPolicy = RequireAuthenticatedUser schützt sie automatisch. Für rollenbasierten Zugriff in Program.cs zusätzliche AddPolicy()-Calls plus [Authorize(Policy = "...")] auf der Komponente.
  • Claims-Mapping: Aktuell sind name, email, preferred_username als Display-Name-Fallback verdrahtet (siehe MainLayout.razor DisplayName). Wenn Zitadel zusätzliche Claims liefert (z. B. role, Custom-Claims), reicht es, MapInboundClaims=false + den vorhandenen TokenValidationParameters zu erweitern.
  • Backchannel-Logout: Zitadel unterstützt es; falls benötigt, kann in Program.cs ein zusätzlicher MapPost("/backchannel-logout") ergänzt werden.