Zentrale Anmeldung
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Central\Controllers;
|
||||
|
||||
use App\Modules\Identity\Services\AuthService;
|
||||
use App\Modules\Tenants\Services\TenantService;
|
||||
|
||||
class LandingController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TenantService $tenantService,
|
||||
private readonly AuthService $authService
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(): array
|
||||
{
|
||||
return [
|
||||
'view' => 'welcome',
|
||||
'data' => [
|
||||
'title' => 'Kaffeeliste SaaS',
|
||||
'tenantOverview' => $this->tenantService->adminOverview(),
|
||||
'centralLoginPreview' => $this->authService->centralLoginPreview(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class LoginController
|
||||
'data' => [
|
||||
'title' => 'Anmeldung',
|
||||
'providers' => $this->authService->availableProviders(),
|
||||
'loginPreview' => $this->authService->centralLoginPreview(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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<string, mixed>
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants\Controllers;
|
||||
|
||||
use App\Modules\Tenants\Services\TenantService;
|
||||
|
||||
class TenantConsoleController
|
||||
{
|
||||
public function __construct(private readonly TenantService $tenantService)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(): array
|
||||
{
|
||||
return [
|
||||
'view' => 'tenants.index',
|
||||
'data' => [
|
||||
'title' => 'Tenant Console',
|
||||
'overview' => $this->tenantService->adminOverview(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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<string, mixed>
|
||||
*/
|
||||
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<int, array<string, mixed>>
|
||||
*/
|
||||
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<int, array<string, mixed>>
|
||||
*/
|
||||
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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="hero__kicker">Identity</p>
|
||||
<h2 class="hero__title">Mandantenbezogener Login mit lokalem Fallback und OIDC-Zielbild.</h2>
|
||||
<p class="hero__lead">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<span class="badge">Tenant erkannt</span>
|
||||
<span class="badge">OIDC first</span>
|
||||
<span class="badge badge--solid">Fallback bereit</span>
|
||||
<section class="hero hero--split">
|
||||
<div class="hero__content">
|
||||
<div>
|
||||
<p class="hero__kicker">Mitglieder-Login</p>
|
||||
<h2 class="hero__title">Eine zentrale Anmeldung, die Mitglieder automatisch in den richtigen Tenant bringt.</h2>
|
||||
<p class="hero__lead">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero__actions">
|
||||
<a class="button" href="#mitglied-login">Mitglieder-Login starten</a>
|
||||
<a class="button button--ghost" href="/tenants">Admin Console ansehen</a>
|
||||
</div>
|
||||
<div class="hero__meta">
|
||||
<span class="badge">E-Mail first</span>
|
||||
<span class="badge">Tenant-Erkennung</span>
|
||||
<span class="badge badge--solid">Mehrfachzuordnung unterstuetzt</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero__aside">
|
||||
<article class="card metric metric--compact">
|
||||
<p class="metric__label">Ein Tenant erkannt</p>
|
||||
<div class="metric__value">1 Klick</div>
|
||||
<p class="muted">Bei genau einer Mitgliedschaft wird direkt in den passenden Tenant weitergeleitet.</p>
|
||||
</article>
|
||||
<article class="card metric metric--compact">
|
||||
<p class="metric__label">Mehrfachzuordnung</p>
|
||||
<div class="metric__value">{{ count($preview['multiple']['matches']) }} Tenants</div>
|
||||
<p class="muted">Die Auswahl erscheint nur dann, wenn fuer dieselbe Mail-Adresse mehrere Kontexte hinterlegt sind.</p>
|
||||
</article>
|
||||
<div class="callout">
|
||||
<strong>SSO und Passwort aus einem Einstieg</strong>
|
||||
Die zentrale Anmeldung entscheidet zuerst ueber den Tenant-Kontext. Danach folgen Passwort-Login, OIDC oder ein
|
||||
klarer Fallback fuer kleinere Tenants.
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="split">
|
||||
<section id="mitglied-login" class="split">
|
||||
<article class="form-panel">
|
||||
<p class="card__eyebrow">Lokaler Login</p>
|
||||
<h3>Mit Passwort anmelden</h3>
|
||||
<form class="form-grid" method="post" action="/login">
|
||||
<p class="card__eyebrow">Zentraler Einstieg</p>
|
||||
<h3>Mit E-Mail und Passwort anmelden</h3>
|
||||
<p class="muted">
|
||||
Die E-Mail-Adresse steuert zuerst die Tenant-Erkennung. Der Passwort- oder SSO-Schritt kommt danach im richtigen Kontext.
|
||||
</p>
|
||||
<form class="form-grid" method="post" action="/login" style="margin-top: 18px;">
|
||||
<div class="field">
|
||||
<label for="email">E-Mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="username" placeholder="mitglied@example.com">
|
||||
<label for="email">E-Mail-Adresse</label>
|
||||
<input id="email" name="email" type="email" autocomplete="username" placeholder="mitglied@unternehmen.de">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" placeholder="........">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" placeholder="••••••••">
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button type="submit">Mit Passwort anmelden</button>
|
||||
<button type="submit">Zentral anmelden</button>
|
||||
<a class="button button--ghost" href="/forgot-password">Passwort vergessen?</a>
|
||||
</div>
|
||||
</form>
|
||||
<div class="note" style="margin-top: 18px;">
|
||||
Zielbild: Eine zentrale Anmeldung fuer Mitglieder, ohne dass zuerst Tenant-Keys oder Subdomains bekannt sein muessen.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h3>Single Sign-on</h3>
|
||||
<p class="muted">
|
||||
Tenant-Admins hinterlegen je Mandant einen oder mehrere OIDC-Provider.
|
||||
So wird die bisherige AD-Naehe in ein flexibles SaaS-Modell ueberfuehrt.
|
||||
</p>
|
||||
<div class="stack" style="margin-top: 18px;">
|
||||
<a class="button" href="/auth/oidc/entra-default">Mit OIDC / Entra anmelden</a>
|
||||
<span class="badge">Provider: entra-default</span>
|
||||
<span class="badge">Tenant: demo</span>
|
||||
<p class="card__eyebrow">Was danach passiert</p>
|
||||
<h3>Automatische Weiterleitung oder Tenant-Auswahl.</h3>
|
||||
<div class="timeline timeline--tight" style="margin-top: 18px;">
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">Genau ein Treffer</p>
|
||||
<p class="timeline__meta">Die Plattform leitet direkt in den Tenant weiter und zeigt dort Passwort- oder SSO-Login an.</p>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">Mehrere Treffer</p>
|
||||
<p class="timeline__meta">Vor dem eigentlichen Login erscheint eine Tenant-Auswahl mit Namen, Domain und Login-Modell.</p>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">Kein Treffer</p>
|
||||
<p class="timeline__meta">Statt Sackgasse gibt es einen klaren Rueckweg zu Tenant-Admin, Einladung oder Support.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note" style="margin-top: 18px;">
|
||||
Lokaler Login bleibt wichtig fuer Initialsetup, Notfallbetrieb und kleinere Tenants ohne SSO.
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="grid grid--3" style="margin-top: 18px;">
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Preview: Einzelzuordnung</p>
|
||||
<h3>{{ $preview['single']['email'] }}</h3>
|
||||
<p class="muted">{{ $preview['single']['message'] }}</p>
|
||||
<div class="tenant-grid" style="margin-top: 18px;">
|
||||
@foreach ($preview['single']['matches'] as $tenant)
|
||||
<div class="tenant-row">
|
||||
<div class="tenant-row__meta">
|
||||
<p class="tenant-row__title">{{ $tenant['name'] }}</p>
|
||||
<p class="tenant-row__copy">{{ $tenant['domain'] }}</p>
|
||||
</div>
|
||||
<span class="status">Auto-Weiterleitung</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Preview: Mehrfachzuordnung</p>
|
||||
<h3>{{ $preview['multiple']['email'] }}</h3>
|
||||
<p class="muted">{{ $preview['multiple']['message'] }}</p>
|
||||
<div class="tenant-grid" style="margin-top: 18px;">
|
||||
@foreach ($preview['multiple']['matches'] as $tenant)
|
||||
<div class="tenant-row">
|
||||
<div class="tenant-row__meta">
|
||||
<p class="tenant-row__title">{{ $tenant['name'] }}</p>
|
||||
<p class="tenant-row__copy">{{ $tenant['domain'] }} - {{ $tenant['login_mode'] }}</p>
|
||||
</div>
|
||||
<span class="status status--warning">Auswahl</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Preview: Unbekannte Mail</p>
|
||||
<h3>{{ $preview['unknown']['email'] }}</h3>
|
||||
<p class="muted">{{ $preview['unknown']['message'] }}</p>
|
||||
<div class="callout" style="margin-top: 18px;">
|
||||
<strong>Keine Sackgasse fuer Mitglieder</strong>
|
||||
Die Anwendung kann stattdessen Tenant-Kontakt, Einladung oder den zentralen Supportpfad anzeigen.
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<span class="badge">Webspace-ready</span>
|
||||
<span class="badge badge--solid">Tenant aware</span>
|
||||
<span class="badge">Webspace-tauglich</span>
|
||||
<span class="badge badge--solid">Mandantenfaehig</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="app-nav" aria-label="Hauptnavigation">
|
||||
<a href="/">Landing</a>
|
||||
<a class="is-primary" href="/">Start</a>
|
||||
<a href="/login">Anmeldung</a>
|
||||
<a href="/tenants">Mandanten-Admin</a>
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
<a href="/members">Mitglieder</a>
|
||||
<a href="/ledger">Ledger</a>
|
||||
@@ -352,8 +459,6 @@
|
||||
<a href="/exports">Exports</a>
|
||||
<a href="/notifications">Notifications</a>
|
||||
<a href="/surveys">Surveys</a>
|
||||
<a href="/tenants">Tenants</a>
|
||||
<a href="/login">Login</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -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')
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="hero__kicker">Central admin</p>
|
||||
<h2 class="hero__title">Mandanten, Domains und SSO sauber zentral verwalten.</h2>
|
||||
<p class="hero__lead">
|
||||
Die SaaS-Version fuehrt eine echte Mandantenebene ein. So werden mehrere
|
||||
Teams, Standorte oder Fachbereiche in derselben Plattform betrieben, aber
|
||||
fachlich und organisatorisch getrennt.
|
||||
</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<span class="badge">Subdomain routing</span>
|
||||
<span class="badge">OIDC first</span>
|
||||
<span class="badge badge--solid">Tenant aware</span>
|
||||
<section class="hero hero--split">
|
||||
<div class="hero__content">
|
||||
<div>
|
||||
<p class="hero__kicker">Central admin console</p>
|
||||
<h2 class="hero__title">Alle Tenants, Mitgliedschaften und Login-Pfade in einer zentralen Admin-Uebersicht.</h2>
|
||||
<p class="hero__lead">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero__actions">
|
||||
<a class="button" href="/login">Mitglieder-Login testen</a>
|
||||
<a class="button button--ghost" href="/">Landingpage ansehen</a>
|
||||
</div>
|
||||
<div class="hero__meta">
|
||||
<span class="badge badge--solid">Tenant Portfolio</span>
|
||||
<span class="badge">Identity & Routing</span>
|
||||
<span class="badge">Mehrfachzuordnungen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero__aside">
|
||||
@foreach ($data['metrics'] as $metric)
|
||||
<article class="card metric metric--compact">
|
||||
<p class="metric__label">{{ $metric['label'] }}</p>
|
||||
<div class="metric__value">{{ $metric['value'] }}</div>
|
||||
<p class="muted">{{ $metric['detail'] }}</p>
|
||||
</article>
|
||||
@endforeach
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="grid grid--3">
|
||||
<article class="card metric">
|
||||
<p class="metric__label">Mandanten</p>
|
||||
<div class="metric__value">3</div>
|
||||
<p class="muted">Aktive Bereiche auf gemeinsamer Plattform.</p>
|
||||
</article>
|
||||
<article class="card metric">
|
||||
<p class="metric__label">Domains</p>
|
||||
<div class="metric__value">5</div>
|
||||
<p class="muted">Zentrale und tenantbezogene Hosts fuer Login und Betrieb.</p>
|
||||
</article>
|
||||
<article class="card metric">
|
||||
<p class="metric__label">Provider</p>
|
||||
<div class="metric__value">2</div>
|
||||
<p class="muted">OIDC-Provider plus lokaler Fallback.</p>
|
||||
</article>
|
||||
<section class="table-card">
|
||||
<div class="table-card__header">
|
||||
<div>
|
||||
<p class="card__eyebrow">Portfolio</p>
|
||||
<h3>Tenant-Portfolio mit Betriebs- und Login-Sicht</h3>
|
||||
</div>
|
||||
<span class="pill">Zentrale Steuerung</span>
|
||||
</div>
|
||||
<div class="table-card__body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mandant</th>
|
||||
<th>Domain</th>
|
||||
<th>Mitglieder</th>
|
||||
<th>Login-Modell</th>
|
||||
<th>SSO</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($data['tenants'] as $tenant)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $tenant['name'] }}</strong><br>
|
||||
<span class="muted">{{ $tenant['tenant_key'] }} - {{ $tenant['primary_contact'] }}</span>
|
||||
</td>
|
||||
<td>{{ $tenant['domain'] }}</td>
|
||||
<td>{{ $tenant['member_count'] }} Mitglieder / {{ $tenant['admin_count'] }} Admins</td>
|
||||
<td>{{ $tenant['login_mode'] }}</td>
|
||||
<td>{{ $tenant['oidc_provider_count'] }} Provider</td>
|
||||
<td>
|
||||
@if ($tenant['status'] === 'active')
|
||||
<span class="status">Aktiv</span>
|
||||
@else
|
||||
<span class="status status--warning">Sandbox</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="split" style="margin-top: 18px;">
|
||||
<article class="table-card">
|
||||
<div class="table-card__header">
|
||||
<div>
|
||||
<p class="card__eyebrow">Mandantenlandschaft</p>
|
||||
<h3>Aktive Tenants</h3>
|
||||
</div>
|
||||
<span class="pill">Central view</span>
|
||||
</div>
|
||||
<div class="table-card__body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mandant</th>
|
||||
<th>Tenant Key</th>
|
||||
<th>Domain</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Office Berlin</td>
|
||||
<td>berlin</td>
|
||||
<td>berlin.kaffeeliste.app</td>
|
||||
<td><span class="status">Aktiv</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Office Koeln</td>
|
||||
<td>koeln</td>
|
||||
<td>koeln.kaffeeliste.app</td>
|
||||
<td><span class="status">Aktiv</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shared Demo</td>
|
||||
<td>demo</td>
|
||||
<td>demo.kaffeeliste.app</td>
|
||||
<td><span class="status status--warning">Sandbox</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<article class="panel">
|
||||
<p class="card__eyebrow">Mehrfachzuordnungen</p>
|
||||
<h3>Wie die Plattform E-Mail-Adressen ueber mehrere Tenants hinweg behandelt.</h3>
|
||||
<div class="timeline" style="margin-top: 18px;">
|
||||
@foreach ($data['shared_access'] as $entry)
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">{{ $entry['email'] }}</p>
|
||||
<p class="timeline__meta">
|
||||
@if ($entry['tenants'] !== [])
|
||||
{{ implode(', ', $entry['tenants']) }}
|
||||
@else
|
||||
Keine Tenant-Zuordnung vorhanden
|
||||
@endif
|
||||
</p>
|
||||
<div class="toolbar" style="margin-top: 2px;">
|
||||
@if ($entry['status'] === 'einzeln')
|
||||
<span class="status">Direkte Weiterleitung</span>
|
||||
@elseif ($entry['status'] === 'mehrfach')
|
||||
<span class="status status--warning">Tenant-Auswahl</span>
|
||||
@else
|
||||
<span class="status status--danger">Einladung noetig</span>
|
||||
@endif
|
||||
</div>
|
||||
<p class="muted">{{ $entry['next_step'] }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h3>Zentrale Aufgaben</h3>
|
||||
<ul class="list-reset">
|
||||
<li><span class="status">Domain Setup</span> Host und Tenant Key fuer saubere Aufloesung.</li>
|
||||
<li><span class="status">Identity</span> OIDC-Provider pro Mandant hinterlegen.</li>
|
||||
<li><span class="status">Feature Flags</span> PayPal, Surveys oder Self-Service-Striche je Tenant steuern.</li>
|
||||
<p class="card__eyebrow">Zentrale Aufgaben</p>
|
||||
<h3>Prioritaeten fuer den Plattformbetrieb.</h3>
|
||||
<ul class="list-reset" style="margin-top: 18px;">
|
||||
@foreach ($data['operations'] as $operation)
|
||||
<li>
|
||||
<span class="status">{{ $operation['state'] }}</span>
|
||||
<strong style="display: block; margin-top: 8px;">{{ $operation['title'] }}</strong>
|
||||
<span class="muted">{{ $operation['detail'] }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<div class="note" style="margin-top: 18px;">
|
||||
Die Tenant Console ist bewusst nicht nur eine Tabelle: Sie verbindet Rollout, Login-Logik, Mehrfachzuordnungen
|
||||
und zentrale Operations-Signale zu einer konsistenten Admin-Oberflaeche.
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@@ -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')
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="hero__kicker">Coffee operations cloud</p>
|
||||
<h2 class="hero__title">Die neue SaaS-Zentrale fuer Kaffeeliste, Mitglieder und Buchungen.</h2>
|
||||
<p class="hero__lead">
|
||||
Diese Version stellt den Produktkern sichtbar in den Vordergrund: Mandanten, Login, Kontostaende,
|
||||
Striche, Einzahlungen, Hinweise und Auswertungen in einer klaren, webspace-tauglichen Oberflaeche.
|
||||
</p>
|
||||
<section class="hero hero--split">
|
||||
<div class="hero__content">
|
||||
<div>
|
||||
<p class="hero__kicker">Zentrale Plattform fuer Mitglieder und Verantwortliche</p>
|
||||
<h2 class="hero__title">Kaffeeliste verbindet zentrale Anmeldung, Tenant-Steuerung und den operativen Alltag in einer klaren SaaS-Oberflaeche.</h2>
|
||||
<p class="hero__lead">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero__actions">
|
||||
<a class="button" href="/dashboard">Zum Dashboard</a>
|
||||
<a class="button button--ghost" href="/login">Login pruefen</a>
|
||||
<a class="button" href="/login">Zentrale Anmeldung starten</a>
|
||||
<a class="button button--ghost" href="/tenants">Tenant Console ansehen</a>
|
||||
</div>
|
||||
<div class="hero__meta">
|
||||
<span class="badge badge--solid">Ein Login fuer alle Mitgliedschaften</span>
|
||||
<span class="badge">Automatische Tenant-Weiterleitung</span>
|
||||
<span class="badge">Mehrfachzuordnung mit Auswahlfluss</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid--3">
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">MVP</p>
|
||||
<h3>Kernfunktionen bleiben erhalten</h3>
|
||||
<p class="muted">Mitglieder, Ledger, Einzahlungen, Striche und Hinweise sind fachlich sichtbar modelliert.</p>
|
||||
<aside class="hero__aside">
|
||||
<article class="card metric metric--compact">
|
||||
<p class="metric__label">{{ $overview['metrics'][0]['label'] }}</p>
|
||||
<div class="metric__value">{{ $overview['metrics'][0]['value'] }}</div>
|
||||
<p class="muted">{{ $overview['metrics'][0]['detail'] }}</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">SaaS</p>
|
||||
<h3>Mandantenfaehig gedacht</h3>
|
||||
<p class="muted">Jede Organisation bekommt ihren eigenen Bereich, eigene Rollen und eigene Inhalte.</p>
|
||||
<article class="card metric metric--compact">
|
||||
<p class="metric__label">{{ $overview['metrics'][1]['label'] }}</p>
|
||||
<div class="metric__value">{{ $overview['metrics'][1]['value'] }}</div>
|
||||
<p class="muted">{{ $overview['metrics'][1]['detail'] }}</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Hosting</p>
|
||||
<h3>Webspace und Cron zuerst</h3>
|
||||
<p class="muted">Die Zielarchitektur bleibt leichtgewichtig und vermeidet dauerhaft laufende Worker.</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="callout">
|
||||
<strong>Zentraler Login statt Inseln</strong>
|
||||
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.
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="grid grid--2">
|
||||
<section class="grid grid--3">
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Fuer Mitglieder</p>
|
||||
<h3>Schneller Einstieg ohne Tenant-Raten</h3>
|
||||
<p class="muted">
|
||||
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.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Fuer Verantwortliche</p>
|
||||
<h3>Alle Tenants in einer Admin-Console</h3>
|
||||
<p class="muted">
|
||||
Domains, SSO-Abdeckung, Rollout-Status und Mehrfachzuordnungen lassen sich tenantuebergreifend steuern und
|
||||
priorisieren.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<p class="card__eyebrow">Fuer den Betrieb</p>
|
||||
<h3>Webspace-tauglich und tenantbewusst</h3>
|
||||
<p class="muted">
|
||||
Die Produktflaechen bleiben leichtgewichtig, dokumentiert und anschlussfaehig fuer Cron, Importe, Exporte und
|
||||
spaetere Identity-Ausbaustufen.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="split" style="margin-top: 18px;">
|
||||
<article class="panel">
|
||||
<h3>Was die Plattform abdeckt</h3>
|
||||
<ul class="list-reset">
|
||||
<li><span class="status">Dashboard</span> Kontostand, Monatswerte und letzte Aktionen.</li>
|
||||
<li><span class="status">Ledger</span> Einzahlungen, Verbrauch und Korrekturen in einer Sicht.</li>
|
||||
<li><span class="status">Members</span> Aktivitaet, Rollen und Mitgliedsstatus pro Mandant.</li>
|
||||
<li><span class="status">Operations</span> Importe, Exporte, Notifications und Surveys als Zusatzmodule.</li>
|
||||
<p class="card__eyebrow">Login-Fluss</p>
|
||||
<h3>So funktioniert die zentrale Anmeldung fuer Mitglieder.</h3>
|
||||
<div class="feature-list" style="margin-top: 18px;">
|
||||
<div class="feature-list__item">
|
||||
<div class="feature-list__badge">1</div>
|
||||
<div>
|
||||
<p class="feature-list__title">E-Mail zuerst</p>
|
||||
<p class="feature-list__copy">Die Plattform prueft zentral, in welchen Tenants die Mail-Adresse hinterlegt ist.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-list__item">
|
||||
<div class="feature-list__badge">2</div>
|
||||
<div>
|
||||
<p class="feature-list__title">Automatische Entscheidung</p>
|
||||
<p class="feature-list__copy">Bei genau einer Mitgliedschaft erfolgt die Weiterleitung direkt in den korrekten Tenant.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-list__item">
|
||||
<div class="feature-list__badge">3</div>
|
||||
<div>
|
||||
<p class="feature-list__title">Auswahl bei Mehrfachzuordnung</p>
|
||||
<p class="feature-list__copy">Bei mehreren Tenants erscheint zuerst eine Auswahl, damit Mitglieder bewusst den richtigen Kontext waehlen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<p class="card__eyebrow">Mehrfachzuordnung</p>
|
||||
<h3>Beispiel fuer eine zentrale Tenant-Auswahl.</h3>
|
||||
<p class="muted">
|
||||
Fuer <strong>{{ $preview['multiple']['email'] }}</strong> werden mehrere Mitgliedschaften erkannt. Statt Fehlleitungen
|
||||
oder separater Logins zeigt die Plattform alle erreichbaren Tenants in einer klaren Auswahl.
|
||||
</p>
|
||||
<div class="tenant-grid" style="margin-top: 18px;">
|
||||
@foreach ($preview['multiple']['matches'] as $tenant)
|
||||
<div class="tenant-row">
|
||||
<div class="tenant-row__meta">
|
||||
<p class="tenant-row__title">{{ $tenant['name'] }}</p>
|
||||
<p class="tenant-row__copy">{{ $tenant['domain'] }}</p>
|
||||
</div>
|
||||
<span class="status">Auswaehlbar</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="note" style="margin-top: 18px;">
|
||||
Genau diese Logik wird spaeter im produktiven Login verwendet: kein technischer Tenant-Schritt fuer Mitglieder,
|
||||
aber trotzdem ein sauberer Kontext fuer jede Organisation.
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="grid grid--2" style="margin-top: 18px;">
|
||||
<article class="panel">
|
||||
<p class="card__eyebrow">Endanwender-Nutzen</p>
|
||||
<h3>Was Mitglieder auf der Plattform erwarten duerfen.</h3>
|
||||
<ul class="list-reset" style="margin-top: 18px;">
|
||||
<li><span class="status">Kontostand</span> Ein klares Dashboard mit Verbrauch, Zahlungen und letzten Buchungen.</li>
|
||||
<li><span class="status">Hinweise</span> Tenantbezogene Inhalte, FAQ und operative Informationen an einem Ort.</li>
|
||||
<li><span class="status">Mitgliedschaften</span> Ein zentraler Einstieg auch fuer Personen mit mehreren Teams oder Standorten.</li>
|
||||
<li><span class="status">Self-Service</span> Direkte Wege zu Ledger, Zahlungen und spaeterem Passwort-/SSO-Fallback.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h3>Projektfokus</h3>
|
||||
<div class="timeline">
|
||||
<p class="card__eyebrow">Admin-Perspektive</p>
|
||||
<h3>Was die zentrale Tenant Console sichtbar macht.</h3>
|
||||
<div class="timeline timeline--tight" style="margin-top: 18px;">
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">1. Root modernisieren</p>
|
||||
<p class="timeline__meta">Die alte PHP-Oberflaeche wird fachlich in die SaaS-Struktur ueberfuehrt.</p>
|
||||
<p class="timeline__title">Tenant-Portfolio</p>
|
||||
<p class="timeline__meta">Mitgliederzahlen, Betriebsstatus und Login-Modell aller Mandanten in einer Sicht.</p>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">2. Nicht benoetigte Seiten entlasten</p>
|
||||
<p class="timeline__meta">Sonderlogik wandert in optionale Module oder faellt bewusst weg.</p>
|
||||
<p class="timeline__title">Identity-Steuerung</p>
|
||||
<p class="timeline__meta">SSO-Abdeckung, Fallback-Logins und tenantbezogene Einstiegspfade konsistent halten.</p>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<p class="timeline__title">3. Doku und Betrieb</p>
|
||||
<p class="timeline__meta">Installationsanleitung, Hosting-Hinweise und Migrationspfad bleiben nachvollziehbar.</p>
|
||||
<p class="timeline__title">Rollout & Betrieb</p>
|
||||
<p class="timeline__meta">Onboarding, Migrationen und operative To-dos nicht pro Tenant verstreut, sondern zentral priorisiert.</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -1,55 +1,53 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => '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' => [],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\ResolveTenant;
|
||||
|
||||
return [
|
||||
[
|
||||
'method' => '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' => [],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user