diff --git a/saas-app/app/Modules/Central/Controllers/LandingController.php b/saas-app/app/Modules/Central/Controllers/LandingController.php new file mode 100644 index 0000000..695122a --- /dev/null +++ b/saas-app/app/Modules/Central/Controllers/LandingController.php @@ -0,0 +1,29 @@ + 'welcome', + 'data' => [ + 'title' => 'Kaffeeliste SaaS', + 'tenantOverview' => $this->tenantService->adminOverview(), + 'centralLoginPreview' => $this->authService->centralLoginPreview(), + ], + ]; + } +} diff --git a/saas-app/app/Modules/Identity/Controllers/LoginController.php b/saas-app/app/Modules/Identity/Controllers/LoginController.php index c4c4bb0..1a5e7cd 100644 --- a/saas-app/app/Modules/Identity/Controllers/LoginController.php +++ b/saas-app/app/Modules/Identity/Controllers/LoginController.php @@ -19,6 +19,7 @@ class LoginController 'data' => [ 'title' => 'Anmeldung', 'providers' => $this->authService->availableProviders(), + 'loginPreview' => $this->authService->centralLoginPreview(), ], ]; } diff --git a/saas-app/app/Modules/Identity/Services/AuthService.php b/saas-app/app/Modules/Identity/Services/AuthService.php index a05517c..372ca73 100644 --- a/saas-app/app/Modules/Identity/Services/AuthService.php +++ b/saas-app/app/Modules/Identity/Services/AuthService.php @@ -4,13 +4,19 @@ declare(strict_types=1); namespace App\Modules\Identity\Services; +use App\Modules\Tenants\Services\TenantService; + class AuthService { + public function __construct(private readonly TenantService $tenantService) + { + } + public function availableProviders(): array { return [ 'password' => [ - 'label' => 'Lokaler Login', + 'label' => 'Zentraler Passwort-Login', 'type' => 'password', ], 'oidc' => [ @@ -20,11 +26,64 @@ class AuthService ]; } + /** + * @return array + */ + public function centralLoginPreview(): array + { + $singleEmail = 'mia@berlin.example'; + $multipleEmail = 'leitung@kaffeeliste.example'; + $unknownEmail = 'extern@example.org'; + + return [ + 'single' => [ + 'email' => $singleEmail, + 'status' => 'redirect-tenant', + 'matches' => $this->tenantService->lookupTenantsByEmail($singleEmail), + 'message' => 'Bei genau einem Treffer wird direkt in den zugeordneten Tenant weitergeleitet.', + ], + 'multiple' => [ + 'email' => $multipleEmail, + 'status' => 'select-tenant', + 'matches' => $this->tenantService->lookupTenantsByEmail($multipleEmail), + 'message' => 'Bei mehreren Trefferkonten erscheint zuerst eine Tenant-Auswahl fuer denselben Mitarbeiter.', + ], + 'unknown' => [ + 'email' => $unknownEmail, + 'status' => 'request-tenant', + 'matches' => [], + 'message' => 'Unbekannte Mail-Adressen werden nicht ins Leere geleitet, sondern klar auf Tenant- oder Admin-Kontakt gefuehrt.', + ], + ]; + } + public function attemptLogin(array $payload): array { + $email = strtolower(trim((string) ($payload['email'] ?? ''))); + $matches = $this->tenantService->lookupTenantsByEmail($email); + + if (count($matches) === 1) { + $tenant = $matches[0]; + + return [ + 'status' => 'redirect-tenant', + 'email' => $email, + 'tenant' => $tenant, + 'redirect_to' => 'https://' . $tenant['domain'] . '/login', + ]; + } + + if (count($matches) > 1) { + return [ + 'status' => 'select-tenant', + 'email' => $email, + 'tenants' => $matches, + ]; + } + return [ - 'status' => 'not-implemented', - 'email' => $payload['email'] ?? null, + 'status' => 'request-tenant', + 'email' => $email !== '' ? $email : null, ]; } } diff --git a/saas-app/app/Modules/Tenants/Controllers/TenantConsoleController.php b/saas-app/app/Modules/Tenants/Controllers/TenantConsoleController.php new file mode 100644 index 0000000..efe421d --- /dev/null +++ b/saas-app/app/Modules/Tenants/Controllers/TenantConsoleController.php @@ -0,0 +1,25 @@ + 'tenants.index', + 'data' => [ + 'title' => 'Tenant Console', + 'overview' => $this->tenantService->adminOverview(), + ], + ]; + } +} diff --git a/saas-app/app/Modules/Tenants/Services/TenantService.php b/saas-app/app/Modules/Tenants/Services/TenantService.php index ef57c38..ba35d98 100644 --- a/saas-app/app/Modules/Tenants/Services/TenantService.php +++ b/saas-app/app/Modules/Tenants/Services/TenantService.php @@ -34,8 +34,178 @@ class TenantService */ public function listTenants(): array { + return array_map( + static fn (array $tenant): Tenant => new Tenant( + $tenant['id'], + $tenant['tenant_key'], + $tenant['name'], + $tenant['status'] + ), + $this->tenantPortfolio() + ); + } + + /** + * @return array + */ + public function adminOverview(): array + { + $portfolio = $this->tenantPortfolio(); + $activeCount = count(array_filter($portfolio, static fn (array $tenant): bool => $tenant['status'] === 'active')); + $memberCount = array_sum(array_map(static fn (array $tenant): int => $tenant['member_count'], $portfolio)); + $providerCount = array_sum(array_map(static fn (array $tenant): int => $tenant['oidc_provider_count'], $portfolio)); + return [ - new Tenant('tenant-placeholder', 'demo', 'Demo Tenant', 'active'), + 'metrics' => [ + [ + 'label' => 'Aktive Tenants', + 'value' => (string) $activeCount, + 'detail' => 'Mandanten mit aktivem Produktbetrieb und freigeschalteter Plattform.', + ], + [ + 'label' => 'Mitglieder gesamt', + 'value' => (string) $memberCount, + 'detail' => 'Alle aktiven Nutzerzuordnungen ueber die Mandantenlandschaft.', + ], + [ + 'label' => 'SSO-Abdeckung', + 'value' => (string) $providerCount, + 'detail' => 'Konfigurierte OIDC-Provider fuer zentrale oder tenantbezogene Anmeldung.', + ], + [ + 'label' => 'Betriebsstatus', + 'value' => 'Stabil', + 'detail' => 'Queues, Exporte und Benachrichtigungen sind fuer Cron-Betrieb vorbereitet.', + ], + ], + 'tenants' => $portfolio, + 'shared_access' => [ + [ + 'email' => 'leitung@kaffeeliste.example', + 'tenants' => ['Werk Berlin', 'Werk Koeln', 'Shared Services'], + 'next_step' => 'Tenant-Auswahl vor Passwort oder SSO anzeigen', + 'status' => 'mehrfach', + ], + [ + 'email' => 'mia@berlin.example', + 'tenants' => ['Werk Berlin'], + 'next_step' => 'Direkt an den Tenant-Login weiterleiten', + 'status' => 'einzeln', + ], + [ + 'email' => 'extern@example.org', + 'tenants' => [], + 'next_step' => 'Kontakt zum Tenant-Admin oder zentrale Einladung anbieten', + 'status' => 'unbekannt', + ], + ], + 'operations' => [ + [ + 'title' => 'Neuen Tenant live schalten', + 'detail' => 'Domain, Initial-Admin, Branding und Importpfad in einem zentralen Rollout steuern.', + 'state' => 'Bereit', + ], + [ + 'title' => 'Mitglieder zentral pruefen', + 'detail' => 'Mehrfachzuordnungen und offene Tenant-Einladungen vor Freischaltung abstimmen.', + 'state' => 'Im Fokus', + ], + [ + 'title' => 'SSO-Rollout konsolidieren', + 'detail' => 'Provider, Redirect-URIs und Fallback-Login tenantweise vereinheitlichen.', + 'state' => 'In Arbeit', + ], + ], + ]; + } + + /** + * @return array> + */ + public function lookupTenantsByEmail(string $email): array + { + $normalized = strtolower(trim($email)); + + $directory = [ + 'mia@berlin.example' => ['berlin'], + 'leitung@kaffeeliste.example' => ['berlin', 'koeln', 'shared-services'], + 'ops@kaffeeliste.example' => ['shared-services', 'finance-hub'], + ]; + + $matches = $directory[$normalized] ?? []; + $portfolio = []; + + foreach ($this->tenantPortfolio() as $tenant) { + if (in_array($tenant['tenant_key'], $matches, true)) { + $portfolio[] = $tenant; + } + } + + return $portfolio; + } + + /** + * @return array> + */ + public function tenantPortfolio(): array + { + return [ + [ + 'id' => 'tenant-berlin', + 'tenant_key' => 'berlin', + 'name' => 'Werk Berlin', + 'status' => 'active', + 'domain' => 'berlin.kaffeeliste.de', + 'member_count' => 42, + 'admin_count' => 4, + 'oidc_provider_count' => 1, + 'tenant_health' => 'stabil', + 'billing_state' => 'aktiv', + 'login_mode' => 'oidc-first', + 'primary_contact' => 'Office Operations Berlin', + ], + [ + 'id' => 'tenant-koeln', + 'tenant_key' => 'koeln', + 'name' => 'Werk Koeln', + 'status' => 'active', + 'domain' => 'koeln.kaffeeliste.de', + 'member_count' => 31, + 'admin_count' => 3, + 'oidc_provider_count' => 1, + 'tenant_health' => 'stabil', + 'billing_state' => 'aktiv', + 'login_mode' => 'oidc-first', + 'primary_contact' => 'Backoffice Koeln', + ], + [ + 'id' => 'tenant-shared-services', + 'tenant_key' => 'shared-services', + 'name' => 'Shared Services', + 'status' => 'active', + 'domain' => 'shared.kaffeeliste.de', + 'member_count' => 18, + 'admin_count' => 2, + 'oidc_provider_count' => 2, + 'tenant_health' => 'stabil', + 'billing_state' => 'aktiv', + 'login_mode' => 'central-routing', + 'primary_contact' => 'Platform Operations', + ], + [ + 'id' => 'tenant-finance-hub', + 'tenant_key' => 'finance-hub', + 'name' => 'Finance Hub', + 'status' => 'sandbox', + 'domain' => 'finance.kaffeeliste.de', + 'member_count' => 9, + 'admin_count' => 2, + 'oidc_provider_count' => 0, + 'tenant_health' => 'boarding', + 'billing_state' => 'test', + 'login_mode' => 'password-fallback', + 'primary_contact' => 'Finance Enablement', + ], ]; } } diff --git a/saas-app/resources/views/auth/login.blade.php b/saas-app/resources/views/auth/login.blade.php index c035467..4f328ab 100644 --- a/saas-app/resources/views/auth/login.blade.php +++ b/saas-app/resources/views/auth/login.blade.php @@ -1,58 +1,161 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Login') +@section('page_title', 'Kaffeeliste SaaS - Zentrale Anmeldung') + +@php + $preview = $loginPreview ?? [ + 'single' => [ + 'email' => 'mia@berlin.example', + 'message' => 'Direkte Weiterleitung in den zugeordneten Tenant.', + 'matches' => [['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de', 'login_mode' => 'oidc-first']], + ], + 'multiple' => [ + 'email' => 'leitung@kaffeeliste.example', + 'message' => 'Tenant-Auswahl vor Passwort oder SSO.', + 'matches' => [ + ['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de', 'login_mode' => 'oidc-first'], + ['name' => 'Werk Koeln', 'domain' => 'koeln.kaffeeliste.de', 'login_mode' => 'oidc-first'], + ['name' => 'Shared Services', 'domain' => 'shared.kaffeeliste.de', 'login_mode' => 'central-routing'], + ], + ], + 'unknown' => [ + 'email' => 'extern@example.org', + 'message' => 'Rueckfuehrung auf Tenant-Admin oder Einladungspfad.', + 'matches' => [], + ], + ]; +@endphp @section('content') -
-
-

