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

This commit is contained in:
2026-06-08 02:45:36 +02:00
parent 6d398323ae
commit 1ae7b98378
14 changed files with 1036 additions and 4 deletions

View File

@@ -0,0 +1,116 @@
:root {
color-scheme: dark;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: #f8fafc;
background:
radial-gradient(circle at top, rgba(255, 222, 173, 0.28), transparent 28%),
linear-gradient(145deg, #101827 0%, #18253d 52%, #0f172a 100%);
}
.login-shell {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.login-panel {
width: min(480px, 100%);
padding: 28px;
border-radius: 28px;
background: rgba(15, 23, 42, 0.58);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.42);
backdrop-filter: blur(24px);
}
.login-panel-top {
margin-bottom: 24px;
}
.login-kicker {
margin: 0 0 12px;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #cbd5e1;
}
.login-panel h1 {
margin: 0 0 10px;
font-size: 36px;
line-height: 1.05;
}
.login-copy {
margin: 0;
color: #dbe5f1;
line-height: 1.5;
}
.login-actions {
display: grid;
gap: 12px;
margin-bottom: 24px;
}
.login-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 48px;
padding: 0 16px;
border-radius: 14px;
text-decoration: none;
font-weight: 700;
}
.login-button-primary {
color: #082032;
background: linear-gradient(180deg, #a7f3d0, #6ee7b7);
}
.login-button-secondary {
color: #f8fafc;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.login-button-disabled {
color: #cbd5e1;
background: rgba(255, 255, 255, 0.08);
border: 1px dashed rgba(255, 255, 255, 0.18);
}
.login-meta {
display: grid;
gap: 14px;
margin: 0;
}
.login-meta div {
display: grid;
gap: 4px;
}
.login-meta dt {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #94a3b8;
}
.login-meta dd {
margin: 0;
color: #e2e8f0;
word-break: break-word;
}

View File

@@ -45,6 +45,20 @@ if (payloadNode) {
return `<span class="app-icon-fallback${safeClassName}">${escapeHtml(app.icon || 'AP')}</span>`;
};
const updateSessionDisplay = () => {
const accountNode = document.querySelector('.tray-pill:last-of-type');
if (!accountNode) {
return;
}
const displayName = payload.session?.display_name;
if (typeof displayName === 'string' && displayName.trim() !== '') {
accountNode.textContent = displayName;
}
};
const buildTaskbarButtonContent = (record) => `
<span class="taskbar-app-button-badge">${buildAppIconMarkup(record.definition, 'taskbar-app-icon')}</span>
<span class="taskbar-app-button-label">${escapeHtml(record.title)}</span>
@@ -702,6 +716,8 @@ if (payloadNode) {
iconsRoot?.appendChild(icon);
});
updateSessionDisplay();
startButton?.setAttribute('aria-expanded', 'false');
startButton?.addEventListener('click', () => {
if (!startMenu || !startButton) {

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\KeycloakAuth;
session_start();
$projectRoot = dirname(__DIR__, 3);
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
$state = (string) ($_GET['state'] ?? '');
$expectedState = (string) ($_SESSION['keycloak_oauth_state'] ?? '');
$code = (string) ($_GET['code'] ?? '');
$title = 'Keycloak Callback';
$message = 'Anmeldung wird verarbeitet.';
$redirectTarget = '/';
if ($state === '' || $expectedState === '' || !hash_equals($expectedState, $state)) {
http_response_code(400);
$message = 'State-Pruefung fehlgeschlagen. Bitte den Login erneut starten.';
} elseif ($code === '') {
http_response_code(400);
$message = 'Kein Authorization Code von Keycloak erhalten.';
} else {
unset($_SESSION['keycloak_oauth_state']);
$tokenExchange = $auth->exchangeAuthorizationCode($code);
if (($tokenExchange['success'] ?? false) !== true) {
http_response_code(502);
$message = (string) ($tokenExchange['error'] ?? 'Token-Austausch mit Keycloak fehlgeschlagen.');
} else {
/** @var array<string, mixed> $tokenPayload */
$tokenPayload = $tokenExchange['data'];
$accessToken = (string) ($tokenPayload['access_token'] ?? '');
if ($accessToken === '') {
http_response_code(502);
$message = 'Keycloak hat kein Access Token geliefert.';
} else {
$userInfo = $auth->fetchUserInfo($accessToken);
if (($userInfo['success'] ?? false) !== true) {
http_response_code(502);
$message = (string) ($userInfo['error'] ?? 'Userinfo-Abruf fehlgeschlagen.');
} else {
/** @var array<string, mixed> $userPayload */
$userPayload = $userInfo['data'];
$auth->establishSession($tokenPayload, $userPayload);
header('Location: ' . $redirectTarget, true, 302);
exit;
}
}
}
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title, ENT_QUOTES) ?></title>
<link rel="stylesheet" href="/assets/auth/login.css">
</head>
<body>
<main class="login-shell">
<section class="login-panel">
<div class="login-panel-top">
<p class="login-kicker">Kusche.Berlin</p>
<h1><?= htmlspecialchars($title, ENT_QUOTES) ?></h1>
<p class="login-copy"><?= htmlspecialchars($message, ENT_QUOTES) ?></p>
</div>
<div class="login-actions">
<a class="login-button login-button-secondary" href="/auth/login">Zurueck zum Login</a>
</div>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\KeycloakAuth;
session_start();
$projectRoot = dirname(__DIR__, 3);
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
$loginUrl = $auth->loginUrl();
if ($loginUrl === null) {
header('Location: /auth/login');
exit;
}
header('Location: ' . $loginUrl, true, 302);
exit;

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\KeycloakAuth;
session_start();
$projectRoot = dirname(__DIR__, 3);
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
$previewUrl = $auth->allowDesktopPreview()
? '/?' . urlencode($auth->previewQueryKey()) . '=1'
: null;
$loginScreen = [
'branding' => $auth->branding(),
'login_url' => $auth->isConfigured() ? '/auth/keycloak' : null,
'preview_url' => $previewUrl,
'keycloak_ready' => $auth->isConfigured(),
'callback_url' => $auth->redirectUri(),
];
require $projectRoot . '/partials/auth/login_screen.php';

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\ConfigLoader;
use App\KeycloakAuth;
session_start();
$projectRoot = dirname(__DIR__, 3);
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
$logoutUrl = null;
if ($auth->isConfigured() && $auth->isAuthenticated()) {
$logoutUrl = $auth->logoutUrl();
}
$auth->logout();
if ($logoutUrl !== null) {
header('Location: ' . $logoutUrl, true, 302);
exit;
}
header('Location: /auth/login', true, 302);
exit;

View File

@@ -5,8 +5,35 @@ declare(strict_types=1);
require_once dirname(__DIR__) . '/src/App/bootstrap.php';
use App\App;
use App\ConfigLoader;
use App\KeycloakAuth;
$app = new App(dirname(__DIR__));
session_start();
$projectRoot = dirname(__DIR__);
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
$auth = new KeycloakAuth($keycloakConfig);
if (!$auth->shouldShowDesktop()) {
$previewUrl = null;
if ($auth->allowDesktopPreview()) {
$previewUrl = '/?' . urlencode($auth->previewQueryKey()) . '=1';
}
$loginScreen = [
'branding' => $auth->branding(),
'login_url' => $auth->isConfigured() ? '/auth/keycloak' : null,
'preview_url' => $previewUrl,
'keycloak_ready' => $auth->isConfigured(),
'callback_url' => $auth->redirectUri(),
];
require $projectRoot . '/partials/auth/login_screen.php';
exit;
}
$app = new App($projectRoot);
$desktopPayload = $app->desktopPayload();
require dirname(__DIR__) . '/partials/desktop/shell.php';
require $projectRoot . '/partials/desktop/shell.php';