From 6de12f164dbbf5d0c7f8c13bea233c9d57c07811 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 14 Aug 2026 01:24:24 +0200 Subject: [PATCH] visual --- custom/apps/mining-checker/assets/js/app.js | 10 ++ public/assets/desktop/desktop.css | 101 +++++++++++++++++--- public/index.php | 16 +++- src/App/KeycloakAuth.php | 87 +++++++++++++++++ src/ModulesCore/ModuleHttp.php | 5 +- 5 files changed, 201 insertions(+), 18 deletions(-) diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 2b00af57..26c128f3 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -473,6 +473,16 @@ source: requestUrl, }); } + if (response.status === 401) { + emitDebug({ + type: 'request:unauthorized', + method: requestOptions.method || 'GET', + url: requestUrl, + redirect_to: '/auth/keycloak', + }); + window.location.href = '/auth/keycloak'; + throw new Error('Nicht autorisiert. Weiterleitung zum Login.'); + } if (!response.ok) { const context = payload && payload.context && typeof payload.context === 'object' ? payload.context : null; const detail = context diff --git a/public/assets/desktop/desktop.css b/public/assets/desktop/desktop.css index e2b043c8..ce22777f 100644 --- a/public/assets/desktop/desktop.css +++ b/public/assets/desktop/desktop.css @@ -1581,7 +1581,8 @@ body .tray-pill.tray-pill-status--unconfigured { @media (max-width: 1100px) { .desktop-stage { min-height: 100vh; - padding-bottom: 120px; + padding-bottom: calc(var(--taskbar-height) + var(--taskbar-bottom) + 16px); + overflow-x: hidden; } .desktop-icons { @@ -1612,34 +1613,92 @@ body .tray-pill.tray-pill-status--unconfigured { .start-menu { position: fixed; - left: 16px; - right: 16px; - bottom: 88px; + left: 8px; + right: 8px; + top: 8px; + bottom: calc(var(--taskbar-height) + var(--taskbar-bottom) + 8px); transform: none; width: auto; + max-width: none; min-height: 0; + max-height: none; grid-template-columns: 1fr; + overflow: auto; + overscroll-behavior: contain; + box-sizing: border-box; } .start-menu-sidebar { border-right: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); + min-width: 0; + padding: 16px 12px 12px; + } + + .start-menu-selection-area { + min-width: 0; + padding: 16px 12px 20px; + } + + .start-menu-function-list, + .start-menu-selection-list, + .start-menu-group, + .start-menu-group-list { + min-width: 0; + } + + .start-menu-selection-list { + max-height: none; + overflow: visible; + padding-right: 0; + } + + .start-menu-entry { + grid-template-columns: 40px minmax(0, 1fr); + } + + .start-menu-entry-toggle { + display: none; } .window-layer { - position: static; - inset: auto; - margin-top: 24px; - display: grid; - gap: 18px; + position: fixed; + inset: 0 0 calc(var(--taskbar-height) + var(--taskbar-bottom) + 6px) 0; + margin-top: 0; + display: block; + z-index: 95; } .window { - position: relative; + position: absolute; + inset: 0; width: 100% !important; - height: auto !important; - left: auto !important; - top: auto !important; + height: 100% !important; + min-height: 0; + max-height: none; + left: 0 !important; + top: 0 !important; + border-radius: 0; + border: 0; + box-shadow: none; + } + + .window-header { + padding: 10px 12px; + } + + .window-header-spacer { + width: 48px; + flex-basis: 48px; + } + + .window-content, + .window-content--module, + .window-content--embedded, + .window-module-host, + .window-app-frame { + height: 100%; + min-height: 0; } .window-app-frame { @@ -1662,6 +1721,22 @@ body .tray-pill.tray-pill-status--unconfigured { grid-template-columns: 1fr; } + .taskbar { + left: 8px; + right: 8px; + bottom: 8px; + min-height: 56px; + padding: 8px 10px; + gap: 8px; + border-radius: 18px; + } + + .taskbar-apps { + gap: 8px; + min-width: 0; + overflow: auto; + } + .desktop-debug-toolbar { display: grid; grid-template-columns: 1fr; diff --git a/public/index.php b/public/index.php index 2cfeeb12..a9fbb5c3 100644 --- a/public/index.php +++ b/public/index.php @@ -17,8 +17,9 @@ $keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak'); $registrationConfig = ConfigLoader::load($projectRoot, 'registration'); $auth = new KeycloakAuth($keycloakConfig); $accountGate = new AccountGate($registrationConfig); +$hasAuthenticatedSession = $auth->ensureAuthenticatedSession(); -if (!$auth->isAuthenticated()) { +if (!$hasAuthenticatedSession) { $statusRedirect = AuthStatusRedirect::consume(); if ($statusRedirect !== null) { @@ -27,7 +28,7 @@ if (!$auth->isAuthenticated()) { } } -if ($auth->isAuthenticated()) { +if ($hasAuthenticatedSession) { $currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : []; $accountCheck = $accountGate->checkUsername((string) ($currentUser['username'] ?? '')); @@ -42,7 +43,7 @@ if ($auth->isAuthenticated()) { } if ( - !$auth->isAuthenticated() + !$hasAuthenticatedSession && $auth->isConfigured() && empty($_SESSION['desktop_silent_sso_attempted']) ) { @@ -51,6 +52,15 @@ if ( exit; } +if ( + !$hasAuthenticatedSession + && $auth->isConfigured() + && $auth->enforceLogin() +) { + header('Location: /auth/keycloak', true, 302); + exit; +} + $app = new App($projectRoot); $desktopPayload = $app->desktopPayload(); diff --git a/src/App/KeycloakAuth.php b/src/App/KeycloakAuth.php index be7bdf15..df71dd9d 100644 --- a/src/App/KeycloakAuth.php +++ b/src/App/KeycloakAuth.php @@ -47,6 +47,62 @@ final class KeycloakAuth return isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']); } + public function ensureAuthenticatedSession(): bool + { + if (!$this->isAuthenticated()) { + return false; + } + + if ($this->hasUsableAccessToken()) { + return true; + } + + if (!$this->hasUsableRefreshToken()) { + $this->logout(); + return false; + } + + $refreshToken = (string) ($_SESSION['desktop_auth']['refresh_token'] ?? ''); + if ($refreshToken === '') { + $this->logout(); + return false; + } + + $refreshResponse = $this->postForm( + $this->tokenEndpoint(), + [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'client_id' => $this->clientId(), + 'client_secret' => $this->clientSecret(), + ] + ); + + if (($refreshResponse['success'] ?? false) !== true) { + $this->logout(); + return false; + } + + /** @var array $tokenPayload */ + $tokenPayload = $refreshResponse['data']; + $accessToken = (string) ($tokenPayload['access_token'] ?? ''); + if ($accessToken === '') { + $this->logout(); + return false; + } + + $userInfo = $this->fetchUserInfo($accessToken); + if (($userInfo['success'] ?? false) !== true) { + $this->logout(); + return false; + } + + /** @var array $userPayload */ + $userPayload = $userInfo['data']; + $this->establishSession($tokenPayload, $userPayload); + return true; + } + public function shouldShowDesktop(): bool { if (!$this->enforceLogin()) { @@ -385,6 +441,37 @@ final class KeycloakAuth return is_array($claims) ? $claims : []; } + private function hasUsableAccessToken(): bool + { + $accessToken = (string) ($_SESSION['desktop_auth']['access_token'] ?? ''); + if ($accessToken === '') { + return false; + } + + return $this->secondsUntilSessionExpiry('expires_in') > 30; + } + + private function hasUsableRefreshToken(): bool + { + $refreshToken = (string) ($_SESSION['desktop_auth']['refresh_token'] ?? ''); + if ($refreshToken === '') { + return false; + } + + return $this->secondsUntilSessionExpiry('refresh_expires_in') > 30; + } + + private function secondsUntilSessionExpiry(string $ttlField): int + { + $authenticatedAt = (int) ($_SESSION['desktop_auth']['authenticated_at'] ?? 0); + $ttl = (int) ($_SESSION['desktop_auth'][$ttlField] ?? 0); + if ($authenticatedAt <= 0 || $ttl <= 0) { + return 0; + } + + return ($authenticatedAt + $ttl) - time(); + } + /** * @param array $fields * @return array{success: bool, data?: array, error?: string} diff --git a/src/ModulesCore/ModuleHttp.php b/src/ModulesCore/ModuleHttp.php index 1fff34f8..a841e427 100644 --- a/src/ModulesCore/ModuleHttp.php +++ b/src/ModulesCore/ModuleHttp.php @@ -14,8 +14,9 @@ final class ModuleHttp { $auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak')); $accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration')); + $hasAuthenticatedSession = $auth->ensureAuthenticatedSession(); - if ($auth->isAuthenticated()) { + if ($hasAuthenticatedSession) { $currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : []; $accountCheck = $accountGate->checkUsername((string) ($currentUser['username'] ?? '')); @@ -35,7 +36,7 @@ final class ModuleHttp } } - if (!$auth->shouldShowDesktop()) { + if (!$hasAuthenticatedSession && !$auth->shouldShowDesktop()) { if ($json) { self::respondJson(['error' => 'Nicht autorisiert.'], 401); }