From b5a4d010baf4a4ae8739f40d062a93ac4eaf9e58 Mon Sep 17 00:00:00 2001 From: vrockai Date: Fri, 20 Sep 2013 15:15:16 +0200 Subject: [PATCH] KEYCLOAK-60 Replace JSF with FreeMarker template engine --- forms/pom.xml | 4 + .../java/org/keycloak/forms/ErrorBean.java | 20 +- .../java/org/keycloak/forms/LoginBean.java | 20 +- .../java/org/keycloak/forms/RealmBean.java | 21 +- .../java/org/keycloak/forms/RegisterBean.java | 22 +- .../java/org/keycloak/forms/SocialBean.java | 18 +- .../java/org/keycloak/forms/TemplateBean.java | 18 +- .../java/org/keycloak/forms/TotpBean.java | 24 +- .../main/java/org/keycloak/forms/UrlBean.java | 44 +-- .../java/org/keycloak/forms/UserBean.java | 17 +- .../org/keycloak/service/FormServiceImpl.java | 250 ++++++++++++++++++ .../main/resources/META-INF/faces-config.xml | 13 - .../META-INF/resources/forms/access.ftl | 1 + .../META-INF/resources/forms/access.xhtml | 2 - .../META-INF/resources/forms/account.ftl | 1 + .../META-INF/resources/forms/account.xhtml | 2 - .../META-INF/resources/forms/error.ftl | 1 + .../META-INF/resources/forms/login-totp.ftl | 1 + .../META-INF/resources/forms/login-totp.xhtml | 2 - .../META-INF/resources/forms/login.ftl | 1 + .../META-INF/resources/forms/login.xhtml | 2 - .../META-INF/resources/forms/password.ftl | 1 + .../META-INF/resources/forms/password.xhtml | 2 - .../META-INF/resources/forms/register.ftl | 1 + .../META-INF/resources/forms/register.xhtml | 2 - .../META-INF/resources/forms/social.ftl | 1 + .../META-INF/resources/forms/social.xhtml | 2 - .../resources/forms/theme/default/access.ftl | 11 + .../forms/theme/default/access.xhtml | 10 - .../resources/forms/theme/default/account.ftl | 33 +++ .../forms/theme/default/account.xhtml | 30 --- .../forms/theme/default/login-totp.ftl | 38 +++ .../forms/theme/default/login-totp.xhtml | 32 --- .../resources/forms/theme/default/login.ftl | 42 +++ .../resources/forms/theme/default/login.xhtml | 35 --- .../forms/theme/default/password.ftl | 29 ++ .../forms/theme/default/password.xhtml | 26 -- .../forms/theme/default/register.ftl | 49 ++++ .../forms/theme/default/register.xhtml | 47 ---- .../resources/forms/theme/default/social.ftl | 12 + .../forms/theme/default/social.xhtml | 10 - .../forms/theme/default/template-login.ftl | 75 ++++++ .../forms/theme/default/template-login.xhtml | 71 ----- ...{template-main.xhtml => template-main.ftl} | 36 +-- .../resources/forms/theme/default/totp.ftl | 38 +++ .../resources/forms/theme/default/totp.xhtml | 37 --- .../META-INF/resources/forms/totp.ftl | 1 + .../META-INF/resources/forms/totp.xhtml | 2 - .../{verify-email.xhtml => verify-email.ftl} | 0 .../org.keycloak.services.FormService | 1 + .../main/resources/META-INF/web-fragment.xml | 16 -- pom.xml | 5 + .../org/keycloak/services/FormService.java | 125 +++++++++ .../services/resources/flows/FormFlows.java | 53 ++-- .../services/resources/flows/Pages.java | 18 +- 55 files changed, 822 insertions(+), 553 deletions(-) create mode 100644 forms/src/main/java/org/keycloak/service/FormServiceImpl.java delete mode 100644 forms/src/main/resources/META-INF/faces-config.xml create mode 100644 forms/src/main/resources/META-INF/resources/forms/access.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/access.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/account.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/account.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/error.ftl create mode 100644 forms/src/main/resources/META-INF/resources/forms/login-totp.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/login.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/login.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/password.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/password.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/register.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/register.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/social.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/social.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/access.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/access.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/account.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/login.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/password.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/social.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/social.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml rename forms/src/main/resources/META-INF/resources/forms/theme/default/{template-main.xhtml => template-main.ftl} (52%) create mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl delete mode 100755 forms/src/main/resources/META-INF/resources/forms/theme/default/totp.xhtml create mode 100644 forms/src/main/resources/META-INF/resources/forms/totp.ftl delete mode 100644 forms/src/main/resources/META-INF/resources/forms/totp.xhtml rename forms/src/main/resources/META-INF/resources/forms/{verify-email.xhtml => verify-email.ftl} (100%) create mode 100644 forms/src/main/resources/META-INF/services/org.keycloak.services.FormService delete mode 100644 forms/src/main/resources/META-INF/web-fragment.xml create mode 100644 services/src/main/java/org/keycloak/services/FormService.java diff --git a/forms/pom.xml b/forms/pom.xml index 83387660fe..0ad29576ee 100755 --- a/forms/pom.xml +++ b/forms/pom.xml @@ -56,6 +56,10 @@ com.google.zxing javase + + org.freemarker + freemarker + diff --git a/forms/src/main/java/org/keycloak/forms/ErrorBean.java b/forms/src/main/java/org/keycloak/forms/ErrorBean.java index d184125233..ff140bdcf3 100644 --- a/forms/src/main/java/org/keycloak/forms/ErrorBean.java +++ b/forms/src/main/java/org/keycloak/forms/ErrorBean.java @@ -21,33 +21,19 @@ */ package org.keycloak.forms; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; -import javax.servlet.http.HttpServletRequest; - -import org.keycloak.services.resources.flows.FormFlows; - /** * @author Stian Thorgersen */ -@ManagedBean(name = "error") -@RequestScoped public class ErrorBean { private String summary; - @PostConstruct - public void init() { - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); - - summary = (String) request.getAttribute(FormFlows.ERROR_MESSAGE); + public ErrorBean(String summary) { + this.summary = summary; } public String getSummary() { return summary; } -} +} \ No newline at end of file diff --git a/forms/src/main/java/org/keycloak/forms/LoginBean.java b/forms/src/main/java/org/keycloak/forms/LoginBean.java index a0288862f6..5777e91d1b 100644 --- a/forms/src/main/java/org/keycloak/forms/LoginBean.java +++ b/forms/src/main/java/org/keycloak/forms/LoginBean.java @@ -24,25 +24,15 @@ package org.keycloak.forms; import java.util.LinkedList; import java.util.List; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ManagedProperty; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; -import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.MultivaluedMap; import org.keycloak.forms.model.RequiredCredential; -import org.keycloak.services.resources.flows.FormFlows; /** * @author Stian Thorgersen */ -@ManagedBean(name = "login") -@RequestScoped public class LoginBean { - @ManagedProperty(value = "#{realm}") private RealmBean realm; private String username; @@ -51,13 +41,10 @@ public class LoginBean { private List requiredCredentials; - @PostConstruct - public void init() { - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); + public LoginBean(RealmBean realm, MultivaluedMap formData){ + + this.realm = realm; - @SuppressWarnings("unchecked") - MultivaluedMap formData = (MultivaluedMap) request.getAttribute(FormFlows.DATA); if (formData != null) { username = formData.getFirst("username"); password = formData.getFirst("password"); @@ -69,6 +56,7 @@ public class LoginBean { requiredCredentials.add(new RequiredCredential(c.getType(), c.isSecret(), c.getFormLabel())); } } + } public String getUsername() { diff --git a/forms/src/main/java/org/keycloak/forms/RealmBean.java b/forms/src/main/java/org/keycloak/forms/RealmBean.java index 8b1dcefc2f..e799b7ac7e 100644 --- a/forms/src/main/java/org/keycloak/forms/RealmBean.java +++ b/forms/src/main/java/org/keycloak/forms/RealmBean.java @@ -21,34 +21,21 @@ */ package org.keycloak.forms; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; -import javax.servlet.http.HttpServletRequest; - import org.keycloak.services.models.RealmModel; -import org.keycloak.services.resources.flows.FormFlows; /** * @author Stian Thorgersen */ -@ManagedBean(name = "realm") -@RequestScoped public class RealmBean { private RealmModel realm; private boolean saas; - @PostConstruct - public void init() { - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); - realm = (RealmModel) request.getAttribute(FormFlows.REALM); - - saas = RealmModel.DEFAULT_REALM.equals(realm.getName()); + public RealmBean(RealmModel realmModel) { + realm = realmModel; + saas = RealmModel.DEFAULT_REALM.equals(realmModel.getName()); } public String getId() { @@ -59,7 +46,7 @@ public class RealmBean { return saas ? "Keycloak" : realm.getName(); } - RealmModel getRealm() { + public RealmModel getRealm() { return realm; } diff --git a/forms/src/main/java/org/keycloak/forms/RegisterBean.java b/forms/src/main/java/org/keycloak/forms/RegisterBean.java index 77d6a8cbe6..cb8c83d308 100755 --- a/forms/src/main/java/org/keycloak/forms/RegisterBean.java +++ b/forms/src/main/java/org/keycloak/forms/RegisterBean.java @@ -24,39 +24,23 @@ package org.keycloak.forms; import java.util.HashMap; import java.util.Map; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ManagedProperty; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; -import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.MultivaluedMap; -import org.keycloak.services.resources.flows.FormFlows; - /** * @author Stian Thorgersen */ -@ManagedBean(name = "register") -@RequestScoped public class RegisterBean { - private HashMap formData; + private Map formData = new HashMap(); private boolean socialRegistration; - @PostConstruct - public void init() { - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); + public RegisterBean(MultivaluedMap formData, boolean socialRegistration) { this.formData = new HashMap(); - Boolean socialRegistrationAttr = (Boolean)request.getAttribute(FormFlows.SOCIAL_REGISTRATION); - this.socialRegistration = socialRegistrationAttr != null && socialRegistrationAttr; + this.socialRegistration = socialRegistration; - @SuppressWarnings("unchecked") - MultivaluedMap formData = (MultivaluedMap) request.getAttribute(FormFlows.DATA); if (formData != null) { for (String k : formData.keySet()) { this.formData.put(k, formData.getFirst(k)); diff --git a/forms/src/main/java/org/keycloak/forms/SocialBean.java b/forms/src/main/java/org/keycloak/forms/SocialBean.java index fab91b165e..2492ca96bb 100644 --- a/forms/src/main/java/org/keycloak/forms/SocialBean.java +++ b/forms/src/main/java/org/keycloak/forms/SocialBean.java @@ -26,10 +26,6 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ManagedProperty; -import javax.faces.bean.RequestScoped; import javax.imageio.spi.ServiceRegistry; import javax.ws.rs.core.UriBuilder; @@ -39,28 +35,24 @@ import org.keycloak.services.resources.flows.Urls; /** * @author Stian Thorgersen */ -@ManagedBean(name = "social") -@RequestScoped public class SocialBean { - @ManagedProperty(value = "#{realm}") private RealmBean realm; - @ManagedProperty(value = "#{register}") private RegisterBean registerBean; - @ManagedProperty(value = "#{url}") private UrlBean url; private List providers; - private UriBuilder socialLoginUrlBuilder; + public SocialBean(RealmBean realm, RegisterBean registerBean, UrlBean url) { + this.realm = realm; + this.registerBean = registerBean; + this.url = url; - @PostConstruct - public void init() { URI baseURI = url.getBaseURI(); - socialLoginUrlBuilder = UriBuilder.fromUri(Urls.socialRedirectToProviderAuth(baseURI, realm.getId())); + UriBuilder socialLoginUrlBuilder = UriBuilder.fromUri(Urls.socialRedirectToProviderAuth(baseURI, realm.getId())); providers = new LinkedList(); for (Iterator itr = ServiceRegistry diff --git a/forms/src/main/java/org/keycloak/forms/TemplateBean.java b/forms/src/main/java/org/keycloak/forms/TemplateBean.java index 841d5f03a7..724aa0e07a 100644 --- a/forms/src/main/java/org/keycloak/forms/TemplateBean.java +++ b/forms/src/main/java/org/keycloak/forms/TemplateBean.java @@ -24,23 +24,14 @@ package org.keycloak.forms; import java.util.HashMap; import java.util.Map; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ManagedProperty; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; - /** * @author Stian Thorgersen */ -@ManagedBean(name = "template") -@RequestScoped public class TemplateBean { - @ManagedProperty(value = "#{realm}") private RealmBean realm; - private String theme; + private String theme = "default"; private String themeUrl; @@ -48,9 +39,9 @@ public class TemplateBean { private String formsPath; - @PostConstruct - public void init() { - formsPath = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() + "/forms"; + + public TemplateBean(RealmBean realm, String contextPath) { + formsPath = contextPath + "/forms"; // TODO Get theme name from realm theme = "default"; @@ -60,6 +51,7 @@ public class TemplateBean { themeConfig.put("styles", themeUrl + "/styles.css"); + // TODO move this into CSS if (realm.isSaas()) { themeConfig.put("logo", themeUrl + "/img/red-hat-logo.png"); themeConfig.put("background", themeUrl + "/img/login-screen-background.jpg"); diff --git a/forms/src/main/java/org/keycloak/forms/TotpBean.java b/forms/src/main/java/org/keycloak/forms/TotpBean.java index b1445743eb..4ae9cdd3f3 100644 --- a/forms/src/main/java/org/keycloak/forms/TotpBean.java +++ b/forms/src/main/java/org/keycloak/forms/TotpBean.java @@ -25,39 +25,28 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Random; -import javax.annotation.PostConstruct; -import javax.faces.application.FacesMessage; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ManagedProperty; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; - import org.picketlink.common.util.Base32; /** * @author Stian Thorgersen */ -@ManagedBean(name = "totp") -@RequestScoped public class TotpBean { - @ManagedProperty(value = "#{user}") private UserBean user; private String totpSecret; private String totpSecretEncoded; + private String contextUrl; - @PostConstruct - public void init() { - FacesContext facesContext = FacesContext.getCurrentInstance(); - FacesMessage facesMessage = new FacesMessage("This is a message"); - facesContext.addMessage(null, facesMessage); + public TotpBean(UserBean user, String contextUrl) { + this.user = user; + this.contextUrl = contextUrl; totpSecret = randomString(20); totpSecretEncoded = Base32.encode(totpSecret.getBytes()); } - private static final String randomString(int length) { + private static String randomString(int length) { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890"; Random r = new Random(); StringBuilder sb = new StringBuilder(); @@ -89,8 +78,7 @@ public class TotpBean { public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException { String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8"); - String contextPath = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath(); - return contextPath + "/forms/qrcode" + "?size=200x200&contents=" + contents; + return contextUrl + "/forms/qrcode" + "?size=200x200&contents=" + contents; } public UserBean getUser() { diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java index f846ee1907..b6e2ff3016 100644 --- a/forms/src/main/java/org/keycloak/forms/UrlBean.java +++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java @@ -23,46 +23,20 @@ package org.keycloak.forms; import java.net.URI; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ManagedProperty; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.UriBuilder; - -import org.keycloak.services.resources.flows.FormFlows; import org.keycloak.services.resources.flows.Urls; /** * @author Stian Thorgersen */ -@ManagedBean(name = "url") -@RequestScoped public class UrlBean { private URI baseURI; - @ManagedProperty(value = "#{realm}") private RealmBean realm; - @ManagedProperty(value = "#{register}") - private RegisterBean registerBean; - - @PostConstruct - public void init() { - FacesContext ctx = FacesContext.getCurrentInstance(); - - HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); - - UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString()) - .replacePath(request.getContextPath()).path("rest"); - - if (request.getAttribute(FormFlows.CODE) != null) { - b.queryParam("code", request.getAttribute(FormFlows.CODE)); - } - - baseURI = b.build(); + public UrlBean(RealmBean realm, URI baseURI){ + this.realm = realm; + this.baseURI = baseURI; } public RealmBean getRealm() { @@ -73,14 +47,6 @@ public class UrlBean { this.realm = realm; } - public RegisterBean getRegisterBean() { - return registerBean; - } - - public void setRegisterBean(RegisterBean registerBean) { - this.registerBean = registerBean; - } - public String getAccessUrl() { return Urls.accountAccessPage(baseURI, realm.getId()).toString(); } @@ -115,10 +81,7 @@ public class UrlBean { public String getRegistrationAction() { if (realm.isSaas()) { - // TODO: saas social registration return Urls.saasRegisterAction(baseURI).toString(); - } else if (registerBean.isSocialRegistration()) { - return Urls.socialRegisterAction(baseURI, realm.getId()).toString(); } else { return Urls.realmRegisterAction(baseURI, realm.getId()).toString(); } @@ -126,6 +89,7 @@ public class UrlBean { public String getRegistrationUrl() { if (realm.isSaas()) { + // TODO: saas social registration return Urls.saasRegisterPage(baseURI).toString(); } else { return Urls.realmRegisterPage(baseURI, realm.getId()).toString(); diff --git a/forms/src/main/java/org/keycloak/forms/UserBean.java b/forms/src/main/java/org/keycloak/forms/UserBean.java index 0be17b3c5d..97fe839dd8 100644 --- a/forms/src/main/java/org/keycloak/forms/UserBean.java +++ b/forms/src/main/java/org/keycloak/forms/UserBean.java @@ -21,30 +21,17 @@ */ package org.keycloak.forms; -import javax.annotation.PostConstruct; -import javax.faces.bean.ManagedBean; -import javax.faces.bean.RequestScoped; -import javax.faces.context.FacesContext; -import javax.servlet.http.HttpServletRequest; - import org.keycloak.services.models.UserModel; -import org.keycloak.services.resources.flows.FormFlows; /** * @author Stian Thorgersen */ -@ManagedBean(name = "user") -@RequestScoped public class UserBean { private UserModel user; - @PostConstruct - public void init() { - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); - - user = (UserModel) request.getAttribute(FormFlows.USER); + public UserBean(UserModel user){ + this.user = user; } public String getFirstName() { diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java new file mode 100644 index 0000000000..41482ad556 --- /dev/null +++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java @@ -0,0 +1,250 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.service; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import org.jboss.resteasy.logging.Logger; +import org.keycloak.forms.ErrorBean; +import org.keycloak.forms.LoginBean; +import org.keycloak.forms.RealmBean; +import org.keycloak.forms.RegisterBean; +import org.keycloak.forms.SocialBean; +import org.keycloak.forms.TemplateBean; +import org.keycloak.forms.TotpBean; +import org.keycloak.forms.UrlBean; +import org.keycloak.forms.UserBean; +import org.keycloak.services.FormService; +import org.keycloak.services.resources.flows.Pages; + +/** + * @author Viliam Rockai + */ +public class FormServiceImpl implements FormService { + + private static final Logger log = Logger.getLogger(FormServiceImpl.class); + + private static final String ID = "FormServiceId"; + private static final String BUNDLE = "org.keycloak.forms.messages"; + private final Map commandMap = new HashMap(); + + public FormServiceImpl(){ + commandMap.put(Pages.LOGIN, new CommandLogin()); + commandMap.put(Pages.REGISTER, new CommandRegister()); + commandMap.put(Pages.ACCOUNT, new CommandAccount()); + commandMap.put(Pages.PASSWORD, new CommandPassword()); + commandMap.put(Pages.ACCESS, new CommandAccess()); + commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp()); + commandMap.put(Pages.SECURITY_FAILURE, new CommandSecurityFailure()); + commandMap.put(Pages.SOCIAL, new CommandSocial()); + commandMap.put(Pages.TOTP, new CommandTotp()); + commandMap.put(Pages.VERIFY_EMAIL, new CommandEmail()); + } + + public String getId(){ + return ID; + } + + public String process(String pageId, FormServiceDataBean dataBean){ + + Map attributes = new HashMap(); + + RealmBean realm = new RealmBean(dataBean.getRealm()); + attributes.put("template", new TemplateBean(realm, dataBean.getContextPath())); + + ResourceBundle rb = ResourceBundle.getBundle(BUNDLE); + attributes.put("rb", rb); + + if (commandMap.containsKey(pageId)){ + commandMap.get(pageId).exec(attributes, dataBean); + } + + return processFmTemplate(pageId, attributes); + } + + private String processFmTemplate(String temp, Map input) { + + Writer out = new StringWriter(); + Configuration cfg = new Configuration(); + + try { + cfg.setClassForTemplateLoading(FormServiceImpl.class,"/META-INF/resources"); + Template template = cfg.getTemplate(temp); + + template.process(input, out); + } catch (IOException e) { + log.error("Failed to load the template " + temp, e); + } catch (TemplateException e) { + log.error("Failed to process template " + temp, e); + } + + return out.toString(); + } + + private class CommandTotp implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + attributes.put("url", new UrlBean(realm, dataBean.getBaseURI())); + + UserBean user = new UserBean(dataBean.getUserModel()); + attributes.put("user", user); + + TotpBean totp = new TotpBean(user, dataBean.getContextPath()); + attributes.put("totp", totp); + + attributes.put("login", new LoginBean(realm, dataBean.getFormData())); + } + } + + private class CommandSocial implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("url", new UrlBean(realm, dataBean.getBaseURI())); + } + } + + private class CommandEmail implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + } + } + + private class CommandSecurityFailure implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + } + } + + private class CommandPassword implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + attributes.put("url", new UrlBean(realm, dataBean.getBaseURI())); + attributes.put("user", new UserBean(dataBean.getUserModel())); + attributes.put("login", new LoginBean(realm, dataBean.getFormData())); + } + } + + private class CommandLoginTotp implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + if (dataBean.getError() != null){ + attributes.put("error", new ErrorBean(dataBean.getError())); + } + + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + + UrlBean url = new UrlBean(realm, dataBean.getBaseURI()); + + attributes.put("url", url); + attributes.put("user", new UserBean(dataBean.getUserModel())); + attributes.put("login", new LoginBean(realm, dataBean.getFormData())); + + RegisterBean register = new RegisterBean(dataBean.getFormData(), dataBean.getSocialRegistration()); + + SocialBean social = new SocialBean(realm, register, url); + attributes.put("social", social); + } + } + + private class CommandAccess implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + attributes.put("url", new UrlBean(realm, dataBean.getBaseURI())); + } + } + + private class CommandAccount implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + attributes.put("url", new UrlBean(realm, dataBean.getBaseURI())); + attributes.put("user", new UserBean(dataBean.getUserModel())); + attributes.put("login", new LoginBean(realm, dataBean.getFormData())); + } + } + + private class CommandLogin implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + if (dataBean.getError() != null){ + attributes.put("error", new ErrorBean(dataBean.getError())); + } + + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + + UrlBean url = new UrlBean(realm, dataBean.getBaseURI()); + + attributes.put("url", url); + attributes.put("user", new UserBean(dataBean.getUserModel())); + attributes.put("login", new LoginBean(realm, dataBean.getFormData())); + + RegisterBean register = new RegisterBean(dataBean.getFormData(), dataBean.getSocialRegistration()); + + SocialBean social = new SocialBean(realm, register, url); + attributes.put("social", social); + } + } + + private class CommandRegister implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + if (dataBean.getError() != null){ + attributes.put("error", new ErrorBean(dataBean.getError())); + } + + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + + UrlBean url = new UrlBean(realm, dataBean.getBaseURI()); + + attributes.put("url", url); + attributes.put("user", new UserBean(dataBean.getUserModel())); + + RegisterBean register = new RegisterBean(dataBean.getFormData(), dataBean.getSocialRegistration()); + attributes.put("register", register); + + SocialBean social = new SocialBean(realm, register, url); + attributes.put("social", social); + } + } + + private interface Command { + public void exec(Map attributes, FormServiceDataBean dataBean); + } + +} \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/faces-config.xml b/forms/src/main/resources/META-INF/faces-config.xml deleted file mode 100644 index 42cc657718..0000000000 --- a/forms/src/main/resources/META-INF/faces-config.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - org.keycloak.forms.messages - messages - - - - diff --git a/forms/src/main/resources/META-INF/resources/forms/access.ftl b/forms/src/main/resources/META-INF/resources/forms/access.ftl new file mode 100644 index 0000000000..bcea82a815 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/access.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/access.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/access.xhtml b/forms/src/main/resources/META-INF/resources/forms/access.xhtml deleted file mode 100644 index 2f39431dd2..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/access.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/account.ftl b/forms/src/main/resources/META-INF/resources/forms/account.ftl new file mode 100755 index 0000000000..5c41000a07 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/account.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/account.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/account.xhtml b/forms/src/main/resources/META-INF/resources/forms/account.xhtml deleted file mode 100644 index 2b1fb28a61..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/account.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/error.ftl b/forms/src/main/resources/META-INF/resources/forms/error.ftl new file mode 100644 index 0000000000..3dd69c9e8d --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/error.ftl @@ -0,0 +1 @@ +template not found \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/login-totp.ftl new file mode 100644 index 0000000000..9bd8509646 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login-totp.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login-totp.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml b/forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml deleted file mode 100644 index a757b27dbc..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login.ftl b/forms/src/main/resources/META-INF/resources/forms/login.ftl new file mode 100644 index 0000000000..6033383132 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/login.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/login.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/login.xhtml b/forms/src/main/resources/META-INF/resources/forms/login.xhtml deleted file mode 100644 index b72c23ab9b..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/login.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/password.ftl b/forms/src/main/resources/META-INF/resources/forms/password.ftl new file mode 100644 index 0000000000..6fd1a4f9d4 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/password.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/password.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/password.xhtml b/forms/src/main/resources/META-INF/resources/forms/password.xhtml deleted file mode 100644 index e25bd055f0..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/password.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/register.ftl b/forms/src/main/resources/META-INF/resources/forms/register.ftl new file mode 100644 index 0000000000..ff86d85000 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/register.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/register.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/register.xhtml b/forms/src/main/resources/META-INF/resources/forms/register.xhtml deleted file mode 100644 index 4d892e6424..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/register.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/social.ftl b/forms/src/main/resources/META-INF/resources/forms/social.ftl new file mode 100644 index 0000000000..baf129e26e --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/social.ftl @@ -0,0 +1 @@ +<#include "./theme/" + template.theme + "/social.ftl"> \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/social.xhtml b/forms/src/main/resources/META-INF/resources/forms/social.xhtml deleted file mode 100644 index 7d1d4a9dd3..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/social.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/access.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/access.ftl new file mode 100755 index 0000000000..a4b2413bba --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/access.ftl @@ -0,0 +1,11 @@ +<#import "template-main.ftl" as layout> +<@layout.mainLayout ; section> + + <#if section = "header"> + + Authorized Applications + + <#elseif section = "content"> + + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/access.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/access.xhtml deleted file mode 100755 index 9b9d5e04a0..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/access.xhtml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Authorized Applications - - - - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/account.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/account.ftl new file mode 100755 index 0000000000..252437ba08 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/account.ftl @@ -0,0 +1,33 @@ +<#import "template-main.ftl" as layout> +<@layout.mainLayout ; section> + + <#if section = "header"> + + Edit Account + + <#elseif section = "content"> + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml deleted file mode 100755 index 07e0ca4e4f..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml +++ /dev/null @@ -1,30 +0,0 @@ - - - - Edit Account - - -
-
- - -
-
- - -
-
- - -
-
- - -
- - - -
-
-
\ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl new file mode 100755 index 0000000000..5ca1109267 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl @@ -0,0 +1,38 @@ +<#import "template-login.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + + <#if section = "title"> + + Log in to ${realm.name} + + <#elseif section = "header"> + + Log in to ${realm.name} + + <#elseif section = "form"> + +
+ + + +
+ + +
+ +
+ + +
+ + +
+ + <#elseif section = "info"> + + <#if realm.registrationAllowed> +

${rb.getString('noAccount')} ${rb.getString('register')}.

+ + + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml deleted file mode 100755 index e265db2543..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml +++ /dev/null @@ -1,32 +0,0 @@ - - - - Log in to #{realm.name} - Log in to #{realm.name} - - -
- - - -
- - -
- -
- - -
- - -
-
- - - -

#{messages.noAccount} #{messages.register}.

-
-
-
\ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl new file mode 100755 index 0000000000..80994d6646 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl @@ -0,0 +1,42 @@ +<#import "template-login.ftl" as layout> +<@layout.registrationLayout bodyClass=""; section> + <#if section = "title"> + + Log in to ${realm.name} + + <#elseif section = "header"> + + Log in to ${(realm.name)?default('')} + + <#elseif section = "form"> + +
+
+
+ + +
+ + <#list login.requiredCredentials as c> +
+ +
+ + +
+
+ + +
+
+ + <#elseif section = "info" > + +
+ <#if realm.registrationAllowed> +

${rb.getString('noAccount')} ${rb.getString('register')}.

+ +
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.xhtml deleted file mode 100755 index 325cde74c1..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.xhtml +++ /dev/null @@ -1,35 +0,0 @@ - - - - Log in to #{realm.name} - Log in to #{realm.name} - - -
-
- - -
- - -
- -
-
- -
- - -
- - -
-
- - - -

#{messages.noAccount} #{messages.register}.

-
-
-
\ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/password.ftl new file mode 100755 index 0000000000..bbb85696af --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/password.ftl @@ -0,0 +1,29 @@ +<#import "template-main.ftl" as layout> +<@layout.mainLayout ; section> + + <#if section = "header"> + + Change Password + + <#elseif section = "content"> + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml deleted file mode 100755 index 05384b1230..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml +++ /dev/null @@ -1,26 +0,0 @@ - - - - Change Password - - -
-
- - -
-
- - -
-
- - -
- - - -
-
-
\ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl new file mode 100755 index 0000000000..a8f1eedf4b --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl @@ -0,0 +1,49 @@ +<#import "template-login.ftl" as layout> +<@layout.registrationLayout bodyClass="register" ; section> + + <#if section = "title"> + + ${rb.getString('registerWith')} ${realm.name} + + <#elseif section = "header"> + + ${rb.getString('registerWith')} ${realm.name} + + <#elseif section = "form"> + +
+

${rb.getString('allRequired')}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

By registering you agree to the Terms of Service and the Privacy Policy.

+
+ + +
+ + <#elseif section = "info"> + +

${rb.getString('alreadyHaveAccount')} ${rb.getString('logIn')}.

+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml deleted file mode 100755 index 98b21dc2be..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - #{messages.registerWith} #{realm.name} - #{messages.registerWith} #{realm.name} - - -
-

#{messages.allRequired}

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
-

By registering you agree to the Terms of Service and the Privacy Policy.

-
- - -
-
- - - -

#{messages.alreadyHaveAccount} #{messages.logIn}.

-
-
-
\ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/social.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/social.ftl new file mode 100755 index 0000000000..97e51c42ea --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/social.ftl @@ -0,0 +1,12 @@ +<#import "template-main.ftl" as layout> +<@layout.mainLayout ; section> + + <#if section = "header"> + + Social Accounts + + <#elseif section = "content"> + + + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/social.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/social.xhtml deleted file mode 100755 index 9ac9a96567..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/social.xhtml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Social Accounts - - - - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl new file mode 100644 index 0000000000..2ad1c19289 --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl @@ -0,0 +1,75 @@ +<#macro registrationLayout bodyClass> + + + + + + + <#nested "title"> + + + + + + + <#if (template.themeConfig.logo)?has_content> +

+ Logo +

+ + +
+

+ <#nested "header"> +

+ +
+
+
+

Application login area

+ <#nested "form"> +
+ + <#if error?has_content> + + + + <#if social.displaySocialProviders> + + + +
+

Info area

+ <#nested "info"> +
+
+
+ + <#if template.themeConfig['displayPoweredBy']> +

+ ${rb.getString('poweredByKeycloak')} +

+ +
+ + <#nested "content"> + + + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml deleted file mode 100644 index 0a313ec616..0000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - <ui:insert name="title" /> - - - - - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl similarity index 52% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml rename to forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl index acbe73c391..411191a882 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl @@ -1,28 +1,29 @@ +<#macro mainLayout> - + Keycloak Account Management - + - + - +