diff --git a/conf/keycloak_theme/keywind/.DS_Store b/conf/keycloak_theme/keywind/.DS_Store
new file mode 100644
index 0000000000..694ba58ec7
Binary files /dev/null and b/conf/keycloak_theme/keywind/.DS_Store differ
diff --git a/conf/keycloak_theme/keywind/login/assets/icons/arrow-top-right-on-square.ftl b/conf/keycloak_theme/keywind/login/assets/icons/arrow-top-right-on-square.ftl
new file mode 100644
index 0000000000..81c4bf81d8
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/icons/arrow-top-right-on-square.ftl
@@ -0,0 +1,7 @@
+<#-- https://github.com/tailwindlabs/heroicons/blob/master/src/20/solid/arrow-top-right-on-square.svg -->
+<#macro kw>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/icons/chevron-down.ftl b/conf/keycloak_theme/keywind/login/assets/icons/chevron-down.ftl
new file mode 100644
index 0000000000..673ef1191f
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/icons/chevron-down.ftl
@@ -0,0 +1,6 @@
+<#-- https://github.com/tailwindlabs/heroicons/blob/master/src/20/solid/chevron-down.svg -->
+<#macro kw>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/bitbucket.ftl b/conf/keycloak_theme/keywind/login/assets/providers/bitbucket.ftl
new file mode 100644
index 0000000000..068bc73d85
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/bitbucket.ftl
@@ -0,0 +1,14 @@
+<#-- https://atlassian.design/resources/logo-library -->
+<#macro kw name="Bitbucket">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/discord.ftl b/conf/keycloak_theme/keywind/login/assets/providers/discord.ftl
new file mode 100644
index 0000000000..8ebecaa487
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/discord.ftl
@@ -0,0 +1,7 @@
+<#-- https://discord.com/branding -->
+<#macro kw name="Discord">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/facebook.ftl b/conf/keycloak_theme/keywind/login/assets/providers/facebook.ftl
new file mode 100644
index 0000000000..bc692e7594
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/facebook.ftl
@@ -0,0 +1,8 @@
+<#-- https://www.facebook.com/brand/resources/facebookapp/logo -->
+<#macro kw name="Facebook">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/github.ftl b/conf/keycloak_theme/keywind/login/assets/providers/github.ftl
new file mode 100644
index 0000000000..9523103c47
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/github.ftl
@@ -0,0 +1,7 @@
+<#-- https://github.com/logos -->
+<#macro kw name="GitHub">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/gitlab.ftl b/conf/keycloak_theme/keywind/login/assets/providers/gitlab.ftl
new file mode 100644
index 0000000000..4acfc132d1
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/gitlab.ftl
@@ -0,0 +1,10 @@
+<#-- https://about.gitlab.com/press/press-kit -->
+<#macro kw name="GitLab">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/google.ftl b/conf/keycloak_theme/keywind/login/assets/providers/google.ftl
new file mode 100644
index 0000000000..b536cdbb8e
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/google.ftl
@@ -0,0 +1,10 @@
+<#-- https://developers.google.com/identity/branding-guidelines -->
+<#macro kw name="Google">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/instagram.ftl b/conf/keycloak_theme/keywind/login/assets/providers/instagram.ftl
new file mode 100644
index 0000000000..c4996d8803
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/instagram.ftl
@@ -0,0 +1,35 @@
+<#-- https://www.facebook.com/brand/resources/instagram/instagram-brand -->
+<#macro kw name="Instagram">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/linkedin.ftl b/conf/keycloak_theme/keywind/login/assets/providers/linkedin.ftl
new file mode 100644
index 0000000000..944d143357
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/linkedin.ftl
@@ -0,0 +1,7 @@
+<#-- https://brand.linkedin.com/downloads -->
+<#macro kw name="LinkedIn">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/microsoft.ftl b/conf/keycloak_theme/keywind/login/assets/providers/microsoft.ftl
new file mode 100644
index 0000000000..408635b842
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/microsoft.ftl
@@ -0,0 +1,10 @@
+<#-- https://learn.microsoft.com/azure/active-directory/develop/howto-add-branding-in-azure-ad-apps -->
+<#macro kw name="Microsoft">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/oidc.ftl b/conf/keycloak_theme/keywind/login/assets/providers/oidc.ftl
new file mode 100644
index 0000000000..f7954ff5e8
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/oidc.ftl
@@ -0,0 +1,9 @@
+<#-- https://openid.net/add-openid/logos -->
+<#macro kw name="OpenID">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/openshift.ftl b/conf/keycloak_theme/keywind/login/assets/providers/openshift.ftl
new file mode 100644
index 0000000000..e85ddef7eb
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/openshift.ftl
@@ -0,0 +1,11 @@
+<#-- https://www.redhat.com/technologies/cloud-computing/openshift -->
+<#macro kw name="Red Hat OpenShift">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/paypal.ftl b/conf/keycloak_theme/keywind/login/assets/providers/paypal.ftl
new file mode 100644
index 0000000000..7946e03cd2
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/paypal.ftl
@@ -0,0 +1,9 @@
+<#-- https://www.paypal.com -->
+<#macro kw name="PayPal">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/providers.ftl b/conf/keycloak_theme/keywind/login/assets/providers/providers.ftl
new file mode 100644
index 0000000000..b9c55f6c8a
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/providers.ftl
@@ -0,0 +1,79 @@
+<#import "./bitbucket.ftl" as bitbucketIcon>
+<#import "./discord.ftl" as discordIcon>
+<#import "./facebook.ftl" as facebookIcon>
+<#import "./github.ftl" as githubIcon>
+<#import "./gitlab.ftl" as gitlabIcon>
+<#import "./google.ftl" as googleIcon>
+<#import "./instagram.ftl" as instagramIcon>
+<#import "./linkedin.ftl" as linkedinIcon>
+<#import "./microsoft.ftl" as microsoftIcon>
+<#import "./oidc.ftl" as oidcIcon>
+<#import "./openshift.ftl" as openshiftIcon>
+<#import "./paypal.ftl" as paypalIcon>
+<#import "./slack.ftl" as slackIcon>
+<#import "./stackoverflow.ftl" as stackoverflowIcon>
+<#import "./twitter.ftl" as twitterIcon>
+
+<#macro bitbucket>
+ <@bitbucketIcon.kw />
+#macro>
+
+<#macro discord>
+ <@discordIcon.kw />
+#macro>
+
+<#macro facebook>
+ <@facebookIcon.kw />
+#macro>
+
+<#macro github>
+ <@githubIcon.kw />
+#macro>
+
+<#macro gitlab>
+ <@gitlabIcon.kw />
+#macro>
+
+<#macro google>
+ <@googleIcon.kw />
+#macro>
+
+<#macro instagram>
+ <@instagramIcon.kw />
+#macro>
+
+<#macro linkedin>
+ <@linkedinIcon.kw />
+#macro>
+
+<#macro microsoft>
+ <@microsoftIcon.kw />
+#macro>
+
+<#macro oidc>
+ <@oidcIcon.kw />
+#macro>
+
+<#macro "openshift-v3">
+ <@openshiftIcon.kw />
+#macro>
+
+<#macro "openshift-v4">
+ <@openshiftIcon.kw />
+#macro>
+
+<#macro paypal>
+ <@paypalIcon.kw />
+#macro>
+
+<#macro slack>
+ <@slackIcon.kw />
+#macro>
+
+<#macro stackoverflow>
+ <@stackoverflowIcon.kw />
+#macro>
+
+<#macro twitter>
+ <@twitterIcon.kw />
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/slack.ftl b/conf/keycloak_theme/keywind/login/assets/providers/slack.ftl
new file mode 100644
index 0000000000..d4dffe3bbb
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/slack.ftl
@@ -0,0 +1,14 @@
+<#-- https://slack.com/media-kit -->
+<#macro kw name="Slack">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/stackoverflow.ftl b/conf/keycloak_theme/keywind/login/assets/providers/stackoverflow.ftl
new file mode 100644
index 0000000000..1ffad8d6a0
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/stackoverflow.ftl
@@ -0,0 +1,8 @@
+<#-- https://stackoverflow.design/brand/logo -->
+<#macro kw name="Stack Overflow">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/assets/providers/twitter.ftl b/conf/keycloak_theme/keywind/login/assets/providers/twitter.ftl
new file mode 100644
index 0000000000..2bc7e7e49d
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/assets/providers/twitter.ftl
@@ -0,0 +1,7 @@
+<#-- https://about.twitter.com/en/who-we-are/brand-toolkit -->
+<#macro kw name="Twitter">
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/alert.ftl b/conf/keycloak_theme/keywind/login/components/atoms/alert.ftl
new file mode 100644
index 0000000000..58e8309f07
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/alert.ftl
@@ -0,0 +1,22 @@
+<#macro kw color="">
+ <#switch color>
+ <#case "error">
+ <#assign colorClass="bg-red-100 text-red-600">
+ <#break>
+ <#case "info">
+ <#assign colorClass="bg-blue-100 text-blue-600">
+ <#break>
+ <#case "success">
+ <#assign colorClass="bg-green-100 text-green-600">
+ <#break>
+ <#case "warning">
+ <#assign colorClass="bg-orange-100 text-orange-600">
+ <#break>
+ <#default>
+ <#assign colorClass="bg-blue-100 text-blue-600">
+ #switch>
+
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/body.ftl b/conf/keycloak_theme/keywind/login/components/atoms/body.ftl
new file mode 100644
index 0000000000..dcc94a06ee
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/body.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/button-group.ftl b/conf/keycloak_theme/keywind/login/components/atoms/button-group.ftl
new file mode 100644
index 0000000000..459120917e
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/button-group.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/button.ftl b/conf/keycloak_theme/keywind/login/components/atoms/button.ftl
new file mode 100644
index 0000000000..eeb0af7b7e
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/button.ftl
@@ -0,0 +1,33 @@
+<#macro kw color="" component="button" size="" rest...>
+ <#switch color>
+ <#case "primary">
+ <#assign colorClass="bg-primary-600 text-white focus:ring-primary-600 hover:bg-primary-700">
+ <#break>
+ <#case "secondary">
+ <#assign colorClass="bg-secondary-100 text-secondary-600 focus:ring-secondary-600 hover:bg-secondary-200 hover:text-secondary-900">
+ <#break>
+ <#default>
+ <#assign colorClass="bg-primary-600 text-white focus:ring-primary-600 hover:bg-primary-700">
+ #switch>
+
+ <#switch size>
+ <#case "medium">
+ <#assign sizeClass="px-4 py-2 text-sm">
+ <#break>
+ <#case "small">
+ <#assign sizeClass="px-2 py-1 text-xs">
+ <#break>
+ <#default>
+ <#assign sizeClass="px-4 py-2 text-sm">
+ #switch>
+
+ <${component}
+ class="${colorClass} ${sizeClass} flex justify-center relative rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-offset-2"
+
+ <#list rest as attrName, attrValue>
+ ${attrName}="${attrValue}"
+ #list>
+ >
+ <#nested>
+ ${component}>
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/card.ftl b/conf/keycloak_theme/keywind/login/components/atoms/card.ftl
new file mode 100644
index 0000000000..c1e808df18
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/card.ftl
@@ -0,0 +1,19 @@
+<#macro kw content="" footer="" header="">
+
+ <#if header?has_content>
+
+ ${header}
+
+ #if>
+ <#if content?has_content>
+
+ ${content}
+
+ #if>
+ <#if footer?has_content>
+
+ ${footer}
+
+ #if>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/checkbox.ftl b/conf/keycloak_theme/keywind/login/components/atoms/checkbox.ftl
new file mode 100644
index 0000000000..e47fd619ac
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/checkbox.ftl
@@ -0,0 +1,19 @@
+<#macro kw checked=false label="" name="" rest...>
+
+ checked#if>
+
+ class="border-secondary-200 h-4 rounded text-primary-600 w-4 focus:ring-primary-200 focus:ring-opacity-50"
+ id="${name}"
+ name="${name}"
+ type="checkbox"
+
+ <#list rest as attrName, attrValue>
+ ${attrName}="${attrValue}"
+ #list>
+ >
+
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/container.ftl b/conf/keycloak_theme/keywind/login/components/atoms/container.ftl
new file mode 100644
index 0000000000..34ead183c2
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/container.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/form.ftl b/conf/keycloak_theme/keywind/login/components/atoms/form.ftl
new file mode 100644
index 0000000000..014bb4f1cc
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/form.ftl
@@ -0,0 +1,11 @@
+<#macro kw rest...>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/heading.ftl b/conf/keycloak_theme/keywind/login/components/atoms/heading.ftl
new file mode 100644
index 0000000000..7665c01965
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/heading.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/input.ftl b/conf/keycloak_theme/keywind/login/components/atoms/input.ftl
new file mode 100644
index 0000000000..01e2897ad9
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/input.ftl
@@ -0,0 +1,37 @@
+<#macro
+ kw
+ autofocus=false
+ disabled=false
+ invalid=false
+ label=""
+ message=""
+ name=""
+ required=true
+ rest...
+>
+
+
+
autofocus#if>
+ <#if disabled>disabled#if>
+ <#if required>required#if>
+
+ aria-invalid="${invalid?c}"
+ class="block border-secondary-200 mt-1 rounded-md w-full focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50 sm:text-sm"
+ id="${name}"
+ name="${name}"
+ placeholder="${label}"
+
+ <#list rest as attrName, attrValue>
+ ${attrName}="${attrValue}"
+ #list>
+ >
+ <#if invalid?? && message??>
+
+ ${message?no_esc}
+
+ #if>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/link.ftl b/conf/keycloak_theme/keywind/login/components/atoms/link.ftl
new file mode 100644
index 0000000000..bde766653f
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/link.ftl
@@ -0,0 +1,30 @@
+<#macro kw color="" component="a" size="" rest...>
+ <#switch color>
+ <#case "primary">
+ <#assign colorClass="text-primary-600 hover:text-primary-500">
+ <#break>
+ <#case "secondary">
+ <#assign colorClass="text-secondary-600 hover:text-secondary-900">
+ <#break>
+ <#default>
+ <#assign colorClass="text-primary-600 hover:text-primary-500">
+ #switch>
+
+ <#switch size>
+ <#case "small">
+ <#assign sizeClass="text-sm">
+ <#break>
+ <#default>
+ <#assign sizeClass="">
+ #switch>
+
+ <${component}
+ class="<#compress>${colorClass} ${sizeClass} inline-flex#compress>"
+
+ <#list rest as attrName, attrValue>
+ ${attrName}="${attrValue}"
+ #list>
+ >
+ <#nested>
+ ${component}>
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/logo.ftl b/conf/keycloak_theme/keywind/login/components/atoms/logo.ftl
new file mode 100644
index 0000000000..f166403e68
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/logo.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/nav.ftl b/conf/keycloak_theme/keywind/login/components/atoms/nav.ftl
new file mode 100644
index 0000000000..81a4abf635
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/nav.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+
+ <#nested>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/atoms/radio.ftl b/conf/keycloak_theme/keywind/login/components/atoms/radio.ftl
new file mode 100644
index 0000000000..5596d5c4b5
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/atoms/radio.ftl
@@ -0,0 +1,18 @@
+<#macro kw checked=false id="" label="" rest...>
+
+ checked#if>
+
+ class="border-secondary-200 focus:ring-primary-600"
+ id="${id}"
+ type="radio"
+
+ <#list rest as attrName, attrValue>
+ ${attrName}="${attrValue}"
+ #list>
+ >
+
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/molecules/identity-provider.ftl b/conf/keycloak_theme/keywind/login/components/molecules/identity-provider.ftl
new file mode 100644
index 0000000000..50c9c81c77
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/molecules/identity-provider.ftl
@@ -0,0 +1,78 @@
+<#import "/assets/providers/providers.ftl" as providerIcons>
+
+<#macro kw providers=[]>
+
+ ${msg("identity-provider-login-label")}
+
+
+ <#list providers as provider>
+ <#switch provider.alias>
+ <#case "bitbucket">
+ <#assign colorClass="hover:bg-provider-bitbucket/10">
+ <#break>
+ <#case "discord">
+ <#assign colorClass="hover:bg-provider-discord/10">
+ <#break>
+ <#case "facebook">
+ <#assign colorClass="hover:bg-provider-facebook/10">
+ <#break>
+ <#case "github">
+ <#assign colorClass="hover:bg-provider-github/10">
+ <#break>
+ <#case "gitlab">
+ <#assign colorClass="hover:bg-provider-gitlab/10">
+ <#break>
+ <#case "google">
+ <#assign colorClass="hover:bg-provider-google/10">
+ <#break>
+ <#case "instagram">
+ <#assign colorClass="hover:bg-provider-instagram/10">
+ <#break>
+ <#case "linkedin">
+ <#assign colorClass="hover:bg-provider-linkedin/10">
+ <#break>
+ <#case "microsoft">
+ <#assign colorClass="hover:bg-provider-microsoft/10">
+ <#break>
+ <#case "oidc">
+ <#assign colorClass="hover:bg-provider-oidc/10">
+ <#break>
+ <#case "openshift-v3">
+ <#assign colorClass="hover:bg-provider-openshift/10">
+ <#break>
+ <#case "openshift-v4">
+ <#assign colorClass="hover:bg-provider-openshift/10">
+ <#break>
+ <#case "paypal">
+ <#assign colorClass="hover:bg-provider-paypal/10">
+ <#break>
+ <#case "slack">
+ <#assign colorClass="hover:bg-provider-slack/10">
+ <#break>
+ <#case "stackoverflow">
+ <#assign colorClass="hover:bg-provider-stackoverflow/10">
+ <#break>
+ <#case "twitter">
+ <#assign colorClass="hover:bg-provider-twitter/10">
+ <#break>
+ <#default>
+ <#assign colorClass="hover:bg-secondary-100">
+ #switch>
+
+
+ <#if providerIcons[provider.alias]??>
+
+ <@providerIcons[provider.alias] />
+
+ <#else>
+ ${provider.displayName!}
+ #if>
+
+ #list>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/molecules/locale-provider.ftl b/conf/keycloak_theme/keywind/login/components/molecules/locale-provider.ftl
new file mode 100644
index 0000000000..198e5be10d
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/molecules/locale-provider.ftl
@@ -0,0 +1,29 @@
+<#import "/assets/icons/chevron-down.ftl" as icon>
+<#import "/components/atoms/link.ftl" as link>
+
+<#macro kw currentLocale="" locales=[]>
+
+ <@link.kw @click="open = true" color="secondary" component="button" type="button">
+
+ ${currentLocale}
+ <@icon.kw />
+
+ @link.kw>
+
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/components/molecules/username.ftl b/conf/keycloak_theme/keywind/login/components/molecules/username.ftl
new file mode 100644
index 0000000000..ba6339389c
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/components/molecules/username.ftl
@@ -0,0 +1,15 @@
+<#import "/assets/icons/arrow-top-right-on-square.ftl" as icon>
+<#import "/components/atoms/link.ftl" as link>
+
+<#macro kw linkHref="" linkTitle="" name="">
+
+ ${name}
+ <@link.kw
+ color="primary"
+ href=linkHref
+ title=linkTitle
+ >
+ <@icon.kw />
+ @link.kw>
+
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/document.ftl b/conf/keycloak_theme/keywind/login/document.ftl
new file mode 100644
index 0000000000..188e16a31d
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/document.ftl
@@ -0,0 +1,35 @@
+<#macro kw script="">
+ ${msg("loginTitle", (realm.displayName!""))}
+
+
+
+
+
+ <#if properties.meta?has_content>
+ <#list properties.meta?split(" ") as meta>
+
+ #list>
+ #if>
+
+ <#if properties.favicons?has_content>
+ <#list properties.favicons?split(" ") as favicon>
+
+ #list>
+ #if>
+
+ <#if properties.styles?has_content>
+ <#list properties.styles?split(" ") as style>
+
+ #list>
+ #if>
+
+ <#if script?has_content>
+
+ #if>
+
+ <#if properties.scripts?has_content>
+ <#list properties.scripts?split(" ") as script>
+
+ #list>
+ #if>
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/error.ftl b/conf/keycloak_theme/keywind/login/error.ftl
new file mode 100644
index 0000000000..52af9c1e64
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/error.ftl
@@ -0,0 +1,18 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/alert.ftl" as alert>
+<#import "components/atoms/link.ftl" as link>
+
+<@layout.registrationLayout displayMessage=false; section>
+ <#if section="header">
+ ${kcSanitize(msg("errorTitle"))?no_esc}
+ <#elseif section="form">
+ <@alert.kw color="error">${kcSanitize(message.summary)?no_esc}@alert.kw>
+ <#if !skipLink??>
+ <#if client?? && client.baseUrl?has_content>
+ <@link.kw color="secondary" href=client.baseUrl size="small">
+ ${kcSanitize(msg("backToApplication"))?no_esc}
+ @link.kw>
+ #if>
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/features/labels/totp-device.ftl b/conf/keycloak_theme/keywind/login/features/labels/totp-device.ftl
new file mode 100644
index 0000000000..98ae12f8d2
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/features/labels/totp-device.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+ <#compress>
+ ${msg("loginTotpDeviceName")} <#if totp.otpCredentials?size gte 1>*#if>
+ #compress>
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/features/labels/totp.ftl b/conf/keycloak_theme/keywind/login/features/labels/totp.ftl
new file mode 100644
index 0000000000..be5158ebe1
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/features/labels/totp.ftl
@@ -0,0 +1,5 @@
+<#macro kw>
+ <#compress>
+ ${msg("authenticatorCode")} *
+ #compress>
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/features/labels/username.ftl b/conf/keycloak_theme/keywind/login/features/labels/username.ftl
new file mode 100644
index 0000000000..6c01d6b346
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/features/labels/username.ftl
@@ -0,0 +1,11 @@
+<#macro kw>
+ <#compress>
+ <#if !realm.loginWithEmailAllowed>
+ ${msg("username")}
+ <#elseif !realm.registrationEmailAsUsername>
+ ${msg("usernameOrEmail")}
+ <#else>
+ ${msg("email")}
+ #if>
+ #compress>
+#macro>
diff --git a/conf/keycloak_theme/keywind/login/login-config-totp.ftl b/conf/keycloak_theme/keywind/login/login-config-totp.ftl
new file mode 100644
index 0000000000..e0b64c6346
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-config-totp.ftl
@@ -0,0 +1,110 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/link.ftl" as link>
+<#import "features/labels/totp.ftl" as totpLabel>
+<#import "features/labels/totp-device.ftl" as totpDeviceLabel>
+
+<#assign totpLabel><@totpLabel.kw />#assign>
+<#assign totpDeviceLabel><@totpDeviceLabel.kw />#assign>
+
+<@layout.registrationLayout
+ displayMessage=!messagesPerField.existsError("totp", "userLabel")
+ displayRequiredFields=false
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("loginTotpTitle")}
+ <#elseif section="form">
+
+ -
+
${msg("loginTotpStep1")}
+
+ <#list totp.supportedApplications as app>
+ - ${msg(app)}
+ #list>
+
+
+ <#if mode?? && mode="manual">
+ -
+
${msg("loginTotpManualStep2")}
+ ${totp.totpSecretEncoded}
+
+ -
+ <@link.kw color="primary" href=totp.qrUrl>
+ ${msg("loginTotpScanBarcode")}
+ @link.kw>
+
+ -
+
${msg("loginTotpManualStep3")}
+
+ - ${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}
+ - ${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}
+ - ${msg("loginTotpDigits")}: ${totp.policy.digits}
+ <#if totp.policy.type="totp">
+ - ${msg("loginTotpInterval")}: ${totp.policy.period}
+ <#elseif totp.policy.type="hotp">
+ - ${msg("loginTotpCounter")}: ${totp.policy.initialCounter}
+ #if>
+
+
+ <#else>
+ -
+
${msg("loginTotpStep2")}
+
+ <@link.kw color="primary" href=totp.manualUrl>
+ ${msg("loginTotpUnableToScan")}
+ @link.kw>
+
+ #if>
+ - ${msg("loginTotpStep3")}
+ - ${msg("loginTotpStep3DeviceName")}
+
+ <@form.kw action=url.loginAction method="post">
+
+ <#if mode??>
+
+ #if>
+ <@input.kw
+ autocomplete="off"
+ autofocus=true
+ invalid=messagesPerField.existsError("totp")
+ label=totpLabel
+ message=kcSanitize(messagesPerField.get("totp"))
+ name="totp"
+ required=false
+ type="text"
+ />
+ <@input.kw
+ autocomplete="off"
+ invalid=messagesPerField.existsError("userLabel")
+ label=totpDeviceLabel
+ message=kcSanitize(messagesPerField.get("userLabel"))
+ name="userLabel"
+ required=false
+ type="text"
+ />
+ <@buttonGroup.kw>
+ <#if isAppInitiatedAction??>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ <@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
+ ${msg("doCancel")}
+ @button.kw>
+ <#else>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ #if>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-idp-link-confirm.ftl b/conf/keycloak_theme/keywind/login/login-idp-link-confirm.ftl
new file mode 100644
index 0000000000..9a2554d51f
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-idp-link-confirm.ftl
@@ -0,0 +1,18 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/form.ftl" as form>
+
+<@layout.registrationLayout; section>
+ <#if section="header">
+ ${msg("confirmLinkIdpTitle")}
+ <#elseif section="form">
+ <@form.kw action=url.loginAction method="post">
+ <@button.kw color="primary" name="submitAction" type="submit" value="updateProfile">
+ ${msg("confirmLinkIdpReviewProfile")}
+ @button.kw>
+ <@button.kw color="primary" name="submitAction" type="submit" value="linkAccount">
+ ${msg("confirmLinkIdpContinue", idpDisplayName)}
+ @button.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-oauth-grant.ftl b/conf/keycloak_theme/keywind/login/login-oauth-grant.ftl
new file mode 100644
index 0000000000..aa4173cbf8
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-oauth-grant.ftl
@@ -0,0 +1,62 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+
+<@layout.registrationLayout; section>
+ <#if section="header">
+ <#if client.attributes.logoUri??>
+
+ #if>
+
+ <#if client.name?has_content>
+ ${msg("oauthGrantTitle", advancedMsg(client.name))}
+ <#else>
+ ${msg("oauthGrantTitle", client.clientId)}
+ #if>
+
+ <#elseif section="form">
+ ${msg("oauthGrantRequest")}
+
+ <#if oauth.clientScopesRequested??>
+ <#list oauth.clientScopesRequested as clientScope>
+ -
+ <#if !clientScope.dynamicScopeParameter??>
+ ${advancedMsg(clientScope.consentScreenText)}
+ <#else>
+ ${advancedMsg(clientScope.consentScreenText)}: ${clientScope.dynamicScopeParameter}
+ #if>
+
+ #list>
+ #if>
+
+ <#if client.attributes.policyUri?? || client.attributes.tosUri??>
+
+ <#if client.name?has_content>
+ ${msg("oauthGrantInformation",advancedMsg(client.name))}
+ <#else>
+ ${msg("oauthGrantInformation",client.clientId)}
+ #if>
+ <#if client.attributes.tosUri??>
+ ${msg("oauthGrantReview")}
+ ${msg("oauthGrantTos")}
+ #if>
+ <#if client.attributes.policyUri??>
+ ${msg("oauthGrantReview")}
+ ${msg("oauthGrantPolicy")}
+ #if>
+
+ #if>
+ <@form.kw action=url.oauthAction method="post">
+
+ <@buttonGroup.kw>
+ <@button.kw color="primary" name="accept" type="submit">
+ ${msg("doYes")}
+ @button.kw>
+ <@button.kw color="secondary" name="cancel" type="submit">
+ ${msg("doNo")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-otp.ftl b/conf/keycloak_theme/keywind/login/login-otp.ftl
new file mode 100644
index 0000000000..b1bb3b975a
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-otp.ftl
@@ -0,0 +1,50 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/radio.ftl" as radio>
+<#import "features/labels/totp.ftl" as totpLabel>
+
+<#assign totpLabel><@totpLabel.kw />#assign>
+
+<@layout.registrationLayout
+ displayMessage=!messagesPerField.existsError("totp")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("doLogIn")}
+ <#elseif section="form">
+ <@form.kw action=url.loginAction method="post">
+ <#if otpLogin.userOtpCredentials?size gt 1>
+
+ <#list otpLogin.userOtpCredentials as otpCredential>
+ <@radio.kw
+ checked=(otpCredential.id == otpLogin.selectedCredentialId)
+ id="kw-otp-credential-${otpCredential?index}"
+ label=otpCredential.userLabel
+ name="selectedCredentialId"
+ tabindex=otpCredential?index
+ value=otpCredential.id
+ />
+ #list>
+
+ #if>
+ <@input.kw
+ autocomplete="off"
+ autofocus=true
+ invalid=messagesPerField.existsError("totp")
+ label=totpLabel
+ message=kcSanitize(messagesPerField.get("totp"))
+ name="otp"
+ type="text"
+ />
+ <@buttonGroup.kw>
+ <@button.kw color="primary" name="submitAction" type="submit">
+ ${msg("doLogIn")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-page-expired.ftl b/conf/keycloak_theme/keywind/login/login-page-expired.ftl
new file mode 100644
index 0000000000..2b6288d946
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-page-expired.ftl
@@ -0,0 +1,18 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+
+<@layout.registrationLayout; section>
+ <#if section="header">
+ ${msg("pageExpiredTitle")}
+ <#elseif section="form">
+ <@buttonGroup.kw>
+ <@button.kw color="primary" component="a" href=url.loginRestartFlowUrl>
+ ${msg("doTryAgain")}
+ @button.kw>
+ <@button.kw color="secondary" component="a" href=url.loginAction>
+ ${msg("doContinue")}
+ @button.kw>
+ @buttonGroup.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-password.ftl b/conf/keycloak_theme/keywind/login/login-password.ftl
new file mode 100644
index 0000000000..54e7d9dca4
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-password.ftl
@@ -0,0 +1,39 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/link.ftl" as link>
+
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError("password"); section>
+ <#if section="header">
+ ${msg("doLogIn")}
+ <#elseif section="form">
+ <@form.kw
+ action=url.loginAction
+ method="post"
+ onsubmit="login.disabled = true; return true;"
+ >
+ <@input.kw
+ autofocus=true
+ invalid=messagesPerField.existsError("password")
+ label=msg("password")
+ message=kcSanitize(messagesPerField.get("password"))?no_esc
+ name="password"
+ type="password"
+ />
+ <#if realm.resetPasswordAllowed>
+
+ <@link.kw color="primary" href=url.loginResetCredentialsUrl size="small">
+ ${msg("doForgotPassword")}
+ @link.kw>
+
+ #if>
+ <@buttonGroup.kw>
+ <@button.kw color="primary" name="login" type="submit">
+ ${msg("doLogIn")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-recovery-authn-code-config.ftl b/conf/keycloak_theme/keywind/login/login-recovery-authn-code-config.ftl
new file mode 100644
index 0000000000..186d710802
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-recovery-authn-code-config.ftl
@@ -0,0 +1,91 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/alert.ftl" as alert>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/checkbox.ftl" as checkbox>
+<#import "components/atoms/form.ftl" as form>
+
+<@layout.registrationLayout script="dist/recoveryCodes.js"; section>
+ <#if section="header">
+ ${msg("recovery-code-config-header")}
+ <#elseif section="form">
+
+ <@alert.kw color="warning">
+
+
${msg("recovery-code-config-warning-title")}
+
${msg("recovery-code-config-warning-message")}
+
+ @alert.kw>
+
+ <#list recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList as code>
+ - ${code[0..3]}-${code[4..7]}-${code[8..]}
+ #list>
+
+
+ <@button.kw @click="print" color="secondary" size="small" type="button">
+ ${msg("recovery-codes-print")}
+ @button.kw>
+ <@button.kw @click="download" color="secondary" size="small" type="button">
+ ${msg("recovery-codes-download")}
+ @button.kw>
+ <@button.kw @click="copy" color="secondary" size="small" type="button">
+ ${msg("recovery-codes-copy")}
+ @button.kw>
+
+ <@form.kw action=url.loginAction method="post">
+
+
+
+ <@checkbox.kw
+ label=msg("recovery-codes-confirmation-message")
+ name="kcRecoveryCodesConfirmationCheck"
+ required="required"
+ x\-ref="confirmationCheck"
+ />
+ <@buttonGroup.kw>
+ <#if isAppInitiatedAction??>
+ <@button.kw color="primary" type="submit">
+ ${msg("recovery-codes-action-complete")}
+ @button.kw>
+ <@button.kw
+ @click="$refs.confirmationCheck.required = false"
+ color="secondary"
+ name="cancel-aia"
+ type="submit"
+ value="true"
+ >
+ ${msg("recovery-codes-action-cancel")}
+ @button.kw>
+ <#else>
+ <@button.kw color="primary" type="submit">
+ ${msg("recovery-codes-action-complete")}
+ @button.kw>
+ #if>
+ @buttonGroup.kw>
+ @form.kw>
+
+ #if>
+@layout.registrationLayout>
+
+
diff --git a/conf/keycloak_theme/keywind/login/login-recovery-authn-code-input.ftl b/conf/keycloak_theme/keywind/login/login-recovery-authn-code-input.ftl
new file mode 100644
index 0000000000..a46bcfa09e
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-recovery-authn-code-input.ftl
@@ -0,0 +1,26 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+
+<@layout.registrationLayout; section>
+ <#if section="header">
+ ${msg("auth-recovery-code-header")}
+ <#elseif section="form">
+ <@form.kw action=url.loginAction method="post">
+ <@input.kw
+ autocomplete="off"
+ autofocus=true
+ label=msg("auth-recovery-code-prompt", recoveryAuthnCodesInputBean.codeNumber?c)
+ name="recoveryCodeInput"
+ type="text"
+ />
+ <@buttonGroup.kw>
+ <@button.kw color="primary" name="login" type="submit">
+ ${msg("doLogIn")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-reset-password.ftl b/conf/keycloak_theme/keywind/login/login-reset-password.ftl
new file mode 100644
index 0000000000..b0516aae82
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-reset-password.ftl
@@ -0,0 +1,44 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/link.ftl" as link>
+<#import "features/labels/username.ftl" as usernameLabel>
+
+<#assign usernameLabel><@usernameLabel.kw />#assign>
+
+<@layout.registrationLayout
+ displayInfo=true
+ displayMessage=!messagesPerField.existsError("username")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("emailForgotTitle")}
+ <#elseif section="form">
+ <@form.kw action=url.loginAction method="post">
+ <@input.kw
+ autocomplete=realm.loginWithEmailAllowed?string("email", "username")
+ autofocus=true
+ invalid=messagesPerField.existsError("username")
+ label=usernameLabel
+ message=kcSanitize(messagesPerField.get("username"))
+ name="username"
+ type="text"
+ value=(auth?has_content && auth.showUsername())?then(auth.attemptedUsername, '')
+ />
+ <@buttonGroup.kw>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ <#elseif section="info">
+ ${msg("emailInstruction")}
+ <#elseif section="nav">
+ <@link.kw color="secondary" href=url.loginUrl size="small">
+ ${kcSanitize(msg("backToLogin"))?no_esc}
+ @link.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-update-password.ftl b/conf/keycloak_theme/keywind/login/login-update-password.ftl
new file mode 100644
index 0000000000..ed82380e2f
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-update-password.ftl
@@ -0,0 +1,64 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/checkbox.ftl" as checkbox>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+
+<@layout.registrationLayout
+ displayMessage=!messagesPerField.existsError("password", "password-confirm")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("updatePasswordTitle")}
+ <#elseif section="form">
+ <@form.kw action=url.loginAction method="post">
+
+
+ <@input.kw
+ autocomplete="new-password"
+ autofocus=true
+ invalid=messagesPerField.existsError("password", "password-confirm")
+ label=msg("passwordNew")
+ name="password-new"
+ type="password"
+ />
+ <@input.kw
+ autocomplete="new-password"
+ invalid=messagesPerField.existsError("password-confirm")
+ label=msg("passwordConfirm")
+ message=kcSanitize(messagesPerField.get("password-confirm"))
+ name="password-confirm"
+ type="password"
+ />
+ <#if isAppInitiatedAction??>
+ <@checkbox.kw
+ checked=true
+ label=msg("logoutOtherSessions")
+ name="logout-sessions"
+ value="on"
+ />
+ #if>
+ <@buttonGroup.kw>
+ <#if isAppInitiatedAction??>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ <@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
+ ${msg("doCancel")}
+ @button.kw>
+ <#else>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ #if>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-update-profile.ftl b/conf/keycloak_theme/keywind/login/login-update-profile.ftl
new file mode 100644
index 0000000000..306bad944a
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-update-profile.ftl
@@ -0,0 +1,71 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+
+<@layout.registrationLayout
+ displayMessage=!messagesPerField.existsError("email", "firstName", "lastName", "username")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("loginProfileTitle")}
+ <#elseif section="form">
+ <@form.kw action=url.loginAction method="post">
+ <#if user.editUsernameAllowed>
+ <@input.kw
+ autocomplete="username"
+ autofocus=true
+ invalid=messagesPerField.existsError("username")
+ label=msg("username")
+ message=kcSanitize(messagesPerField.get("username"))
+ name="username"
+ type="text"
+ value=(user.username)!''
+ />
+ #if>
+ <@input.kw
+ autocomplete="email"
+ invalid=messagesPerField.existsError("email")
+ label=msg("email")
+ message=kcSanitize(messagesPerField.get("email"))
+ name="email"
+ type="email"
+ value=(user.email)!''
+ />
+ <@input.kw
+ autocomplete="given-name"
+ invalid=messagesPerField.existsError("firstName")
+ label=msg("firstName")
+ message=kcSanitize(messagesPerField.get("firstName"))
+ name="firstName"
+ type="text"
+ value=(user.firstName)!''
+ />
+ <@input.kw
+ autocomplete="family-name"
+ invalid=messagesPerField.existsError("lastName")
+ label=msg("lastName")
+ message=kcSanitize(messagesPerField.get("lastName"))
+ name="lastName"
+ type="text"
+ value=(user.lastName)!''
+ />
+ <@buttonGroup.kw>
+ <#if isAppInitiatedAction??>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ <@button.kw color="secondary" name="cancel-aia" type="submit" value="true">
+ ${msg("doCancel")}
+ @button.kw>
+ <#else>
+ <@button.kw color="primary" type="submit">
+ ${msg("doSubmit")}
+ @button.kw>
+ #if>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login-username.ftl b/conf/keycloak_theme/keywind/login/login-username.ftl
new file mode 100644
index 0000000000..b8064b2da0
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login-username.ftl
@@ -0,0 +1,71 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/checkbox.ftl" as checkbox>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/link.ftl" as link>
+<#import "components/molecules/identity-provider.ftl" as identityProvider>
+<#import "features/labels/username.ftl" as usernameLabel>
+
+<#assign usernameLabel><@usernameLabel.kw />#assign>
+
+<@layout.registrationLayout
+ displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??
+ displayMessage=!messagesPerField.existsError("username")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("loginAccountTitle")}
+ <#elseif section="form">
+ <#if realm.password>
+ <@form.kw
+ action=url.loginAction
+ method="post"
+ onsubmit="login.disabled = true; return true;"
+ >
+ <#if !usernameHidden??>
+ <@input.kw
+ autocomplete=realm.loginWithEmailAllowed?string("email", "username")
+ autofocus=true
+ disabled=usernameEditDisabled??
+ invalid=messagesPerField.existsError("username")
+ label=usernameLabel
+ message=kcSanitize(messagesPerField.get("username"))?no_esc
+ name="username"
+ type="text"
+ value=(login.username)!''
+ />
+ #if>
+ <#if realm.rememberMe && !usernameHidden??>
+
+ <@checkbox.kw
+ checked=login.rememberMe??
+ label=msg("rememberMe")
+ name="rememberMe"
+ />
+
+ #if>
+ <@buttonGroup.kw>
+ <@button.kw color="primary" name="login" type="submit">
+ ${msg("doLogIn")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+ <#elseif section="info">
+ <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
+
+ ${msg("noAccount")}
+ <@link.kw color="primary" href=url.registrationUrl>
+ ${msg("doRegister")}
+ @link.kw>
+
+ #if>
+ <#elseif section="socialProviders">
+ <#if realm.password && social.providers??>
+ <@identityProvider.kw providers=social.providers />
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/login.ftl b/conf/keycloak_theme/keywind/login/login.ftl
new file mode 100644
index 0000000000..3084138178
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/login.ftl
@@ -0,0 +1,88 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/checkbox.ftl" as checkbox>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/link.ftl" as link>
+<#import "components/molecules/identity-provider.ftl" as identityProvider>
+<#import "features/labels/username.ftl" as usernameLabel>
+
+<#assign usernameLabel><@usernameLabel.kw />#assign>
+
+<@layout.registrationLayout
+ displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??
+ displayMessage=!messagesPerField.existsError("username", "password")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("loginAccountTitle")}
+ <#elseif section="form">
+ <#if realm.password>
+ <@form.kw
+ action=url.loginAction
+ method="post"
+ onsubmit="login.disabled = true; return true;"
+ >
+
+ <@input.kw
+ autocomplete=realm.loginWithEmailAllowed?string("email", "username")
+ autofocus=true
+ disabled=usernameEditDisabled??
+ invalid=messagesPerField.existsError("username", "password")
+ label=usernameLabel
+ message=kcSanitize(messagesPerField.getFirstError("username", "password"))
+ name="username"
+ type="text"
+ value=(login.username)!'username'
+ />
+ <@input.kw
+ invalid=messagesPerField.existsError("username", "password")
+ label=msg("password")
+ name="password"
+ type="password"
+ value=(login.password)!'password'
+ />
+ <#if realm.rememberMe && !usernameEditDisabled?? || realm.resetPasswordAllowed>
+
+ <#if realm.rememberMe && !usernameEditDisabled??>
+ <@checkbox.kw
+ checked=login.rememberMe??
+ label=msg("rememberMe")
+ name="rememberMe"
+ />
+ #if>
+ <#if realm.resetPasswordAllowed>
+ <@link.kw color="primary" href=url.loginResetCredentialsUrl size="small">
+ ${msg("doForgotPassword")}
+ @link.kw>
+ #if>
+
+ #if>
+ <@buttonGroup.kw>
+ <@button.kw color="primary" name="login" type="submit">
+ ${msg("doLogIn")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ #if>
+ <#elseif section="info">
+ <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
+
+ ${msg("noAccount")}
+ <@link.kw color="primary" href=url.registrationUrl>
+ ${msg("doRegister")}
+ @link.kw>
+
+ #if>
+ <#elseif section="socialProviders">
+ <#if realm.password && social.providers??>
+ <@identityProvider.kw providers=social.providers />
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/logout-confirm.ftl b/conf/keycloak_theme/keywind/login/logout-confirm.ftl
new file mode 100644
index 0000000000..e7ec486263
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/logout-confirm.ftl
@@ -0,0 +1,25 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/link.ftl" as link>
+
+<@layout.registrationLayout; section>
+ <#if section="header">
+ ${msg("logoutConfirmTitle")}
+ <#elseif section="form">
+ ${msg("logoutConfirmHeader")}
+ <@form.kw action=url.logoutConfirmAction method="post">
+
+ <@button.kw color="primary" name="confirmLogout" type="submit" value=msg('doLogout')>
+ ${msg("doLogout")}
+ @button.kw>
+ @form.kw>
+ <#if !logoutConfirm.skipLink>
+ <#if (client.baseUrl)?has_content>
+ <@link.kw color="secondary" href=client.baseUrl size="small">
+ ${kcSanitize(msg("backToApplication"))?no_esc}
+ @link.kw>
+ #if>
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/register.ftl b/conf/keycloak_theme/keywind/login/register.ftl
new file mode 100644
index 0000000000..c1a2f061fb
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/register.ftl
@@ -0,0 +1,88 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+<#import "components/atoms/form.ftl" as form>
+<#import "components/atoms/input.ftl" as input>
+<#import "components/atoms/link.ftl" as link>
+
+<@layout.registrationLayout
+ displayMessage=!messagesPerField.existsError("firstName", "lastName", "email", "username", "password", "password-confirm")
+ ;
+ section
+>
+ <#if section="header">
+ ${msg("registerTitle")}
+ <#elseif section="form">
+ <@form.kw action=url.registrationAction method="post">
+ <@input.kw
+ autocomplete="given-name"
+ autofocus=true
+ invalid=messagesPerField.existsError("firstName")
+ label=msg("firstName")
+ message=kcSanitize(messagesPerField.get("firstName"))
+ name="firstName"
+ type="text"
+ value=(register.formData.firstName)!''
+ />
+ <@input.kw
+ autocomplete="family-name"
+ invalid=messagesPerField.existsError("lastName")
+ label=msg("lastName")
+ message=kcSanitize(messagesPerField.get("lastName"))
+ name="lastName"
+ type="text"
+ value=(register.formData.lastName)!''
+ />
+ <@input.kw
+ autocomplete="email"
+ invalid=messagesPerField.existsError("email")
+ label=msg("email")
+ message=kcSanitize(messagesPerField.get("email"))
+ name="email"
+ type="email"
+ value=(register.formData.email)!''
+ />
+ <#if !realm.registrationEmailAsUsername>
+ <@input.kw
+ autocomplete="username"
+ invalid=messagesPerField.existsError("username")
+ label=msg("username")
+ message=kcSanitize(messagesPerField.get("username"))
+ name="username"
+ type="text"
+ value=(register.formData.username)!''
+ />
+ #if>
+ <#if passwordRequired??>
+ <@input.kw
+ autocomplete="new-password"
+ invalid=messagesPerField.existsError("password", "password-confirm")
+ label=msg("password")
+ message=kcSanitize(messagesPerField.getFirstError("password", "password-confirm"))
+ name="password"
+ type="password"
+ />
+ <@input.kw
+ autocomplete="new-password"
+ invalid=messagesPerField.existsError("password-confirm")
+ label=msg("passwordConfirm")
+ message=kcSanitize(messagesPerField.get("password-confirm"))
+ name="password-confirm"
+ type="password"
+ />
+ #if>
+ <#if recaptchaRequired??>
+
+ #if>
+ <@buttonGroup.kw>
+ <@button.kw color="primary" type="submit">
+ ${msg("doRegister")}
+ @button.kw>
+ @buttonGroup.kw>
+ @form.kw>
+ <#elseif section="nav">
+ <@link.kw color="secondary" href=url.loginUrl size="small">
+ ${kcSanitize(msg("backToLogin"))?no_esc}
+ @link.kw>
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/resources/dist/assets/index-a7b84447.js b/conf/keycloak_theme/keywind/login/resources/dist/assets/index-a7b84447.js
new file mode 100644
index 0000000000..c1b2f3c662
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/resources/dist/assets/index-a7b84447.js
@@ -0,0 +1 @@
+var s={};Object.defineProperty(s,"__esModule",{value:!0});function v(e,r,a){var l;if(a===void 0&&(a={}),!r.codes){r.codes={};for(var n=0;n=8&&(t-=8,c[u++]=255&i>>t)}if(t>=r.bits||255&i<<8-t)throw new SyntaxError("Unexpected end of data");return c}function o(e,r,a){a===void 0&&(a={});for(var l=a,n=l.pad,b=n===void 0?!0:n,c=(1<r.bits;)i-=r.bits,t+=r.chars[c&u>>i];if(i&&(t+=r.chars[c&u<Te&&R.splice(t,1)}function $r(){!Me&&!Ce&&(Ce=!0,queueMicrotask(Rr))}function Rr(){Ce=!1,Me=!0;for(let e=0;ee.effect(t,{scheduler:r=>{Ie?Ir(r):r()}}),yt=e.raw}function ct(e){z=e}function Lr(e){let t=()=>{};return[n=>{let i=z(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),Z(i))},i},()=>{t()}]}var xt=[],bt=[],mt=[];function Fr(e){mt.push(e)}function wt(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,bt.push(t))}function Kr(e){xt.push(e)}function Dr(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Et(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var qe=new MutationObserver(Je),We=!1;function Ve(){qe.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),We=!0}function St(){Br(),qe.disconnect(),We=!1}var U=[],Ee=!1;function Br(){U=U.concat(qe.takeRecords()),U.length&&!Ee&&(Ee=!0,queueMicrotask(()=>{kr(),Ee=!1}))}function kr(){Je(U),U.length=0}function x(e){if(!We)return e();St();let t=e();return Ve(),t}var Ue=!1,ae=[];function zr(){Ue=!0}function Hr(){Ue=!1,Je(ae),ae=[]}function Je(e){if(Ue){ae=ae.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,u=e[o].oldValue,c=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},l=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&u===null?c():s.hasAttribute(a)?(l(),c()):l()}i.forEach((o,s)=>{Et(s,o)}),n.forEach((o,s)=>{xt.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(bt.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,mt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function At(e){return ee(K(e))}function X(e,t,r){return e._x_dataStack=[t,...K(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function lt(e,t){let r=e._x_dataStack[0];Object.entries(t).forEach(([n,i])=>{r[n]=i})}function K(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?K(e.host):e.parentNode?K(e.parentNode):[]}function ee(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,u=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...u,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function Ot(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let u=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,u,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,u)})};return r(e)}function Ct(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>qr(n,i),s=>Pe(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let u=n.initialize(o,s,a);return r.initialValue=u,i(o,s,a)}}else r.initialValue=n;return r}}function qr(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function Pe(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),Pe(e[t[0]],t.slice(1),r)}}var Mt={};function S(e,t){Mt[e]=t}function $e(e,t){return Object.entries(Mt).forEach(([r,n])=>{Object.defineProperty(e,`$${r}`,{get(){let[i,o]=Rt(t);return i={interceptor:Ct,...i},wt(t,o),n(t,i)},enumerable:!1})}),e}function Wr(e,t,r,...n){try{return r(...n)}catch(i){Y(i,e,t)}}function Y(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
+
+${r?'Expression: "'+r+`"
+
+`:""}`,t),setTimeout(()=>{throw e},0)}var se=!0;function Vr(e){let t=se;se=!1,e(),se=t}function F(e,t,r={}){let n;return m(e,t)(i=>n=i,r),n}function m(...e){return Tt(...e)}var Tt=It;function Ur(e){Tt=e}function It(e,t){let r={};$e(r,e);let n=[r,...K(e)],i=typeof t=="function"?Jr(n,t):Yr(n,t,e);return Wr.bind(null,e,t,i)}function Jr(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(ee([n,...e]),i);ue(r,o)}}var Se={};function Gr(e,t){if(Se[e])return Se[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(async()=>{ ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return Y(s,t,e),Promise.resolve()}})();return Se[e]=o,o}function Yr(e,t,r){let n=Gr(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=ee([o,...e]);if(typeof n=="function"){let u=n(n,a).catch(c=>Y(c,r,t));n.finished?(ue(i,n.result,a,s,r),n.result=void 0):u.then(c=>{ue(i,c,a,s,r)}).catch(c=>Y(c,r,t)).finally(()=>n.result=void 0)}}}function ue(e,t,r,n,i){if(se&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>ue(e,s,r,n)).catch(s=>Y(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var Ge="x-";function H(e=""){return Ge+e}function Qr(e){Ge=e}var Re={};function g(e,t){return Re[e]=t,{before(r){if(!Re[r]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}const n=$.indexOf(r);$.splice(n>=0?n:$.indexOf("DEFAULT"),0,e)}}}function Ye(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,u])=>({name:a,value:u})),s=Pt(o);o=o.map(a=>s.find(u=>u.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(Lt((o,s)=>n[o]=s)).filter(Kt).map(en(n,r)).sort(tn).map(o=>Xr(e,o))}function Pt(e){return Array.from(e).map(Lt()).filter(t=>!Kt(t))}var je=!1,V=new Map,$t=Symbol();function Zr(e){je=!0;let t=Symbol();$t=t,V.set(t,[]);let r=()=>{for(;V.get(t).length;)V.get(t).shift()();V.delete(t)},n=()=>{je=!1,r()};e(r),n()}function Rt(e){let t=[],r=a=>t.push(a),[n,i]=Lr(e);return t.push(i),[{Alpine:re,effect:n,cleanup:r,evaluateLater:m.bind(m,e),evaluate:F.bind(F,e)},()=>t.forEach(a=>a())]}function Xr(e,t){let r=()=>{},n=Re[t.type]||r,[i,o]=Rt(e);Dr(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),je?V.get($t).push(n):n())};return s.runCleanups=o,s}var jt=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Nt=e=>e;function Lt(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=Ft.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var Ft=[];function Qe(e){Ft.push(e)}function Kt({name:e}){return Dt().test(e)}var Dt=()=>new RegExp(`^${Ge}([^:^.]+)\\b`);function en(e,t){return({name:r,value:n})=>{let i=r.match(Dt()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(u=>u.replace(".","")),expression:n,original:a}}}var Ne="DEFAULT",$=["ignore","ref","data","id","bind","init","for","model","modelable","transition","show","if",Ne,"teleport"];function tn(e,t){let r=$.indexOf(e.type)===-1?Ne:e.type,n=$.indexOf(t.type)===-1?Ne:t.type;return $.indexOf(r)-$.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function M(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>M(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)M(n,t),n=n.nextElementSibling}function D(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}function rn(){document.body||D("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `
diff --git a/conf/keycloak_theme/keywind/login/webauthn-error.ftl b/conf/keycloak_theme/keywind/login/webauthn-error.ftl
new file mode 100644
index 0000000000..852d1e3d5f
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/webauthn-error.ftl
@@ -0,0 +1,34 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+
+<@layout.registrationLayout displayMessage=true; section>
+ <#if section="header">
+ ${kcSanitize(msg("webauthn-error-title"))?no_esc}
+ <#elseif section="form">
+
+
+ <@buttonGroup.kw>
+ <@button.kw
+ @click="$refs.executionValueInput.value = '${execution}'; $refs.isSetRetryInput.value = 'retry'; $refs.errorCredentialForm.submit()"
+ color="primary"
+ name="try-again"
+ tabindex="4"
+ type="button"
+ >
+ ${kcSanitize(msg("doTryAgain"))?no_esc}
+ @button.kw>
+ <#if isAppInitiatedAction??>
+
+ #if>
+ @buttonGroup.kw>
+
+ #if>
+@layout.registrationLayout>
diff --git a/conf/keycloak_theme/keywind/login/webauthn-register.ftl b/conf/keycloak_theme/keywind/login/webauthn-register.ftl
new file mode 100644
index 0000000000..57f4dad87b
--- /dev/null
+++ b/conf/keycloak_theme/keywind/login/webauthn-register.ftl
@@ -0,0 +1,54 @@
+<#import "template.ftl" as layout>
+<#import "components/atoms/button.ftl" as button>
+<#import "components/atoms/button-group.ftl" as buttonGroup>
+
+<@layout.registrationLayout script="dist/webAuthnRegister.js"; section>
+ <#if section="title">
+ title
+ <#elseif section="header">
+ ${kcSanitize(msg("webauthn-registration-title"))?no_esc}
+ <#elseif section="form">
+
+
+ <@buttonGroup.kw>
+ <@button.kw @click="registerSecurityKey" color="primary" type="submit">
+ ${msg("doRegister")}
+ @button.kw>
+ <#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
+
+ #if>
+ @buttonGroup.kw>
+
+ #if>
+@layout.registrationLayout>
+
+
diff --git a/conf/sample.env_web-client b/conf/sample.env_web-client
index edadbeb868..55ee5e9563 100644
--- a/conf/sample.env_web-client
+++ b/conf/sample.env_web-client
@@ -5,10 +5,10 @@ NEXT_PUBLIC_DOWNLOAD_APP_IOS=#
NEXT_PUBLIC_DOWNLOAD_APP_ANDROID=#
KEYCLOAK_CLIENT_ID=hasura
KEYCLOAK_CLIENT_SECRET=oMtCPAV7diKpE564SBspgKj4HqlKM4Hy
-AUTH_ISSUER=http://localhost:8088/realms/hasura
+AUTH_ISSUER=http://localhost:8088/realms/$KEYCLOAK_CLIENT_ID
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=my-secret
-END_SESSION_URL=http://localhost:8088/realms/hasura/protocol/openid-connect/logout
-REFRESH_TOKEN_URL=http://localhost:8088/realms/hasura/protocol/openid-connect/token
+END_SESSION_URL=http://localhost:8088/realms/$KEYCLOAK_CLIENT_ID/protocol/openid-connect/logout
+REFRESH_TOKEN_URL=http://localhost:8088/realms/$KEYCLOAK_CLIENT_ID/protocol/openid-connect/token
HASURA_ADMIN_TOKEN=myadminsecretkey
NEXT_PUBLIC_GRAPHQL_ENGINE_URL=localhost:8080
diff --git a/docker-compose.yml b/docker-compose.yml
index 621e26287a..14f571d67f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,6 +17,7 @@ services:
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD-admin}
volumes:
- ./conf/keycloak_conf:/opt/keycloak/data/import
+ - ./conf/keycloak_theme/keywind:/opt/keycloak/themes/keywind
ports:
- "8088:8088"
depends_on:
@@ -60,7 +61,7 @@ services:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
- interval: 5s
+ interval: 10s
timeout: 10s
retries: 30
networks: