Add neo-brutalist Keycloak login theme 'ecosplay'
- Custom theme under themes/ecosplay/login (extends keycloak parent) with template.ftl and login.ftl matching the e-cosplay.fr style: thick black borders, hard offset shadows, italic uppercase, indigo accent, hover translate effect, marquee header, watermark. - Tailwind via Play CDN for utility classes (no build step). - Mount the theme dir read-only into the Keycloak container. - Init container now also sets loginTheme=ecosplay on master realm alongside the SMTP config; service renamed keycloak-init. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,15 +35,17 @@ services:
|
|||||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
ports:
|
ports:
|
||||||
- "9450:8080"
|
- "9450:8080"
|
||||||
|
volumes:
|
||||||
|
- ./themes/ecosplay:/opt/keycloak/themes/ecosplay:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- keycloak-net
|
- keycloak-net
|
||||||
|
|
||||||
keycloak-smtp-init:
|
keycloak-init:
|
||||||
image: quay.io/keycloak/keycloak:26.0
|
image: quay.io/keycloak/keycloak:26.0
|
||||||
container_name: ecosplay-auth-smtp-init
|
container_name: ecosplay-auth-init
|
||||||
depends_on:
|
depends_on:
|
||||||
keycloak:
|
keycloak:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
@@ -57,6 +59,7 @@ services:
|
|||||||
SMTP_FROM_DISPLAY_NAME: E-Cosplay
|
SMTP_FROM_DISPLAY_NAME: E-Cosplay
|
||||||
SMTP_USER: AKIAWTT2T22CWBRBBDYN
|
SMTP_USER: AKIAWTT2T22CWBRBBDYN
|
||||||
SMTP_PASSWORD: BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP
|
SMTP_PASSWORD: BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP
|
||||||
|
LOGIN_THEME: ecosplay
|
||||||
entrypoint: ["/bin/bash", "-c"]
|
entrypoint: ["/bin/bash", "-c"]
|
||||||
command:
|
command:
|
||||||
- |
|
- |
|
||||||
@@ -69,7 +72,7 @@ services:
|
|||||||
echo "Waiting for Keycloak to be ready..."
|
echo "Waiting for Keycloak to be ready..."
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
echo "Keycloak ready, configuring SMTP on master realm..."
|
echo "Keycloak ready, configuring master realm (SMTP + theme)..."
|
||||||
/opt/keycloak/bin/kcadm.sh update realms/master \
|
/opt/keycloak/bin/kcadm.sh update realms/master \
|
||||||
-s "smtpServer.host=$$SMTP_HOST" \
|
-s "smtpServer.host=$$SMTP_HOST" \
|
||||||
-s "smtpServer.port=$$SMTP_PORT" \
|
-s "smtpServer.port=$$SMTP_PORT" \
|
||||||
@@ -79,8 +82,9 @@ services:
|
|||||||
-s "smtpServer.starttls=true" \
|
-s "smtpServer.starttls=true" \
|
||||||
-s "smtpServer.ssl=false" \
|
-s "smtpServer.ssl=false" \
|
||||||
-s "smtpServer.user=$$SMTP_USER" \
|
-s "smtpServer.user=$$SMTP_USER" \
|
||||||
-s "smtpServer.password=$$SMTP_PASSWORD"
|
-s "smtpServer.password=$$SMTP_PASSWORD" \
|
||||||
echo "SMTP configuration applied to master realm."
|
-s "loginTheme=$$LOGIN_THEME"
|
||||||
|
echo "Master realm configured."
|
||||||
networks:
|
networks:
|
||||||
- keycloak-net
|
- keycloak-net
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|||||||
95
themes/ecosplay/login/login.ftl
Normal file
95
themes/ecosplay/login/login.ftl
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
|
||||||
|
<#if section = "header">
|
||||||
|
${msg("loginAccountTitle")}
|
||||||
|
<#elseif section = "form">
|
||||||
|
<#if realm.password>
|
||||||
|
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post" class="space-y-6">
|
||||||
|
|
||||||
|
<#if !usernameHidden??>
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block font-black uppercase tracking-widest text-xs mb-2 text-gray-900">
|
||||||
|
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
|
||||||
|
</label>
|
||||||
|
<input tabindex="1" id="username" name="username" type="text" autofocus autocomplete="username"
|
||||||
|
value="${(login.username!'')}"
|
||||||
|
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||||
|
class="w-full px-4 py-4 bg-white text-gray-900 font-bold not-italic border-4 border-gray-900 placeholder-gray-400 focus:outline-none focus:bg-yellow-100 focus:shadow-[6px_6px_0px_rgba(79,70,229,1)] transition-all" />
|
||||||
|
<#if messagesPerField.existsError('username','password')>
|
||||||
|
<span class="block mt-2 text-red-600 font-black uppercase text-xs tracking-widest" aria-live="polite">
|
||||||
|
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
|
||||||
|
</span>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block font-black uppercase tracking-widest text-xs mb-2 text-gray-900">
|
||||||
|
${msg("password")}
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input tabindex="2" id="password" name="password" type="password" autocomplete="current-password"
|
||||||
|
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||||
|
class="w-full px-4 py-4 bg-white text-gray-900 font-bold not-italic border-4 border-gray-900 placeholder-gray-400 focus:outline-none focus:bg-yellow-100 focus:shadow-[6px_6px_0px_rgba(79,70,229,1)] transition-all" />
|
||||||
|
</div>
|
||||||
|
<#if usernameHidden?? && messagesPerField.existsError('username','password')>
|
||||||
|
<span class="block mt-2 text-red-600 font-black uppercase text-xs tracking-widest" aria-live="polite">
|
||||||
|
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
|
||||||
|
</span>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4 flex-wrap pt-2">
|
||||||
|
<#if realm.rememberMe && !usernameHidden??>
|
||||||
|
<label for="rememberMe" class="flex items-center gap-3 font-black uppercase text-xs tracking-widest cursor-pointer">
|
||||||
|
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"
|
||||||
|
<#if login.rememberMe??>checked</#if>
|
||||||
|
class="w-5 h-5 border-4 border-gray-900 accent-indigo-600">
|
||||||
|
${msg("rememberMe")}
|
||||||
|
</label>
|
||||||
|
</#if>
|
||||||
|
<#if realm.resetPasswordAllowed>
|
||||||
|
<a tabindex="5" href="${url.loginResetCredentialsUrl}"
|
||||||
|
class="font-black uppercase text-xs tracking-widest text-indigo-600 hover:text-gray-900 underline decoration-4 underline-offset-4">
|
||||||
|
${msg("doForgotPassword")}
|
||||||
|
</a>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
|
||||||
|
|
||||||
|
<button tabindex="4" name="login" id="kc-login" type="submit"
|
||||||
|
class="block w-full px-8 py-5 bg-indigo-600 text-white font-black uppercase italic tracking-widest text-lg border-4 border-gray-900 shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-2 hover:translate-y-2 transition-all">
|
||||||
|
${msg("doLogIn")}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#elseif section = "info">
|
||||||
|
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="font-bold uppercase tracking-tight text-gray-700">${msg("noAccount")}</span>
|
||||||
|
<a tabindex="6" href="${url.registrationUrl}"
|
||||||
|
class="ml-2 font-black uppercase text-indigo-600 hover:text-gray-900 underline decoration-4 underline-offset-4">
|
||||||
|
${msg("doRegister")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#elseif section = "socialProviders">
|
||||||
|
<#if realm.password && social?? && social.providers?? && social.providers?has_content>
|
||||||
|
<div class="mt-8 pt-6 border-t-4 border-gray-900">
|
||||||
|
<p class="text-indigo-600 font-black uppercase tracking-[0.3em] mb-4 text-xs">// ${msg("identity-provider-login-label")}</p>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<#list social.providers as p>
|
||||||
|
<a id="social-${p.alias}" href="${p.loginUrl}"
|
||||||
|
class="flex items-center justify-center gap-2 px-4 py-3 bg-white text-gray-900 font-black uppercase tracking-widest text-xs border-4 border-gray-900 shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all">
|
||||||
|
<#if p.iconClasses?has_content><i class="${p.iconClasses!}" aria-hidden="true"></i></#if>
|
||||||
|
<span>${p.displayName!}</span>
|
||||||
|
</a>
|
||||||
|
</#list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
||||||
120
themes/ecosplay/login/template.ftl
Normal file
120
themes/ecosplay/login/template.ftl
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false showAnotherWayIfPresent=true>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="${locale.currentLanguageTag}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<title>${msg("loginTitle",(realm.displayName!''))}</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
@keyframes marquee {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
100% { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
.animate-marquee {
|
||||||
|
display: flex;
|
||||||
|
width: 200%;
|
||||||
|
animation: marquee 40s linear infinite;
|
||||||
|
}
|
||||||
|
body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus {
|
||||||
|
-webkit-text-fill-color: #111827;
|
||||||
|
-webkit-box-shadow: 0 0 0px 1000px #ffffff inset;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#fbfbfb] italic min-h-screen flex flex-col overflow-x-hidden ${properties.kcBodyClass!}">
|
||||||
|
|
||||||
|
<!-- Background watermark -->
|
||||||
|
<div aria-hidden="true" class="fixed inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
|
||||||
|
<span class="text-[30rem] font-black uppercase leading-none block -rotate-12 translate-y-20">ECOSPLAY</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top marquee -->
|
||||||
|
<div class="bg-gray-900 py-4 border-b-4 border-indigo-600 overflow-hidden relative z-10">
|
||||||
|
<div class="flex whitespace-nowrap animate-marquee italic">
|
||||||
|
<#list 1..6 as i>
|
||||||
|
<span class="text-white font-black uppercase mx-8 text-lg opacity-80">
|
||||||
|
Authentification // E-Cosplay // Communauté Inclusive // Hauts-de-France
|
||||||
|
</span>
|
||||||
|
</#list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="flex-grow flex items-center justify-center px-4 py-16 relative z-10">
|
||||||
|
<div class="w-full max-w-xl">
|
||||||
|
|
||||||
|
<!-- Card -->
|
||||||
|
<div class="bg-white border-4 border-gray-900 shadow-[12px_12px_0px_rgba(0,0,0,1)] p-8 md:p-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8 pb-6 border-b-4 border-gray-900">
|
||||||
|
<p class="text-indigo-600 font-black uppercase tracking-[0.3em] mb-3 text-xs">// ${realm.displayName!"E-Cosplay"}</p>
|
||||||
|
<h1 class="text-4xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9]">
|
||||||
|
<#nested "header">
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
|
||||||
|
<div class="mb-6 border-4 border-gray-900 p-4 font-black uppercase tracking-tight text-sm shadow-[6px_6px_0px_rgba(0,0,0,1)]
|
||||||
|
<#if message.type = 'success'>bg-green-200</#if>
|
||||||
|
<#if message.type = 'warning'>bg-yellow-300</#if>
|
||||||
|
<#if message.type = 'error'>bg-red-300</#if>
|
||||||
|
<#if message.type = 'info'>bg-indigo-200</#if>
|
||||||
|
">
|
||||||
|
${kcSanitize(message.summary)?no_esc}
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<!-- Required fields hint -->
|
||||||
|
<#if displayRequiredFields>
|
||||||
|
<p class="mb-6 text-xs font-black uppercase tracking-widest text-gray-500">
|
||||||
|
<span class="text-red-600">*</span> ${msg("requiredFields")}
|
||||||
|
</p>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<!-- Main form / content -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<#nested "form">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social providers -->
|
||||||
|
<#nested "socialProviders">
|
||||||
|
|
||||||
|
<!-- Info section -->
|
||||||
|
<#if displayInfo>
|
||||||
|
<div class="mt-8 pt-6 border-t-4 border-gray-900 text-sm font-bold">
|
||||||
|
<#nested "info">
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Locale selector -->
|
||||||
|
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
|
||||||
|
<div class="mt-6 bg-white border-4 border-gray-900 p-3 shadow-[6px_6px_0px_rgba(0,0,0,1)] flex items-center justify-center gap-4 flex-wrap">
|
||||||
|
<span class="font-black uppercase text-xs tracking-widest text-gray-500">// Langue</span>
|
||||||
|
<#list locale.supported as l>
|
||||||
|
<a href="${l.url}" class="font-black uppercase text-xs tracking-widest <#if l.languageTag == locale.currentLanguageTag>text-indigo-600 underline decoration-4 underline-offset-4<#else>text-gray-900 hover:text-indigo-600</#if>">
|
||||||
|
${l.label}
|
||||||
|
</a>
|
||||||
|
</#list>
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer band -->
|
||||||
|
<footer class="bg-gray-900 border-t-4 border-indigo-600 py-6 px-4 relative z-10">
|
||||||
|
<p class="text-center text-white font-black uppercase tracking-widest text-xs italic">
|
||||||
|
© E-Cosplay // Créer la réalité
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</#macro>
|
||||||
3
themes/ecosplay/login/theme.properties
Normal file
3
themes/ecosplay/login/theme.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
parent=keycloak
|
||||||
|
import=common/keycloak
|
||||||
|
locales=en,fr
|
||||||
Reference in New Issue
Block a user