From 1f9629f111e0ba1eb602c6a9eebf47dc01b19913 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Fri, 20 Feb 2026 18:15:37 +0100 Subject: [PATCH] feat(auth): add custom Keycloak login theme matching dashboard Branded Keycloak login page with CERTifAI dark theme: indigo/teal color palette, Inter + Space Grotesk fonts, animated background gradients, card glow, logo float, and button shimmer effects. Overrides PatternFly v4 defaults including autofill color fix. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 +- docker-compose.yml | 1 + keycloak/realm-export.json | 1 + .../certifai/login/resources/css/login.css | 583 ++++++++++++++++++ .../certifai/login/resources/img/logo.svg | 25 + .../themes/certifai/login/theme.properties | 3 + 6 files changed, 616 insertions(+), 1 deletion(-) create mode 100644 keycloak/themes/certifai/login/resources/css/login.css create mode 100644 keycloak/themes/certifai/login/resources/img/logo.svg create mode 100644 keycloak/themes/certifai/login/theme.properties diff --git a/.gitignore b/.gitignore index 3e7e175..75620fe 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,11 @@ # Logs *.log -# Keycloak runtime data (but keep realm-export.json) +# Keycloak runtime data (but keep config and theme) keycloak/* !keycloak/realm-export.json +!keycloak/themes/ +!keycloak/themes/** # Node modules node_modules/ diff --git a/docker-compose.yml b/docker-compose.yml index 28a4111..7194306 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - --import-realm volumes: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro + - ./keycloak/themes/certifai:/opt/keycloak/themes/certifai:ro healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"] interval: 10s diff --git a/keycloak/realm-export.json b/keycloak/realm-export.json index 4b1c52a..7e3aa42 100644 --- a/keycloak/realm-export.json +++ b/keycloak/realm-export.json @@ -9,6 +9,7 @@ "loginWithEmailAllowed": true, "duplicateEmailsAllowed": false, "resetPasswordAllowed": true, + "loginTheme": "certifai", "editUsernameAllowed": false, "bruteForceProtected": true, "permanentLockout": false, diff --git a/keycloak/themes/certifai/login/resources/css/login.css b/keycloak/themes/certifai/login/resources/css/login.css new file mode 100644 index 0000000..c8123a2 --- /dev/null +++ b/keycloak/themes/certifai/login/resources/css/login.css @@ -0,0 +1,583 @@ +/* CERTifAI Keycloak Login Theme + * Overrides PatternFly v4 / legacy Keycloak classes to match the dashboard. + * + * Actual page structure (Keycloak 26 with parent=keycloak): + * html.login-pf > body + * div.login-pf-page + * div#kc-header.login-pf-page-header + * div#kc-header-wrapper + * div.card-pf + * header.login-pf-header > h1#kc-page-title + * div#kc-content > div#kc-content-wrapper + * form#kc-form-login + * .form-group (email) + * .form-group (password + .pf-c-input-group) + * .form-group.login-pf-settings (forgot pwd) + * .form-group #kc-form-buttons (submit: input#kc-login.pf-c-button.pf-m-primary) + * div#kc-info.login-pf-signup (register link) + * + * Classes used: pf-c-* (PF v4), login-pf-*, card-pf, form-group + */ + +/* ===== Google Fonts ===== */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap'); + +/* ===== CSS Variables ===== */ +:root { + --cai-bg-body: #0f1116; + --cai-bg-card: #1a1d26; + --cai-bg-surface: #1e222d; + --cai-bg-input: #12141a; + --cai-text-primary: #e2e8f0; + --cai-text-heading: #f1f5f9; + --cai-text-muted: #8892a8; + --cai-text-faint: #5a6478; + --cai-border-primary: #1e222d; + --cai-border-secondary: #2a2f3d; + --cai-accent: #91a4d2; + --cai-accent-secondary: #6d85c6; + --cai-brand-indigo: #4B3FE0; + --cai-brand-teal: #38B2AC; + --cai-error: #f87171; + --cai-success: #4ade80; +} + +/* ===== Animations ===== */ + +/* Slow-moving ambient gradient behind the page */ +@keyframes ambientShift { + 0% { background-position: 0% 0%; } + 25% { background-position: 100% 50%; } + 50% { background-position: 50% 100%; } + 75% { background-position: 0% 50%; } + 100% { background-position: 0% 0%; } +} + +/* Subtle glow pulse on the card */ +@keyframes cardGlow { + 0%, 100% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 60px rgba(75, 63, 224, 0.04); } + 50% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 80px rgba(56, 178, 172, 0.06); } +} + +/* Gentle float for the logo */ +@keyframes logoFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} + +/* Gradient shimmer on the button */ +@keyframes buttonShimmer { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +/* ===== Base Page ===== */ +html.login-pf { + background-color: var(--cai-bg-body) !important; +} + +html.login-pf body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; + background: + radial-gradient(ellipse at 20% 20%, rgba(75, 63, 224, 0.07) 0%, transparent 50%), + radial-gradient(ellipse at 80% 80%, rgba(56, 178, 172, 0.05) 0%, transparent 50%), + radial-gradient(ellipse at 50% 50%, rgba(109, 133, 198, 0.03) 0%, transparent 70%), + var(--cai-bg-body) !important; + background-size: 200% 200%, 200% 200%, 100% 100%, 100% 100% !important; + animation: ambientShift 20s ease-in-out infinite !important; + color: var(--cai-text-primary) !important; + min-height: 100vh; +} + +/* ===== Page Layout ===== */ +.login-pf-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 40px 24px; + position: relative; +} + +/* ===== Header (Logo + Realm Name) ===== */ +#kc-header.login-pf-page-header { + background: transparent !important; + background-image: none !important; + padding: 0 0 32px !important; + text-align: center; + max-width: 440px; + width: 100%; + margin: 0; +} + +#kc-header-wrapper { + font-family: 'Space Grotesk', sans-serif !important; + font-size: 28px !important; + font-weight: 700 !important; + color: var(--cai-text-heading) !important; + letter-spacing: -0.02em; + text-transform: none !important; + padding: 0 !important; +} + +/* Logo via ::before pseudo-element */ +#kc-header-wrapper::before { + content: ''; + display: block; + width: 64px; + height: 64px; + margin: 0 auto 16px; + background-image: url('../img/logo.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + animation: logoFloat 4s ease-in-out infinite; + filter: drop-shadow(0 0 12px rgba(75, 63, 224, 0.3)); +} + +/* ===== Login Card ===== */ +.card-pf { + background-color: var(--cai-bg-card) !important; + border: 1px solid var(--cai-border-secondary) !important; + border-radius: 12px !important; + max-width: 440px; + width: 100%; + padding: 32px !important; + margin: 0 !important; + animation: cardGlow 6s ease-in-out infinite; + position: relative; + overflow: hidden; +} + +/* Subtle gradient border effect on the card via ::before overlay */ +.card-pf::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient( + 90deg, + transparent, + var(--cai-brand-indigo), + var(--cai-brand-teal), + var(--cai-accent-secondary), + transparent + ); + opacity: 0.5; +} + +/* ===== Card Header (Sign In Title) ===== */ +.login-pf-header { + border-bottom: none !important; + padding: 0 0 24px !important; + margin: 0 !important; +} + +#kc-page-title { + font-family: 'Space Grotesk', sans-serif !important; + font-size: 22px !important; + font-weight: 600 !important; + color: var(--cai-text-heading) !important; + text-align: center; + margin: 0 !important; +} + +/* ===== Form Groups ===== */ +.form-group { + margin-bottom: 20px !important; +} + +/* ===== Labels ===== */ +.pf-c-form__label, +.pf-c-form__label-text, +.login-pf-page .form-group label, +.card-pf label { + font-family: 'Inter', sans-serif !important; + font-size: 13px !important; + font-weight: 500 !important; + color: var(--cai-text-muted) !important; + margin-bottom: 6px !important; + display: block; +} + +/* ===== Text Inputs ===== */ +.pf-c-form-control, +.login-pf-page .form-control, +.card-pf input[type="text"], +.card-pf input[type="password"], +.card-pf input[type="email"] { + background-color: var(--cai-bg-input) !important; + border: 1px solid var(--cai-border-secondary) !important; + border-radius: 8px !important; + color: var(--cai-text-primary) !important; + font-family: 'Inter', sans-serif !important; + font-size: 14px !important; + padding: 10px 14px !important; + height: auto !important; + line-height: 1.5 !important; + transition: border-color 0.2s ease, box-shadow 0.2s ease !important; + box-shadow: none !important; + outline: none !important; +} + +.pf-c-form-control:focus, +.pf-c-form-control:focus-within, +.card-pf input[type="text"]:focus, +.card-pf input[type="password"]:focus, +.card-pf input[type="email"]:focus { + border-color: var(--cai-accent) !important; + box-shadow: 0 0 0 1px var(--cai-accent), 0 0 12px rgba(145, 164, 210, 0.1) !important; + outline: none !important; +} + +.pf-c-form-control::placeholder, +.card-pf input::placeholder { + color: var(--cai-text-faint) !important; +} + +/* Override browser autofill yellow background */ +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 9999px var(--cai-bg-input) inset !important; + -webkit-text-fill-color: var(--cai-text-primary) !important; + caret-color: var(--cai-text-primary) !important; + transition: background-color 5000s ease-in-out 0s !important; + background-color: var(--cai-bg-input) !important; + color: var(--cai-text-primary) !important; +} + +/* Firefox autofill override */ +input:autofill { + background-color: var(--cai-bg-input) !important; + color: var(--cai-text-primary) !important; + border-color: var(--cai-border-secondary) !important; +} + +/* Additional specificity for autofill inside input-group */ +.pf-c-input-group input:-webkit-autofill, +.card-pf input:-webkit-autofill, +.form-group input:-webkit-autofill, +#username:-webkit-autofill, +#password:-webkit-autofill { + -webkit-box-shadow: 0 0 0 9999px var(--cai-bg-input) inset !important; + -webkit-text-fill-color: var(--cai-text-primary) !important; + background-color: var(--cai-bg-input) !important; +} + +/* ===== Password Input Group ===== */ +/* FIX: The .pf-c-input-group has white bg from PF4, causing white corners + * behind the rounded child elements. Set transparent + matching border-radius. */ +.pf-c-input-group { + display: flex !important; + align-items: stretch !important; + background-color: transparent !important; + background: transparent !important; + border-radius: 8px !important; + overflow: hidden !important; +} + +.pf-c-input-group > .pf-c-form-control, +.pf-c-input-group > input.pf-c-form-control, +.pf-c-input-group > input[type="password"], +#password { + border-radius: 8px 0 0 8px !important; + border-right: none !important; + flex: 1; +} + +/* Password visibility toggle */ +.pf-c-button.pf-m-control, +.pf-c-input-group > .pf-c-button.pf-m-control { + background-color: var(--cai-bg-surface) !important; + color: var(--cai-text-muted) !important; + border-top: 1px solid var(--cai-border-secondary) !important; + border-right: 1px solid var(--cai-border-secondary) !important; + border-bottom: 1px solid var(--cai-border-secondary) !important; + border-left: 1px solid var(--cai-border-primary) !important; + border-radius: 0 8px 8px 0 !important; + padding: 0 14px !important; + transition: color 0.2s ease, background-color 0.2s ease !important; + line-height: 1 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.pf-c-button.pf-m-control:hover, +.pf-c-input-group > .pf-c-button.pf-m-control:hover { + color: var(--cai-accent) !important; + background-color: rgba(145, 164, 210, 0.08) !important; +} + +.pf-c-button.pf-m-control:focus, +.pf-c-input-group > .pf-c-button.pf-m-control:focus { + box-shadow: none !important; + outline: none !important; +} + +/* ===== Primary Button (Sign In) ===== */ +.pf-c-button.pf-m-primary, +input.pf-c-button.pf-m-primary, +#kc-login { + background: linear-gradient(135deg, + var(--cai-accent), + var(--cai-accent-secondary), + var(--cai-brand-indigo), + var(--cai-accent-secondary), + var(--cai-accent)) !important; + background-size: 300% 100% !important; + animation: buttonShimmer 6s ease-in-out infinite !important; + border: none !important; + border-radius: 8px !important; + color: #0a0c10 !important; + font-family: 'Inter', sans-serif !important; + font-size: 14px !important; + font-weight: 600 !important; + padding: 12px 20px !important; + cursor: pointer !important; + transition: opacity 0.15s ease, box-shadow 0.2s ease !important; + text-shadow: none !important; + box-shadow: 0 2px 12px rgba(109, 133, 198, 0.2) !important; + width: 100%; + text-align: center; +} + +.pf-c-button.pf-m-primary:hover, +input.pf-c-button.pf-m-primary:hover, +#kc-login:hover { + opacity: 0.95; + box-shadow: 0 4px 20px rgba(109, 133, 198, 0.35) !important; +} + +.pf-c-button.pf-m-primary:focus, +#kc-login:focus { + box-shadow: 0 0 0 2px var(--cai-accent), 0 4px 20px rgba(109, 133, 198, 0.3) !important; + outline: none !important; +} + +/* ===== Links ===== */ +.login-pf-page a, +.card-pf a { + color: var(--cai-accent) !important; + text-decoration: none !important; + transition: color 0.15s ease !important; +} + +.login-pf-page a:hover, +.card-pf a:hover { + color: var(--cai-accent-secondary) !important; + text-decoration: none !important; +} + +/* Forgot Password link */ +.login-pf-settings { + text-align: right; + margin-bottom: 24px !important; +} + +.login-pf-settings a { + font-size: 13px !important; +} + +/* ===== Registration / Info Section ===== */ +#kc-info.login-pf-signup { + background-color: var(--cai-bg-surface) !important; + border-top: 1px solid var(--cai-border-primary) !important; + padding: 16px 32px !important; + margin: 0 -32px -32px !important; + border-radius: 0 0 12px 12px !important; + text-align: center; +} + +#kc-info-wrapper, +#kc-registration { + font-size: 14px !important; + color: var(--cai-text-muted) !important; +} + +#kc-registration span { + color: var(--cai-text-muted) !important; +} + +/* ===== Alert / Error Messages ===== */ +.alert, +.pf-c-alert { + background-color: var(--cai-bg-surface) !important; + border: 1px solid var(--cai-border-secondary) !important; + border-radius: 8px !important; + color: var(--cai-text-primary) !important; + padding: 12px 16px !important; + margin-bottom: 16px !important; + font-size: 14px !important; +} + +.alert-error, +.alert-warning, +.pf-c-alert.pf-m-danger, +.pf-c-alert.pf-m-warning { + border-color: var(--cai-error) !important; +} + +.alert-error .kc-feedback-text, +.pf-c-alert .pf-c-alert__title { + color: var(--cai-text-primary) !important; +} + +.alert-success { + border-color: var(--cai-success) !important; +} + +/* ===== Checkboxes (Remember Me) ===== */ +.pf-c-check, +.login-pf-page .checkbox { + display: flex; + align-items: center; + gap: 8px; +} + +.pf-c-check__label, +.login-pf-page .checkbox label { + font-size: 13px !important; + color: var(--cai-text-muted) !important; + cursor: pointer; +} + +.pf-c-check__input, +.login-pf-page input[type="checkbox"] { + accent-color: var(--cai-accent); + width: 16px; + height: 16px; +} + +/* ===== Select / Dropdown ===== */ +.card-pf select, +.login-pf-page select { + background-color: var(--cai-bg-input) !important; + border: 1px solid var(--cai-border-secondary) !important; + border-radius: 8px !important; + color: var(--cai-text-primary) !important; + padding: 10px 14px !important; + font-family: 'Inter', sans-serif !important; + font-size: 14px !important; +} + +/* ===== Social Login / Identity Providers ===== */ +#kc-social-providers { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid var(--cai-border-primary); +} + +#kc-social-providers ul { + list-style: none; + padding: 0; + margin: 0; +} + +#kc-social-providers li { + margin-bottom: 8px; +} + +#kc-social-providers a, +#kc-social-providers .pf-c-button { + background-color: var(--cai-bg-surface) !important; + border: 1px solid var(--cai-border-secondary) !important; + border-radius: 8px !important; + color: var(--cai-text-primary) !important; + padding: 10px 16px !important; + display: block; + text-align: center; + font-size: 14px !important; + font-weight: 500 !important; + transition: border-color 0.15s ease !important; +} + +#kc-social-providers a:hover, +#kc-social-providers .pf-c-button:hover { + border-color: var(--cai-accent) !important; +} + +/* ===== Form Buttons Row ===== */ +#kc-form-buttons { + margin-top: 8px !important; +} + +#kc-form-options { + margin-bottom: 4px; +} + +/* ===== Tooltip ===== */ +.kc-tooltip-text { + background-color: var(--cai-bg-surface) !important; + color: var(--cai-text-primary) !important; + border: 1px solid var(--cai-border-secondary) !important; + border-radius: 8px !important; + font-size: 13px !important; +} + +/* ===== Scrollbar ===== */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: var(--cai-bg-body); +} + +::-webkit-scrollbar-thumb { + background: var(--cai-border-secondary); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--cai-text-faint); +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .login-pf-page { + padding: 24px 16px; + } + + .card-pf { + padding: 24px !important; + } + + #kc-header-wrapper { + font-size: 24px !important; + } + + #kc-header-wrapper::before { + width: 48px; + height: 48px; + } + + #kc-info.login-pf-signup { + margin: 0 -24px -24px !important; + padding: 16px 24px !important; + } +} + +/* ===== Override PatternFly background images ===== */ +.login-pf-page .login-pf-page-header, +.login-pf body { + background-image: none !important; +} + +/* Remove any PF4 container-fluid stretching */ +.container-fluid { + padding: 0 !important; + max-width: none !important; +} + +/* Ensure the card doesn't stretch full width */ +.login-pf-page > .card-pf { + max-width: 440px; + margin: 0 auto !important; +} diff --git a/keycloak/themes/certifai/login/resources/img/logo.svg b/keycloak/themes/certifai/login/resources/img/logo.svg new file mode 100644 index 0000000..ac16408 --- /dev/null +++ b/keycloak/themes/certifai/login/resources/img/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/keycloak/themes/certifai/login/theme.properties b/keycloak/themes/certifai/login/theme.properties new file mode 100644 index 0000000..bb5f523 --- /dev/null +++ b/keycloak/themes/certifai/login/theme.properties @@ -0,0 +1,3 @@ +parent=keycloak +import=common/keycloak +styles=css/login.css