Skip to content

Evolutionary-Networking-Designs/EnterprisePortal

Repository files navigation

EnterprisePortal

Enterprise business portal built on Oqtane (Blazor) with .NET 10, Clean Architecture, MudBlazor UI, and full-stack service orchestration via .NET Aspire.

Overview

EnterprisePortal is a production-ready platform providing multi-tenant administration, centralized identity, job scheduling, and secure file storage — all behind a single Traefik ingress with automated TLS and mutual-TLS enforcement on admin routes.

Framework Oqtane 10.1.2 (Blazor)
Runtime .NET 10
UI MudBlazor 9.3.0 (Material Design)
Architecture Clean Architecture (Core → Domain → Application → Infrastructure)
Orchestration .NET Aspire (AppHost + ServiceDefaults)
Identity Zitadel (OIDC/OAuth2 — single provider for all services)
Ingress Traefik v3 (ports 80/443, pluggable TLS — Let's Encrypt / Smallstep CA / user-provided, mTLS admin routes)
Database PostgreSQL 16
Cache FusionCache (L1 in-memory + L2 Garnet)
Jobs Hangfire (PostgreSQL backend)
Storage Rustfs (S3-compatible)
PKI Smallstep CA (internal certificate authority)

Services Stack

Service Role Auth
Traefik Reverse proxy, sole ingress (80/443), Let's Encrypt DNS-01/HTTP-01, Smallstep CA internal PKI, or user-provided PEM cert; mTLS enforcement on admin routes
Zitadel OIDC identity provider — SSO for all services
PostgreSQL Primary relational database Internal
PgAdmin Database management UI OAuth2 via Zitadel
Rustfs S3-compatible object storage OIDC via Zitadel
Garnet Redis-compatible cache — FusionCache L2 distributed backend + Traefik dynamic routing Internal
FusionCache Two-level cache (L1 in-memory, L2 Garnet) — shared IMemoryCache with Oqtane Internal
Hangfire Background job scheduler, dashboard at /hangfire OIDC admin role via Portal
Smallstep CA Internal mTLS certificate authority, issues certs for admin routes OIDC provisioner via Zitadel

All services are internal-only. Traefik is the only container exposing host ports.


Security Architecture

mTLS for Admin Routes

Smallstep CA is the internal certificate authority. It issues client certificates used by Traefik to enforce mutual TLS on admin-facing routes.

  • Traefik tls.options: admin requires a valid client certificate signed by the Smallstep root CA
  • All admin routes (/hangfire, /pgadmin, /rustfs, /traefik, /aspire) use a dual-path pattern: mTLS client cert at priority 20 (for infra/automation), Zitadel SSO ForwardAuth at priority 10 (for browser admin access)
  • /zitadel has no mTLS or ForwardAuth — Zitadel self-manages its own auth (ForwardAuth would create a circular dependency)
  • HTTP is permanently redirected to HTTPS at the entry point level

ForwardAuth Middleware

Every admin route passes through a ForwardAuth middleware that calls Portal.Server /api/auth/validate. Requests are only forwarded if the caller has an authenticated session. Unauthenticated requests receive 401 and are redirected to the Zitadel login page.

Single Sign-On via Zitadel

Zitadel is the sole OIDC provider for the entire platform:

Service Integration
Portal OpenID Connect (cookie session)
PgAdmin OAuth2 auto-provisioning
Rustfs OIDC identity federation
Hangfire dashboard Role claim (admin) enforced by Portal middleware
Smallstep CA OIDC provisioner (device/service cert issuance)

First Run Setup Wizard

On first launch, the portal redirects all requests to /setup. The setup wizard is accessible at http://localhost:8080/setup and walks through every configuration step before the portal goes live.

Steps:

  1. Domain & URL — Set the public domain and base URL
  2. TLS Provider — Choose a certificate provider:
    • Let's Encrypt — DNS-01 or HTTP-01 challenge via a supported DNS provider; requires provider credentials in the next step
    • Smallstep CA — Internal PKI; the wizard initialises the CA inline; short-lived 30-day certs auto-renewed by Traefik
    • User-provided — Paste your own certificate and private key PEM (from an enterprise CA or self-signed); no ACME required
  3. DNS Credentials — Enter credentials for the selected DNS provider. This step only appears when Let's Encrypt is selected. Smallstep CA and user-provided both skip this step.
  4. Admin Account — Create the initial administrator (email, password, display name)
  5. Review & Apply — Confirm all settings; wizard provisions Zitadel OIDC, bootstraps Smallstep CA, and marks the portal as initialized. After setup completes, restart the app with dotnet run to activate the full stack (Traefik, HTTPS, OIDC, storage services).

Once complete, FirstRunMiddleware stops intercepting requests and the portal runs normally.


Let's Encrypt DNS-01 Providers

Applies when the TLS provider is set to letsencrypt during setup (or via the tls:provider config key).

During setup (or via --acme-resolver CLI arg), select your provider. Each provider also has a -staging variant for testing against the Let's Encrypt staging CA.

Provider --acme-resolver value Required credentials
AWS Route 53 letsencrypt-dns (default) AWS Access Key ID, Secret Access Key, Region
Cloudflare cloudflare DNS API Token
DigitalOcean digitalocean Auth Token
Azure DNS azure Client ID, Client Secret, Subscription ID, Tenant ID, Resource Group
GoDaddy godaddy API Key, API Secret

Append -staging to any resolver value (e.g. cloudflare-staging) to use the Let's Encrypt staging CA during testing.


Admin Pages

Page Path Purpose
System Health /admin/health Live status of TLS certificate expiry (all three TLS provider types), PostgreSQL, Zitadel, and Garnet. Auto-refreshes every 60 seconds.
Configuration /admin/config View and manage portal configuration key/value store
Security /admin/security Zitadel OIDC status, Smallstep CA status, TLS/ACME status
Background Jobs /admin/hangfire Link to Hangfire dashboard, job statistics
Backup & Recovery /admin/backup Trigger and monitor backups (config, PostgreSQL, Zitadel, encryption keys)

All admin pages require an authenticated session with the admin role.


Backup System

The portal performs automated daily backups and supports manual on-demand backups:

Automatic Daily Backups (Hangfire-scheduled)

  • Config Backupconfig-backup.json — 3:00 AM UTC
  • PostgreSQL Dumpbackups/postgres/ — 2:00 AM UTC (7-day retention)
  • Zitadel Exportbackups/zitadel/ — 2:30 AM UTC (7-day retention)
  • Encryption Keysbackups/encryption/ (includes secrets.json) — 1:00 AM UTC (30-day retention)

Manual Backup Trigger

Trigger backups on-demand via the admin API:

# Trigger a specific backup type
POST /api/admin/backup/trigger/{type}
  where {type} = config | postgres | zitadel | encryption | all

# Get backup status
GET /api/admin/backup/status

Admin Backup UI

The /admin/backup page provides per-type trigger buttons and status display for recent backup operations.


Getting Started

Prerequisites

Run

git clone https://github.com/Evolutionary-Networking-Designs/EnterprisePortal.git
cd EnterprisePortal
dotnet run --project src/Aspire/EnterprisePortal.AppHost

Then open http://localhost:8080/setup to complete first-run configuration.

Optional: pass config via CLI instead of the setup wizard

dotnet run --project src/Aspire/EnterprisePortal.AppHost \
  -- --acme-resolver cloudflare \
     --portal-base-url https://yourdomain.com \
     --acme-email admin@yourdomain.com

Optional CLI / config.db keys

Key Required Default Description
portal:base-url No http://portal:8080 Base URL the portal is reachable at (used for ForwardAuth callbacks). Override via --portal-base-url.
tls:provider No letsencrypt TLS certificate provider. Values: letsencrypt | smallstep | user-provided. Override via --tls-provider.
tls:acme-resolver No letsencrypt-dns ACME resolver to use. Override via --acme-resolver.
zitadel:auth-domain No Dedicated domain for Zitadel SSO (e.g. auth.example.com). When set, Traefik routes this domain to Zitadel via Host() rule instead of using /zitadel on the portal domain. Override via --zitadel-auth-domain.

Access Points

URL Service Phase
http://localhost:8080/setup First Run Setup Wizard Phase 1 (first run)
http://localhost:8080 Aspire dashboard (dev only — logs, traces, resources) Phase 1
https://yourdomain.com/ Portal (Oqtane) Phase 2 (after setup + restart)
https://yourdomain.com/admin/config Admin — Configuration Phase 2
https://yourdomain.com/admin/security Admin — Security status Phase 2
https://yourdomain.com/admin/hangfire Admin — Background jobs Phase 2
https://yourdomain.com/hangfire Hangfire dashboard (mTLS client cert OR Zitadel SSO (admin)) Phase 2
https://yourdomain.com/pgadmin PgAdmin (mTLS client cert OR Zitadel SSO (admin)) Phase 2
https://yourdomain.com/rustfs Rustfs object storage (mTLS client cert OR Zitadel SSO (admin)) Phase 2
https://yourdomain.com/traefik Traefik dashboard (mTLS client cert OR Zitadel SSO (admin)) Phase 2
https://yourdomain.com/zitadel Zitadel identity provider (Zitadel self-managed auth — no mTLS, no ForwardAuth; or https://auth.example.com if zitadel:auth-domain is set) Phase 2
https://yourdomain.com/aspire .NET Aspire dashboard (mTLS client cert OR Zitadel SSO (admin)) Phase 2

Project Structure

src/
├── Aspire/
│   ├── EnterprisePortal.AppHost/       # .NET Aspire orchestrator (entry point: dotnet run)
│   └── EnterprisePortal.ServiceDefaults/
├── Core/                               # Abstractions: Entity, AggregateRoot, repository interfaces
├── Domain/                             # Domain model: Tenant, User, Permission aggregates + events
├── Application/                        # CQRS: Commands, Queries, Handlers, Validators
├── Infrastructure/                     # EF Core, repositories, Hangfire, config store, caching (FusionCache L1+L2), setup services
├── Portal/
│   ├── Portal.Server/                  # Oqtane host — Blazor Server, middleware, auth, admin pages
│   ├── Portal.Client/                  # Blazor WebAssembly client — MudBlazor enterprise theme, layout
│   └── Portal.Shared/                  # Shared types (client/server)
└── Modules/
    └── EnterprisePortal.Module/        # Custom Oqtane module — Dashboard with admin summary cards

Branching Strategy

Branch Purpose
main Stable, production-ready. Only receives merges from dev after CI passes.
dev Integration branch. All feature branches merge here first.
feat/{issue}-{slug} Individual feature work. Branch from dev; PR back to dev.

Rules:

  • No direct commits to main.
  • Feature branches follow the pattern feat/{issue-number}-{kebab-slug} (e.g., feat/39-ci-coverage).
  • All PRs to dev and main must pass CI tests before merge.

Building & Testing

Build

dotnet build EnterprisePortal.slnx

Run Tests

dotnet test src/Domain.Tests/Domain.Tests.csproj
dotnet test src/Application.Tests/Application.Tests.csproj
dotnet test src/Infrastructure.Tests/Infrastructure.Tests.csproj

Run Tests with Coverage

dotnet test src/Domain.Tests/Domain.Tests.csproj --collect:"XPlat Code Coverage" --results-directory ./TestResults/Domain
dotnet test src/Application.Tests/Application.Tests.csproj --collect:"XPlat Code Coverage" --results-directory ./TestResults/Application
dotnet test src/Infrastructure.Tests/Infrastructure.Tests.csproj --collect:"XPlat Code Coverage" --results-directory ./TestResults/Infrastructure

Coverage reports (Cobertura XML) are written to ./TestResults/ and uploaded as CI artifacts on every run.

CI & Test Policy

Every push and PR to dev or main triggers the CI workflow (.github/workflows/ci.yml) which builds, runs all tests, and collects code coverage via XPlat Code Coverage. PRs that fail tests are blocked from merging. Coverage artifacts are retained for 30 days.

Current Test Suite: 98 tests passing

  • Domain: 37 tests
  • Application: 16 tests
  • Infrastructure: 45 tests

Test Run History

See docs/test-runs/test-run-log.md for a record of every test run and coverage over time. Coverage targets per component are tracked in docs/test-runs/coverage-targets.md.


Team & Governance

Role Agent Responsibilities
Lead Parzival Architecture, planning, milestone ownership
Frontend Art3mis Oqtane, MudBlazor, UI/UX, admin pages, setup wizard
Backend Aech .NET, CQRS, Infrastructure, Aspire orchestration
Tester Shoto Test strategy, quality, coverage
Docs Scribe Documentation, decisions, history
QA/Ops Ralph Release, deployment, ops

About

Enterprise Business Portal based on Oqtane

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors