visual
All checks were successful
Deploy / deploy-staging (push) Successful in 25s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-08-14 01:24:24 +02:00
parent 97e5a2e2da
commit 6de12f164d
5 changed files with 201 additions and 18 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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();

View File

@@ -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<string, mixed> $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<string, mixed> $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<string, string> $fields
* @return array{success: bool, data?: array<string, mixed>, error?: string}

View File

@@ -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);
}