Identity

-

Mandantenbezogener Login mit lokalem Fallback und OIDC-Zielbild.

-

- Die Legacy-Anbindung ueber `AUTH_USER` und LDAP wird in eine flexiblere - Identity-Schicht ueberfuehrt. SSO bleibt bevorzugt, lokaler Login und - Reset-Prozess sichern den Betrieb ab. -

-
-
- Tenant erkannt - OIDC first - Fallback bereit +
+
+
+

Mitglieder-Login

+

Eine zentrale Anmeldung, die Mitglieder automatisch in den richtigen Tenant bringt.

+

+ Statt verschiedene Tenant-URLs zu kennen, starten Mitglieder zentral mit ihrer E-Mail-Adresse. Die Plattform + erkennt Einzel- oder Mehrfachzuordnungen und fuehrt danach passend weiter: direkt in den Tenant, in eine + Tenant-Auswahl oder in einen klaren Kontaktpfad. +

+
+ +
+ E-Mail first + Tenant-Erkennung + Mehrfachzuordnung unterstuetzt +
+ +
-
+
-

Lokaler Login

-

Mit Passwort anmelden

-
+

Zentraler Einstieg

+

Mit E-Mail und Passwort anmelden

+

+ Die E-Mail-Adresse steuert zuerst die Tenant-Erkennung. Der Passwort- oder SSO-Schritt kommt danach im richtigen Kontext. +

+
- - + +
- +
- + Passwort vergessen?
+
+ Zielbild: Eine zentrale Anmeldung fuer Mitglieder, ohne dass zuerst Tenant-Keys oder Subdomains bekannt sein muessen. +
-

Single Sign-on

-

- Tenant-Admins hinterlegen je Mandant einen oder mehrere OIDC-Provider. - So wird die bisherige AD-Naehe in ein flexibles SaaS-Modell ueberfuehrt. -

-
- Mit OIDC / Entra anmelden - Provider: entra-default - Tenant: demo +

Was danach passiert

+

Automatische Weiterleitung oder Tenant-Auswahl.

+
+
+

Genau ein Treffer

+

Die Plattform leitet direkt in den Tenant weiter und zeigt dort Passwort- oder SSO-Login an.

+
+
+

Mehrere Treffer

+

Vor dem eigentlichen Login erscheint eine Tenant-Auswahl mit Namen, Domain und Login-Modell.

+
+
+

Kein Treffer

+

Statt Sackgasse gibt es einen klaren Rueckweg zu Tenant-Admin, Einladung oder Support.

+
-
- Lokaler Login bleibt wichtig fuer Initialsetup, Notfallbetrieb und kleinere Tenants ohne SSO. +
+
+ +
+
+

Preview: Einzelzuordnung

+

{{ $preview['single']['email'] }}

+

{{ $preview['single']['message'] }}

+
+ @foreach ($preview['single']['matches'] as $tenant) +
+
+

{{ $tenant['name'] }}

+

{{ $tenant['domain'] }}

+
+ Auto-Weiterleitung +
+ @endforeach +
+
+ +
+

Preview: Mehrfachzuordnung

+

{{ $preview['multiple']['email'] }}

+

{{ $preview['multiple']['message'] }}

+
+ @foreach ($preview['multiple']['matches'] as $tenant) +
+
+

{{ $tenant['name'] }}

+

{{ $tenant['domain'] }} - {{ $tenant['login_mode'] }}

+
+ Auswahl +
+ @endforeach +
+
+ +
+

Preview: Unbekannte Mail

+

{{ $preview['unknown']['email'] }}

+

{{ $preview['unknown']['message'] }}

+
+ Keine Sackgasse fuer Mitglieder + Die Anwendung kann stattdessen Tenant-Kontakt, Einladung oder den zentralen Supportpfad anzeigen.
diff --git a/saas-app/resources/views/layouts/app.blade.php b/saas-app/resources/views/layouts/app.blade.php index cc0442d..6a283e7 100644 --- a/saas-app/resources/views/layouts/app.blade.php +++ b/saas-app/resources/views/layouts/app.blade.php @@ -32,10 +32,15 @@ radial-gradient(circle at top left, rgba(180, 83, 9, 0.12), transparent 32%), radial-gradient(circle at top right, rgba(15, 118, 110, 0.14), transparent 28%), linear-gradient(180deg, #faf7f1 0%, var(--bg) 100%); - font-family: "Segoe UI", "Aptos", "Trebuchet MS", sans-serif; + font-family: "Aptos", "Segoe UI", "Trebuchet MS", sans-serif; line-height: 1.55; } + h1, h2, h3, h4, .hero__title, .brand__title { + font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; + letter-spacing: -0.02em; + } + a { color: var(--brand-strong); text-decoration: none; } a:hover { text-decoration: underline; } img { max-width: 100%; } @@ -114,6 +119,10 @@ font-size: 0.92rem; font-weight: 600; } + .app-nav a.is-primary { + background: linear-gradient(135deg, var(--brand) 0%, #115e59 100%); + color: #fff; + } .app-nav a:hover { text-decoration: none; border-color: rgba(15, 118, 110, 0.2); @@ -133,6 +142,15 @@ radial-gradient(circle at bottom left, rgba(15, 118, 110, 0.16), transparent 26%); box-shadow: var(--shadow); } + .hero--split { + grid-template-columns: minmax(0, 1.25fr) minmax(300px, 0.75fr); + align-items: stretch; + } + .hero__content, + .hero__aside { + display: grid; + gap: 18px; + } .hero__kicker { margin: 0 0 12px; text-transform: uppercase; @@ -154,6 +172,11 @@ font-size: 1.02rem; } .hero__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 4px; } + .hero__meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + } .button, button, input[type="submit"] { @@ -208,11 +231,91 @@ .list-reset { margin: 0; padding: 0; list-style: none; } .list-reset li + li { margin-top: 12px; } .stack { display: grid; gap: 14px; } + .feature-list { + display: grid; + gap: 12px; + } + .feature-list__item { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 14px 16px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(31, 41, 51, 0.08); + } + .feature-list__badge { + flex: 0 0 auto; + width: 34px; + height: 34px; + border-radius: 12px; + display: grid; + place-items: center; + background: rgba(15, 118, 110, 0.1); + color: var(--brand-strong); + font-weight: 800; + } + .feature-list__title { + margin: 0 0 4px; + font-weight: 700; + } + .feature-list__copy { + margin: 0; + color: var(--muted); + font-size: 0.94rem; + } .split { display: grid; gap: 18px; grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.9fr); } + .auth-summary__card { + padding: 16px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.75); + border: 1px solid rgba(31, 41, 51, 0.08); + } + .tenant-grid { + display: grid; + gap: 12px; + } + .tenant-row { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; + padding: 14px 16px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.82); + border: 1px solid rgba(31, 41, 51, 0.08); + } + .tenant-row__meta { + display: grid; + gap: 4px; + } + .tenant-row__title { + margin: 0; + font-weight: 700; + } + .tenant-row__copy { + margin: 0; + color: var(--muted); + font-size: 0.92rem; + } + .callout { + padding: 16px 18px; + border-radius: 18px; + background: rgba(15, 118, 110, 0.08); + border: 1px solid rgba(15, 118, 110, 0.12); + color: var(--brand-strong); + } + .callout strong { + display: block; + margin-bottom: 4px; + } + .metric--compact { + padding: 18px 20px; + } .toolbar { display: flex; flex-wrap: wrap; @@ -289,6 +392,7 @@ color: var(--brand-strong); } .timeline { display: grid; gap: 14px; } + .timeline--tight { gap: 10px; } .timeline__item { display: grid; gap: 6px; @@ -314,7 +418,8 @@ .grid--2, .grid--3, .grid--4, - .split { grid-template-columns: 1fr; } + .split, + .hero--split { grid-template-columns: 1fr; } .app-header__inner { flex-direction: column; align-items: flex-start; } .header-meta { justify-content: flex-start; } .hero, @@ -337,12 +442,14 @@
- Webspace-ready - Tenant aware + Webspace-tauglich + Mandantenfaehig
diff --git a/saas-app/resources/views/tenants/index.blade.php b/saas-app/resources/views/tenants/index.blade.php index 36122a1..33cfd81 100644 --- a/saas-app/resources/views/tenants/index.blade.php +++ b/saas-app/resources/views/tenants/index.blade.php @@ -1,93 +1,141 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Tenants') +@section('page_title', 'Kaffeeliste SaaS - Tenant Console') + +@php + $data = $overview ?? [ + 'metrics' => [], + 'tenants' => [], + 'shared_access' => [], + 'operations' => [], + ]; +@endphp @section('content') -
-
-

Central admin

-

Mandanten, Domains und SSO sauber zentral verwalten.

-

- Die SaaS-Version fuehrt eine echte Mandantenebene ein. So werden mehrere - Teams, Standorte oder Fachbereiche in derselben Plattform betrieben, aber - fachlich und organisatorisch getrennt. -

-
-
- Subdomain routing - OIDC first - Tenant aware +
+
+
+

Central admin console

+

Alle Tenants, Mitgliedschaften und Login-Pfade in einer zentralen Admin-Uebersicht.

+

+ Diese Konsole ist die zentrale Schaltstelle fuer Tenant-Rollout, Domains, zentrale Anmeldung und den Umgang + mit Mitarbeitenden, die in mehreren Tenants hinterlegt sind. Statt verteilter Einzelansichten entsteht eine + gemeinsame Portfolio-Sicht fuer Betrieb und Weiterentwicklung. +

+
+ +
+ Tenant Portfolio + Identity & Routing + Mehrfachzuordnungen +
+ +
-
-
-

Mandanten

-
3
-

Aktive Bereiche auf gemeinsamer Plattform.

-
-
-

Domains

-
5
-

Zentrale und tenantbezogene Hosts fuer Login und Betrieb.

-
-
-

Provider

-
2
-

OIDC-Provider plus lokaler Fallback.

-
+
+
+
+

Portfolio

+

Tenant-Portfolio mit Betriebs- und Login-Sicht

+
+ Zentrale Steuerung +
+
+ + + + + + + + + + + + + @foreach ($data['tenants'] as $tenant) + + + + + + + + + @endforeach + +
MandantDomainMitgliederLogin-ModellSSOStatus
+ {{ $tenant['name'] }}
+ {{ $tenant['tenant_key'] }} - {{ $tenant['primary_contact'] }} +
{{ $tenant['domain'] }}{{ $tenant['member_count'] }} Mitglieder / {{ $tenant['admin_count'] }} Admins{{ $tenant['login_mode'] }}{{ $tenant['oidc_provider_count'] }} Provider + @if ($tenant['status'] === 'active') + Aktiv + @else + Sandbox + @endif +
+
-
-
-
-

Mandantenlandschaft

-

Aktive Tenants

-
- Central view -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MandantTenant KeyDomainStatus
Office Berlinberlinberlin.kaffeeliste.appAktiv
Office Koelnkoelnkoeln.kaffeeliste.appAktiv
Shared Demodemodemo.kaffeeliste.appSandbox
+
+

Mehrfachzuordnungen

+

Wie die Plattform E-Mail-Adressen ueber mehrere Tenants hinweg behandelt.

+
+ @foreach ($data['shared_access'] as $entry) +
+

{{ $entry['email'] }}

+

+ @if ($entry['tenants'] !== []) + {{ implode(', ', $entry['tenants']) }} + @else + Keine Tenant-Zuordnung vorhanden + @endif +

+
+ @if ($entry['status'] === 'einzeln') + Direkte Weiterleitung + @elseif ($entry['status'] === 'mehrfach') + Tenant-Auswahl + @else + Einladung noetig + @endif +
+

{{ $entry['next_step'] }}

+
+ @endforeach
-

Zentrale Aufgaben

-
    -
  • Domain Setup Host und Tenant Key fuer saubere Aufloesung.
  • -
  • Identity OIDC-Provider pro Mandant hinterlegen.
  • -
  • Feature Flags PayPal, Surveys oder Self-Service-Striche je Tenant steuern.
  • +

    Zentrale Aufgaben

    +

    Prioritaeten fuer den Plattformbetrieb.

    +
      + @foreach ($data['operations'] as $operation) +
    • + {{ $operation['state'] }} + {{ $operation['title'] }} + {{ $operation['detail'] }} +
    • + @endforeach
    +
    + Die Tenant Console ist bewusst nicht nur eine Tabelle: Sie verbindet Rollout, Login-Logik, Mehrfachzuordnungen + und zentrale Operations-Signale zu einer konsistenten Admin-Oberflaeche. +
@endsection diff --git a/saas-app/resources/views/welcome.blade.php b/saas-app/resources/views/welcome.blade.php index 332e129..1f16802 100644 --- a/saas-app/resources/views/welcome.blade.php +++ b/saas-app/resources/views/welcome.blade.php @@ -1,65 +1,174 @@ @extends('layouts.app') -@section('page_title', 'Kaffeeliste SaaS - Landing') +@section('page_title', 'Kaffeeliste SaaS - Zentrale Plattform') + +@php + $overview = $tenantOverview ?? [ + 'metrics' => [ + ['label' => 'Aktive Tenants', 'value' => '4', 'detail' => 'Mandanten ueber die Plattform verteilt.'], + ['label' => 'Mitglieder gesamt', 'value' => '100', 'detail' => 'Aktive Nutzerkonten im Verbund.'], + ['label' => 'SSO-Abdeckung', 'value' => '4', 'detail' => 'Tenant-Logins mit zentraler Identitaetsstrategie.'], + ['label' => 'Betriebsstatus', 'value' => 'Stabil', 'detail' => 'Queues und Exporte fuer den Webspace-Betrieb vorbereitet.'], + ], + 'tenants' => [], + ]; + $preview = $centralLoginPreview ?? [ + 'single' => ['email' => 'mia@berlin.example', 'matches' => [['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de']]], + 'multiple' => ['email' => 'leitung@kaffeeliste.example', 'matches' => [['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de'], ['name' => 'Werk Koeln', 'domain' => 'koeln.kaffeeliste.de']]], + 'unknown' => ['email' => 'extern@example.org', 'matches' => []], + ]; +@endphp @section('content') -
-
-

Coffee operations cloud

-

Die neue SaaS-Zentrale fuer Kaffeeliste, Mitglieder und Buchungen.

-

- Diese Version stellt den Produktkern sichtbar in den Vordergrund: Mandanten, Login, Kontostaende, - Striche, Einzahlungen, Hinweise und Auswertungen in einer klaren, webspace-tauglichen Oberflaeche. -

+
+
+
+

Zentrale Plattform fuer Mitglieder und Verantwortliche

+

Kaffeeliste verbindet zentrale Anmeldung, Tenant-Steuerung und den operativen Alltag in einer klaren SaaS-Oberflaeche.

+

+ Endanwender finden ihren Kontostand, Striche, Einzahlungen und Hinweise ohne Tenant-Chaos. Gleichzeitig + behalten Verantwortliche alle Mandanten, Domains, SSO-Pfade und Mehrfachzuordnungen in einer gemeinsamen + Admin-Konsole im Blick. +

+
+
+ Ein Login fuer alle Mitgliedschaften + Automatische Tenant-Weiterleitung + Mehrfachzuordnung mit Auswahlfluss
-
-
-

MVP

-

Kernfunktionen bleiben erhalten

-

Mitglieder, Ledger, Einzahlungen, Striche und Hinweise sind fachlich sichtbar modelliert.

+
+
+ Zentraler Login statt Inseln + Mitglieder geben zuerst nur ihre E-Mail-Adresse an. Danach entscheidet die Plattform, ob direkt in einen Tenant + weitergeleitet wird oder ob zuerst eine Tenant-Auswahl erscheinen muss. +
+
-
+
+
+

Fuer Mitglieder

+

Schneller Einstieg ohne Tenant-Raten

+

+ Eine zentrale Anmeldung nimmt die Mail-Adresse entgegen und fuehrt danach automatisch in den passenden Bereich, + statt Nutzer auf Subdomains oder technische Tenant-Keys zu verweisen. +

+
+
+

Fuer Verantwortliche

+

Alle Tenants in einer Admin-Console

+

+ Domains, SSO-Abdeckung, Rollout-Status und Mehrfachzuordnungen lassen sich tenantuebergreifend steuern und + priorisieren. +

+
+
+

Fuer den Betrieb

+

Webspace-tauglich und tenantbewusst

+

+ Die Produktflaechen bleiben leichtgewichtig, dokumentiert und anschlussfaehig fuer Cron, Importe, Exporte und + spaetere Identity-Ausbaustufen. +

+
+
+ +
-

Was die Plattform abdeckt

-
    -
  • Dashboard Kontostand, Monatswerte und letzte Aktionen.
  • -
  • Ledger Einzahlungen, Verbrauch und Korrekturen in einer Sicht.
  • -
  • Members Aktivitaet, Rollen und Mitgliedsstatus pro Mandant.
  • -
  • Operations Importe, Exporte, Notifications und Surveys als Zusatzmodule.
  • +

    Login-Fluss

    +

    So funktioniert die zentrale Anmeldung fuer Mitglieder.

    +
    +
    +
    1
    +
    +

    E-Mail zuerst

    +

    Die Plattform prueft zentral, in welchen Tenants die Mail-Adresse hinterlegt ist.

    +
    +
    +
    +
    2
    +
    +

    Automatische Entscheidung

    +

    Bei genau einer Mitgliedschaft erfolgt die Weiterleitung direkt in den korrekten Tenant.

    +
    +
    +
    +
    3
    +
    +

    Auswahl bei Mehrfachzuordnung

    +

    Bei mehreren Tenants erscheint zuerst eine Auswahl, damit Mitglieder bewusst den richtigen Kontext waehlen.

    +
    +
    +
    +
+ +
+

Mehrfachzuordnung

+

Beispiel fuer eine zentrale Tenant-Auswahl.

+

+ Fuer {{ $preview['multiple']['email'] }} werden mehrere Mitgliedschaften erkannt. Statt Fehlleitungen + oder separater Logins zeigt die Plattform alle erreichbaren Tenants in einer klaren Auswahl. +

+
+ @foreach ($preview['multiple']['matches'] as $tenant) +
+
+

{{ $tenant['name'] }}

+

{{ $tenant['domain'] }}

+
+ Auswaehlbar +
+ @endforeach +
+
+ Genau diese Logik wird spaeter im produktiven Login verwendet: kein technischer Tenant-Schritt fuer Mitglieder, + aber trotzdem ein sauberer Kontext fuer jede Organisation. +
+
+
+ +
+
+

Endanwender-Nutzen

+

Was Mitglieder auf der Plattform erwarten duerfen.

+
    +
  • Kontostand Ein klares Dashboard mit Verbrauch, Zahlungen und letzten Buchungen.
  • +
  • Hinweise Tenantbezogene Inhalte, FAQ und operative Informationen an einem Ort.
  • +
  • Mitgliedschaften Ein zentraler Einstieg auch fuer Personen mit mehreren Teams oder Standorten.
  • +
  • Self-Service Direkte Wege zu Ledger, Zahlungen und spaeterem Passwort-/SSO-Fallback.
+
-

Projektfokus

-
+

Admin-Perspektive

+

Was die zentrale Tenant Console sichtbar macht.

+
-

1. Root modernisieren

-

Die alte PHP-Oberflaeche wird fachlich in die SaaS-Struktur ueberfuehrt.

+

Tenant-Portfolio

+

Mitgliederzahlen, Betriebsstatus und Login-Modell aller Mandanten in einer Sicht.

-

2. Nicht benoetigte Seiten entlasten

-

Sonderlogik wandert in optionale Module oder faellt bewusst weg.

+

Identity-Steuerung

+

SSO-Abdeckung, Fallback-Logins und tenantbezogene Einstiegspfade konsistent halten.

-

3. Doku und Betrieb

-

Installationsanleitung, Hosting-Hinweise und Migrationspfad bleiben nachvollziehbar.

+

Rollout & Betrieb

+

Onboarding, Migrationen und operative To-dos nicht pro Tenant verstreut, sondern zentral priorisiert.

diff --git a/saas-app/routes/auth.php b/saas-app/routes/auth.php index 57dc4b2..5a61746 100644 --- a/saas-app/routes/auth.php +++ b/saas-app/routes/auth.php @@ -1,55 +1,53 @@ 'GET', 'uri' => '/login', 'action' => 'App\\Modules\\Identity\\Controllers\\LoginController@show', 'name' => 'login', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], [ 'method' => 'POST', 'uri' => '/login', 'action' => 'App\\Modules\\Identity\\Controllers\\LoginController@authenticate', 'name' => 'login.attempt', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], [ 'method' => 'GET', 'uri' => '/forgot-password', 'action' => 'App\\Modules\\Identity\\Controllers\\ForgotPasswordController@show', 'name' => 'password.request', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], [ 'method' => 'POST', 'uri' => '/forgot-password', 'action' => 'App\\Modules\\Identity\\Controllers\\ForgotPasswordController@sendResetLink', 'name' => 'password.email', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], [ 'method' => 'GET', 'uri' => '/auth/oidc/providers', 'action' => 'App\\Modules\\Identity\\Controllers\\OidcController@providers', 'name' => 'auth.oidc.providers', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], [ 'method' => 'GET', 'uri' => '/auth/oidc/{provider}', 'action' => 'App\\Modules\\Identity\\Controllers\\OidcController@start', 'name' => 'auth.oidc.start', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], [ 'method' => 'GET', 'uri' => '/auth/oidc/{provider}/callback', 'action' => 'App\\Modules\\Identity\\Controllers\\OidcController@callback', 'name' => 'auth.oidc.callback', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], ], ]; diff --git a/saas-app/routes/tenants.php b/saas-app/routes/tenants.php index d4168ec..02c4203 100644 --- a/saas-app/routes/tenants.php +++ b/saas-app/routes/tenants.php @@ -1,13 +1,18 @@ 'GET', 'uri' => '/tenants', - 'action' => 'App\\Modules\\Tenants\\Services\\TenantService@listTenants', + 'action' => 'App\\Modules\\Tenants\\Controllers\\TenantConsoleController@index', 'name' => 'tenants.index', - 'middleware' => [ResolveTenant::class], + 'middleware' => [], + ], + [ + 'method' => 'GET', + 'uri' => '/admin/tenants', + 'action' => 'App\\Modules\\Tenants\\Controllers\\TenantConsoleController@index', + 'name' => 'admin.tenants.index', + 'middleware' => [], ], ]; diff --git a/saas-app/routes/web.php b/saas-app/routes/web.php index a71cf47..8dc2e54 100644 --- a/saas-app/routes/web.php +++ b/saas-app/routes/web.php @@ -6,17 +6,10 @@ return [ [ 'method' => 'GET', 'uri' => '/', - 'action' => 'LandingController@index', + 'action' => 'App\\Modules\\Central\\Controllers\\LandingController@index', 'name' => 'landing', 'middleware' => [], ], - [ - 'method' => 'GET', - 'uri' => '/login', - 'action' => 'Auth\\LoginController@show', - 'name' => 'login', - 'middleware' => [ResolveTenant::class], - ], [ 'method' => 'GET', 'uri' => '/dashboard',