Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2014-02-04 10:09:25 -05:00
commit 4e66a3bf81
211 changed files with 5205 additions and 2913 deletions

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>Keycloak</title>
<title>Keycloak Admin Console</title>
<link rel="icon" href="/auth/admin-ui/img/favicon.ico">

View file

@ -47,6 +47,9 @@ module.config([ '$routeProvider', function($routeProvider) {
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'RealmDetailCtrl'
@ -78,6 +81,9 @@ module.config([ '$routeProvider', function($routeProvider) {
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'RealmSocialCtrl'
@ -780,4 +786,10 @@ module.filter('remove', function() {
return out;
};
});
module.filter('capitalize', function() {
return function(input) {
return input.substring(0, 1).toUpperCase() + input.substring(1);
}
});

View file

@ -141,8 +141,9 @@ module.controller('RealmCreateCtrl', function($scope, Current, Realm, $upload, $
});
module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) {
module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
$scope.createRealm = !realm.realm;
$scope.serverInfo = serverInfo;
console.log('RealmDetailCtrl');
@ -259,14 +260,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $ht
module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) {
console.log('RealmRequiredCredentialsCtrl');
$scope.realm = {
id : realm.realm, realm : realm.realm, social : realm.social,
requiredCredentials : realm.requiredCredentials,
requiredApplicationCredentials : realm.requiredApplicationCredentials,
requiredOAuthClientCredentials : realm.requiredOAuthClientCredentials,
registrationAllowed : realm.registrationAllowed,
passwordPolicy: realm.passwordPolicy
};
$scope.realm = realm;
var oldCopy = angular.copy($scope.realm);
@ -274,8 +268,12 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
$scope.policyMessages = PasswordPolicy.policyMessages;
$scope.policy = PasswordPolicy.parse(realm.passwordPolicy);
var oldPolicy = angular.copy($scope.policy);
$scope.addPolicy = function(policy){
if (!$scope.policy) {
$scope.policy = [];
}
$scope.policy.push(policy);
}
@ -298,7 +296,7 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
}, true);
$scope.$watch('policy', function(oldVal, newVal) {
if (oldVal != newVal) {
if (!angular.equals($scope.policy, oldPolicy)) {
$scope.realm.passwordPolicy = PasswordPolicy.toString($scope.policy);
$scope.changed = true;
}
@ -311,14 +309,13 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
$location.url("/realms/" + realm.realm + "/required-credentials");
Notifications.success("Your changes have been saved to the realm.");
oldCopy = angular.copy($scope.realm);
oldPolicy = angular.copy($scope.policy);
});
};
$scope.reset = function() {
$scope.realm = angular.copy(oldCopy);
$scope.policy = PasswordPolicy.parse(oldCopy.passwordPolicy);
console.debug(realm.passwordPolicy);
$scope.policy = angular.copy(oldPolicy);
$scope.changed = false;
};
});
@ -473,97 +470,41 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, appli
});
module.controller('RealmSocialCtrl', function($scope, realm, Realm, $location, Notifications) {
module.controller('RealmSocialCtrl', function($scope, realm, Realm, serverInfo, $location, Notifications) {
console.log('RealmSocialCtrl');
$scope.realm = { id : realm.id, realm : realm.realm, social : realm.social, registrationAllowed : realm.registrationAllowed,
tokenLifespan : realm.tokenLifespan, accessCodeLifespan : realm.accessCodeLifespan };
$scope.realm = angular.copy(realm);
$scope.serverInfo = serverInfo;
if (!realm["socialProviders"]){
$scope.realm["socialProviders"] = {};
} else {
$scope.realm["socialProviders"] = realm.socialProviders;
}
$scope.allProviders = serverInfo.socialProviders;
$scope.configuredProviders = [];
// Hardcoded provider list in form of map providerId:providerName
$scope.allProviders = { google:"Google", facebook:"Facebook", twitter:"Twitter" };
$scope.availableProviders = [];
for (var provider in $scope.allProviders){
$scope.availableProviders.push(provider);
}
$scope.$watch('realm.socialProviders', function(socialProviders) {
$scope.configuredProviders = [];
for (var providerConfig in socialProviders) {
var i = providerConfig.split('.');
if (i.length == 2 && i[1] == 'key') {
$scope.configuredProviders.push(i[0]);
}
}
}, true);
var oldCopy = angular.copy($scope.realm);
$scope.changed = false;
$scope.callbackUrl = $location.absUrl().replace(/\/admin.*/, "/rest/social/callback");
// To get rid of the "undefined" option in the provider select list
// Setting the 1st option from the list (if the list is not empty)
var selectFirstProvider = function(){
if ($scope.unsetProviders.length > 0){
$scope.newProviderId = $scope.unsetProviders[0];
} else {
$scope.newProviderId = null;
}
}
// Fill in configured providers
var initSocial = function() {
// postSaveProviders is used for remembering providers which were already validated after pressing the save button
// thanks to this it's easy to distinguish between newly added fields and those already tried to be saved
$scope.postSaveProviders = [];
$scope.unsetProviders = [];
$scope.configuredProviders = [];
for (var providerConfig in $scope.realm.socialProviders){
// Get the provider ID which is before the '.' (i.e. google in google.key or google.secret)
if ($scope.realm.socialProviders.hasOwnProperty(providerConfig)){
var pId = providerConfig.split('.')[0];
if ($scope.configuredProviders.indexOf(pId) < 0){
$scope.configuredProviders.push(pId);
}
}
$scope.addProvider = function(pId) {
if (!$scope.realm.socialProviders) {
$scope.realm.socialProviders = {};
}
// If no providers are already configured, you can add any of them
if ($scope.configuredProviders.length == 0){
$scope.unsetProviders = $scope.availableProviders.slice(0);
} else {
for (var i = 0; i < $scope.availableProviders.length; i++){
var providerId = $scope.availableProviders[i];
if ($scope.configuredProviders.indexOf(providerId) < 0){
$scope.unsetProviders.push(providerId);
}
}
}
selectFirstProvider();
};
initSocial();
$scope.addProvider = function() {
if ($scope.availableProviders.indexOf($scope.newProviderId) > -1){
$scope.realm.socialProviders[$scope.newProviderId+".key"]="";
$scope.realm.socialProviders[$scope.newProviderId+".secret"]="";
$scope.configuredProviders.push($scope.newProviderId);
$scope.unsetProviders.splice($scope.unsetProviders.indexOf($scope.newProviderId),1);
selectFirstProvider();
}
$scope.realm.socialProviders[pId + ".key"] = "";
$scope.realm.socialProviders[pId + ".secret"] = "";
};
$scope.removeProvider = function(pId) {
delete $scope.realm.socialProviders[pId+".key"];
delete $scope.realm.socialProviders[pId+".secret"];
$scope.configuredProviders.splice($scope.configuredProviders.indexOf(pId),1);
// Removing from postSaveProviders, so the empty fields are not red if the provider is added to the list again
var rId = $scope.postSaveProviders.indexOf(pId);
if (rId > -1){
$scope.postSaveProviders.splice(rId,1)
}
$scope.unsetProviders.push(pId);
};
$scope.$watch('realm', function() {
@ -586,8 +527,6 @@ module.controller('RealmSocialCtrl', function($scope, realm, Realm, $location, N
$scope.reset = function() {
$scope.realm = angular.copy(oldCopy);
$scope.changed = false;
// Initialize lists of configured and unset providers again
initSocial();
};
});

View file

@ -35,6 +35,10 @@ module.factory('RealmListLoader', function(Loader, Realm, $q) {
return Loader.get(Realm);
});
module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) {
return Loader.get(ServerInfo);
});
module.factory('RealmLoader', function(Loader, Realm, $route, $q) {
return Loader.get(Realm, function() {
return {

View file

@ -1,7 +1,7 @@
<div class="header rcue">
<div class="navbar utility">
<div class="navbar-inner clearfix container">
<h1><a href="#/"><strong>Keycloak</strong> Central Login</a></h1>
<h1><a href="#/"><strong>Keycloak</strong> Admin Console</a></h1>
<ul class="nav pull-right" data-ng-hide="auth.loggedIn">
<li><a href="/auth/rest/admin/login?path={{fragment}}">Login</a></li>
<li><a href="/auth/rest/admin/registrations">Register</a></li>

View file

@ -57,8 +57,8 @@
<div class="actions">
<div class="select-rcue">
<select ng-model="selectedPolicy"
ng-options="p.name for p in (allPolicies|remove:policy:'name')"
data-ng-change="addPolicy(selectedPolicy); selectedAllPolicies = null">
ng-options="(p.name|capitalize) for p in (allPolicies|remove:policy:'name')"
data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
<option value="" disabled selected>Add policy...</option>
</select>
</div>
@ -75,7 +75,7 @@
<tr ng-repeat="p in policy">
<td>
<div class="clearfix">
<input class="input-small disabled" type="text" value="{{p.name}}" readonly>
<input class="input-small disabled" type="text" value="{{p.name|capitalize}}" readonly>
</div>
</td>
<td>

View file

@ -66,6 +66,24 @@
<input ng-model="realm.requireSsl" name="requireSsl" id="requireSsl" onoffswitch />
</div>
</fieldset>
<fieldset>
<legend uncollapsed><span class="text">Optional Settings</span></legend>
<div class="form-group">
<label for="loginTheme">Login Theme</label>
<div class="controls">
<select id="loginTheme" name="loginTheme" ng-model="realm.loginTheme" ng-options="t for t in serverInfo.themes.login"></select>
</div>
</div>
<div class="form-group">
<label for="accountTheme">Account Theme</label>
<div class="controls">
<select id="accountTheme" name="accountTheme" ng-model="realm.accountTheme" ng-options="t for t in serverInfo.themes.account"></select>
</div>
</div>
</fieldset>
<div class="form-actions" data-ng-show="createRealm">
<button type="submit" kc-save class="primary" data-ng-show="changed">Save

View file

@ -38,11 +38,10 @@
<div class="actions">
<div class="select-rcue">
<select ng-model="newProviderId"
ng-options="p as allProviders[p] for p in unsetProviders"
placeholder="Please select"></select>
</div>
<div>
<button ng-click="addProvider()" ng-disabled="">Add Provider</button>
ng-options="(p|capitalize) for p in (allProviders|remove:configuredProviders)"
data-ng-change="addProvider(newProviderId); newProviderId = null">
<option value="" disabled selected>Add provider...</option>
</select>
</div>
</div>
</th>
@ -58,7 +57,7 @@
<tr ng-repeat="pId in configuredProviders">
<td>
<div class="clearfix">
<input class="input-small disabled" type="text" value="{{allProviders[pId]}}" readonly>
<input class="input-small disabled" type="text" value="{{pId|capitalize}}" readonly>
</div>
</td>
<td>

View file

@ -41,6 +41,8 @@ public class RealmRepresentation {
protected List<OAuthClientRepresentation> oauthClients;
protected Map<String, String> socialProviders;
protected Map<String, String> smtpServer;
protected String loginTheme;
protected String accountTheme;
public String getSelf() {
return self;
@ -317,4 +319,20 @@ public class RealmRepresentation {
public void setRoles(RolesRepresentation roles) {
this.roles = roles;
}
public String getLoginTheme() {
return loginTheme;
}
public void setLoginTheme(String loginTheme) {
this.loginTheme = loginTheme;
}
public String getAccountTheme() {
return accountTheme;
}
public void setAccountTheme(String accountTheme) {
this.accountTheme = accountTheme;
}
}

View file

@ -0,0 +1,70 @@
package org.keycloak.util;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ProviderLoader<T> implements Iterable<T> {
private ServiceLoader<T> serviceLoader;
public static <T> Iterable<T> load(Class<T> service) {
ServiceLoader<T> providers = ServiceLoader.load(service);
return new ProviderLoader(providers);
}
private ProviderLoader(ServiceLoader<T> serviceLoader) {
this.serviceLoader = serviceLoader;
}
@Override
public Iterator iterator() {
return new ProviderIterator(serviceLoader.iterator());
}
private static class ProviderIterator<T> implements Iterator<T> {
private Iterator<T> itr;
private T next;
private ProviderIterator(Iterator<T> itr) {
this.itr = itr;
setNext();
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public T next() {
T n = next;
setNext();
return n;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private void setNext() {
next = null;
while (itr.hasNext()) {
if (itr.hasNext()) {
T n = itr.next();
if (!System.getProperties().containsKey(n.getClass().getName() + ".disabled")) {
next = n;
return;
}
}
}
}
}
}

View file

@ -35,6 +35,10 @@
<directory>${project.build.directory}/unpacked/deployments</directory>
<outputDirectory>keycloak/standalone/deployments</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.build.directory}/unpacked/examples/themes</directory>
<outputDirectory>keycloak/standalone/configuration/themes</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.build.directory}/unpacked/adapter</directory>
<outputDirectory>keycloak</outputDirectory>

View file

@ -41,5 +41,8 @@
<exclude name="**/*.iml"/>
</fileset>
</copy>
<copy todir="target/examples/themes" overwrite="true">
<fileset dir="../../examples/themes"/>
</copy>
</target>
</project>

View file

@ -13,6 +13,7 @@
<!ENTITY SocialGoogle SYSTEM "modules/social-google.xml">
<!ENTITY SocialTwitter SYSTEM "modules/social-twitter.xml">
<!ENTITY SocialProviderSPI SYSTEM "modules/social-spi.xml">
<!ENTITY Themes SYSTEM "modules/themes.xml">
<!ENTITY Email SYSTEM "modules/email.xml">
]>
@ -77,6 +78,8 @@
&SocialProviderSPI;
</chapter>
&Themes;
<chapter>
<title>Email</title>
<para>

View file

@ -1,11 +1,12 @@
<section id="social-spi">
<title>Social Provider SPI</title>
<para>
Keycloak provides an SPI to make it easy to add additional social providers. This is done by implementing the
<ulink url="https://raw.github.com/keycloak/keycloak/master/social/core/src/main/java/org/keycloak/social/SocialProvider.java">SocialProvider</ulink>
interface and providing a provider configuration file (<literal>META-INF/services/org.keycloak.social.SocialProvider</literal>).
Keycloak provides an SPI to make it easy to add additional social providers. This is done by implementing
<literal>org.keycloak.social.SocialProvider</literal> in <literal>social/core</literal>
and adding a provider configuration file (<literal>META-INF/services/org.keycloak.social.SocialProvider</literal>).
</para>
<para>
A good reference for implementing a Social Provider is the <ulink url="https://github.com/keycloak/keycloak/tree/master/social/google">Google provider</ulink>.
A good reference for implementing a Social Provider is the Google provider which you can find in <literal>social/google</literal>
on GitHub or in the source download.
</para>
</section>

View file

@ -0,0 +1,163 @@
<chapter id="themes">
<title>Themes</title>
<para>
Keycloak provides theme support for login forms and account management. This allows customizing the look
and feel of end-user facing pages so they can be integrated with your brand and applications.
</para>
<section>
<title>Configure theme</title>
<para>
To configure the theme used by a realm open the <literal>Keycloak Admin Console</literal>, select your realm
from the drop-down box in the top left corner. In the <literal>Optional Settings</literal> use the drop-down
boxes for <literal>Login Theme</literal> and <literal>Account Theme</literal> to select the theme used
by login forms and account management pages.
</para>
</section>
<section>
<title>Creating a theme</title>
<para>
There are two types of themes in Keycloak, login and account. Login themes are used to customize the
login forms, while account themes are used to customize account management. A theme consists of:
<itemizedlist>
<listitem><para>FreeMarker templates</para></listitem>
<listitem><para>Stylesheets</para></listitem>
<listitem><para>Scripts</para></listitem>
<listitem><para>Images</para></listitem>
<listitem><para>Message bundles</para></listitem>
<listitem><para>Theme properties</para></listitem>
</itemizedlist>
</para>
<para>
A theme can extend another theme. When extending a theme you can override individual files (templates, stylesheets, etc.).
The recommended way to create a theme is to extend the base theme. The base theme provides templates
and a default message bundle. It should be possible to achieve the customization required by styling these
templates.
</para>
<para>
To create a new theme, create a folder in <literal>.../standalone/configuration/themes/login</literal> or
<literal>.../standalone/configuration/themes/account</literal>. The name of the folder is the name of the theme.
Then create a file <literal>theme.properties</literal> inside the theme folder. The contents of the file should be:
</para>
<programlisting>parent=base</programlisting>
<para>
You have now created your theme. Check that it works by configuring it for a realm. It should look the same
as the base theme as you've not added anything to it yet. The next sections will describe how to modify
the theme.</para>
<section>
<title>Stylesheets</title>
<para>
A theme can have one or more stylesheets, to add a stylesheet create a file inside <literal>resources/css</literal> (for example <literal>resources/css/styles.css</literal>)
inside your theme folder. Then registering it in <literal>theme.properties</literal> by adding:
</para>
<programlisting>styles=css/styles.css</programlisting>
<para>
The <literal>styles</literal> property supports a space separated list so you can add as many
as you want. For example:
</para>
<programlisting>styles=css/styles.css css/more-styles.css</programlisting>
</section>
<section>
<para>
A theme can have one or more scripts, to add a script create a file inside <literal>resources/js</literal> (for example <literal>resources/js/script.js</literal>)
inside your theme folder. Then registering it in <literal>theme.properties</literal> by adding:
</para>
<programlisting>scripts=js/script.js</programlisting>
<para>
The <literal>scripts</literal> property supports a space separated list so you can add as many
as you want. For example:
</para>
<programlisting>scripts=js/script.js js/more-script.js</programlisting>
</section>
<section>
<title>Images</title>
<para>
To make images available to the theme add them to <literal>resources/img</literal>. They can then be used
through stylesheets. For example:
</para>
<programlisting>body {
background-image: url('../img/image.jpg');
}</programlisting>
<para>
Or in templates, for example:
</para>
<programlisting>&lt;img src="${url.resourcesPath}/img/image.jpg"&gt;</programlisting>
</section>
<section>
<title>Messages</title>
<para>
Text in the templates are loaded from message bundles. Currently internationalization isn't supported,
but that will be added in a later release. A theme that extends another theme will inherit all messages
from the parents message bundle, but can override individual messages. For example to replace
<literal>Username</literal> on the login form with <literal>Your Username</literal> create the file
<literal>messages/messages.properties</literal> inside your theme folder and add the following content:
</para>
<programlisting>username=Your Username</programlisting>
</section>
<section>
<title>Templates</title>
<para>
For advanced use-cases where you need to modify the html structure it is also possible to override
one or more of the templates. For example to override the login page create <literal>login.ftl</literal>
inside your theme folder. The base templates all use <literal>template.ftl</literal> to create the
basic structure of the page.
</para>
<para>
The base templates are a good reference if you need to create your own templates, they can be
found inside <literal>forms/common-themes/src/main/resources/theme</literal> on GitHub or in the source
download.
</para>
</section>
</section>
<section>
<title>SPI</title>
<para>
For full control of login forms and account management Keycloak provides a number of SPIs.
</para>
<section>
<title>Theme SPI</title>
<para>
The Theme SPI allows creating different mechanisms to providing themes for the default FreeMarker based
implementations of login forms and account management. To create a theme provider you will need to implement
<literal>org.keycloak.freemarker.ThemeProvider</literal> and <literal>org.keycloak.freemarker.Theme</literal> in
<literal>forms/common-freemarker</literal>.
</para>
<para>
Keycloak comes with two theme providers, one that loads themes from the classpath (used by default themes)
and another that loads themes from a folder (used by custom themes). Looking at these
would be a good place to start to create your own theme provider. You can find them inside
<literal>forms/common-themes</literal> on GitHub or the source download.
</para>
</section>
<section>
<title>Account SPI</title>
<para>
The Account SPI allows implementing the account management pages using whatever web framework or templating
engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProvider</literal>
and <literal>org.keycloak.account.Account</literal> in <literal>forms/account-api</literal>.
</para>
<para>
Keycloaks default account management provider is built on the FreeMarker template engine (<literal>forms/account-freemarker</literal>).
To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-account-freemarker-1.0-alpha-1.jar</literal>
or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProvider</literal>.
</para>
</section>
<section>
<title>Login SPI</title>
<para>
The Login SPI allows implementing the login forms using whatever web framework or templating
engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProvider</literal>
and <literal>org.keycloak.login.LoginForms</literal> in <literal>forms/login-api</literal>.
</para>
<para>
Keycloaks default login forms provider is built on the FreeMarker template engine (<literal>forms/login-freemarker</literal>).
To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-login-freemarker-1.0-alpha-1.jar</literal>
or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider</literal>.
</para>
</section>
</section>
</chapter>

View file

@ -0,0 +1,201 @@
@import url('../../keycloak/lib/zocial/zocial.css');
body {
background-color: #040507;
background-image: url('../img/bkgrnd.jpg');
background-size: cover;
background-repeat: no-repeat;
color: #ccc;
font-family: sans-serif;
}
a {
color: #fff;
text-decoration: none;
}
.content {
position: absolute;
top: 25%;
left: 50%;
width: 550px;
margin-left: -225px;
}
h2 {
position: fixed;
top: 50px;
left: 0;
width: 100%;
text-align: center;
margin: 0 auto;
color: rgba(255, 255, 255, 0.08);
text-shadow: none;
font-size: 80px;
}
div.app-form {
float: left;
width: 350px;
}
div.app-form label {
display: block;
font-size: 16px;
}
div.social-login {
border-left: 1px solid rgba(255, 255, 255, 0.2);
float: right;
width: 150px;
padding: 20px 0 200px 40px;
}
div.info-area {
position: fixed;
bottom: 0;
left: 0;
margin-top: 40px;
background-color: rgba(0, 0, 0, 0.4);
padding: 20px;
width: 100%;
}
div.info-area p {
margin-right: 30px;
display: inline;
text-shadow: none;
}
input[type=text], input[type=password] {
color: #ddd;
font-size: 18px;
margin-bottom: 20px;
background-color: rgba(3,70,114,0.15);
border: 0px solid rgba(0,0,0,0.2);
box-shadow:inset 0 0 2px 2px rgba(0,0,0,0.15);
padding: 10px;
width: 296px;
}
input[type=text]:hover, input[type=password]:hover {
background-color: rgba(3,70,114,0.4);
}
input[type=submit] {
border: none;
background: -webkit-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
background: -moz-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
background: -ms-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
background: -o-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
color: rgba(0,0,0,0.6);
font-size: 14px;
font-weight: bold;
padding: 10px;
margin-top: 20px;
margin-right: 10px;
width: 150px;
}
input[type=submit]:hover {
background-color: rgba(255,255,255,0.8);
}
p.powered {
font-size: 12px;
position: fixed;
top: 10px;
right: 10px;
margin: 0;
padding: 0;
text-shadow: none;
}
p.powered a {
color: #ccc;
text-decoration: none;
}
div.feedback {
box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
position: fixed;
top: 0;
left: 0;
width: 100%;
text-align: center;
}
div.success {
background-color: rgba(155,155,255,0.1);
}
div.warning {
background-color: rgba(255,175,0,0.1);
}
div.error {
background-color: rgba(255,0,0,0.1);
}
div.feedback p {
margin: 0;
padding: 1em;
}
div.rcue-logo {
background-image: url('../img/logo.png');
background-repeat: no-repeat;
height: 500px;
position: absolute;
left: 30px;
top: 30px;
width: 500px;
z-index: -1;
}
div.social-login span {
display: none;
}
div.social-login p {
display: none;
}
div.social-login ul {
list-style: none;
margin: 0;
padding: 0;
}
div.social-login ul li {
margin-bottom: 20px;
}
div.social-login ul li span {
display: inline;
width: 100px;
}
a.zocial {
border: none;
background: -webkit-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
background: -moz-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
background: -ms-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
background: -o-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
color: rgba(0,0,0,0.6);
width: 130px;
text-shadow: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,2 @@
parent=base
styles=css/styles.css

49
forms/account-api/pom.xml Executable file
View file

@ -0,0 +1,49 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-forms</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-account-api</artifactId>
<name>Keycloak Account Management API</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,30 @@
package org.keycloak.account;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface Account {
public Response createResponse(AccountPages page);
public Account setError(String message);
public Account setSuccess(String message);
public Account setWarning(String message);
public Account setUser(UserModel user);
public Account setStatus(Response.Status status);
public Account setRealm(RealmModel realm);
public Account setReferrer(String referrer);
}

View file

@ -0,0 +1,17 @@
package org.keycloak.account;
import java.util.ServiceLoader;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountLoader {
private AccountLoader() {
}
public static AccountProvider load() {
return ServiceLoader.load(AccountProvider.class).iterator().next();
}
}

View file

@ -0,0 +1,10 @@
package org.keycloak.account;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public enum AccountPages {
ACCOUNT, PASSWORD, TOTP;
}

View file

@ -0,0 +1,12 @@
package org.keycloak.account;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AccountProvider {
public Account createAccount(UriInfo uriInfo);
}

View file

@ -0,0 +1,70 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-forms</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-account-freemarker</artifactId>
<name>Keycloak Account Management FreeMarker</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-forms-common-freemarker</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,158 @@
package org.keycloak.account.freemarker;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.account.Account;
import org.keycloak.account.AccountPages;
import org.keycloak.account.freemarker.model.AccountBean;
import org.keycloak.account.freemarker.model.MessageBean;
import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeLoader;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerAccount implements Account {
private static final Logger logger = Logger.getLogger(FreeMarkerAccount.class);
private UserModel user;
private Response.Status status = Response.Status.OK;
private RealmModel realm;
private String referrer;
public static enum MessageType {SUCCESS, WARNING, ERROR}
private UriInfo uriInfo;
private String message;
private MessageType messageType;
public FreeMarkerAccount(UriInfo uriInfo) {
this.uriInfo = uriInfo;
}
@Override
public Response createResponse(AccountPages page) {
Map<String, Object> attributes = new HashMap<String, Object>();
Theme theme;
try {
theme = ThemeLoader.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT);
} catch (FreeMarkerException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
Properties messages;
try {
messages = theme.getMessages();
attributes.put("rb", messages);
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
}
URI baseUri = uriInfo.getBaseUri();
if (message != null) {
attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
}
attributes.put("url", new UrlBean(realm, theme, baseUri, getReferrerUri()));
switch (page) {
case ACCOUNT:
attributes.put("account", new AccountBean(user));
break;
case TOTP:
attributes.put("totp", new TotpBean(user, baseUri));
break;
}
try {
String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
return Response.serverError().build();
}
}
private String getReferrerUri() {
if (referrer != null) {
for (ApplicationModel a : realm.getApplications()) {
if (a.getName().equals(referrer)) {
return a.getBaseUrl();
}
}
}
return null;
}
@Override
public Account setError(String message) {
this.message = message;
this.messageType = MessageType.ERROR;
return this;
}
@Override
public Account setSuccess(String message) {
this.message = message;
this.messageType = MessageType.SUCCESS;
return this;
}
@Override
public Account setWarning(String message) {
this.message = message;
this.messageType = MessageType.WARNING;
return this;
}
@Override
public Account setUser(UserModel user) {
this.user = user;
return this;
}
@Override
public Account setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
@Override
public Account setStatus(Response.Status status) {
this.status = status;
return this;
}
@Override
public Account setReferrer(String referrer) {
this.referrer = referrer;
return this;
}
}

View file

@ -0,0 +1,18 @@
package org.keycloak.account.freemarker;
import org.keycloak.account.Account;
import org.keycloak.account.AccountProvider;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerAccountProvider implements AccountProvider {
@Override
public Account createAccount(UriInfo uriInfo) {
return new FreeMarkerAccount(uriInfo);
}
}

View file

@ -0,0 +1,23 @@
package org.keycloak.account.freemarker;
import org.keycloak.account.AccountPages;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Templates {
public static String getTemplate(AccountPages page) {
switch (page) {
case ACCOUNT:
return "account.ftl";
case PASSWORD:
return "password.ftl";
case TOTP:
return "totp.ftl";
default:
throw new IllegalArgumentException();
}
}
}

View file

@ -0,0 +1,32 @@
package org.keycloak.account.freemarker.model;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountBean {
private UserModel user;
public AccountBean(UserModel user) {
this.user = user;
}
public String getFirstName() {
return user.getFirstName();
}
public String getLastName() {
return user.getLastName();
}
public String getUsername() {
return user.getLoginName();
}
public String getEmail() {
return user.getEmail();
}
}

View file

@ -19,11 +19,9 @@
* 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.forms;
package org.keycloak.account.freemarker.model;
import org.keycloak.services.resources.flows.FormFlows;
import java.util.ResourceBundle;
import org.keycloak.account.freemarker.FreeMarkerAccount;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -32,14 +30,10 @@ public class MessageBean {
private String summary;
private FormFlows.MessageType type;
private FreeMarkerAccount.MessageType type;
public MessageBean(String summary, FormFlows.MessageType type, ResourceBundle rb) {
if (rb.containsKey(summary)) {
this.summary = rb.getString(summary);
} else {
this.summary = summary;
}
public MessageBean(String message, FreeMarkerAccount.MessageType type) {
this.summary = message;
this.type = type;
}
@ -51,16 +45,16 @@ public class MessageBean {
return this.type.toString().toLowerCase();
}
public boolean isSuccess(){
return FormFlows.MessageType.SUCCESS.equals(this.type);
public boolean isSuccess() {
return FreeMarkerAccount.MessageType.SUCCESS.equals(this.type);
}
public boolean isWarning(){
return FormFlows.MessageType.WARNING.equals(this.type);
public boolean isWarning() {
return FreeMarkerAccount.MessageType.WARNING.equals(this.type);
}
public boolean isError(){
return FormFlows.MessageType.ERROR.equals(this.type);
public boolean isError() {
return FreeMarkerAccount.MessageType.ERROR.equals(this.type);
}
}

View file

@ -19,11 +19,13 @@
* 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.forms;
package org.keycloak.account.freemarker.model;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.Base32;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Random;
@ -33,15 +35,14 @@ import java.util.Random;
*/
public class TotpBean {
private UserBean user;
private String totpSecret;
private String totpSecretEncoded;
private boolean enabled;
private String contextUrl;
public TotpBean(UserBean user, String contextUrl) {
this.user = user;
this.contextUrl = contextUrl;
public TotpBean(UserModel user, URI baseUri) {
this.enabled = user.isTotp();
this.contextUrl = baseUri.getPath();
totpSecret = randomString(20);
totpSecretEncoded = Base32.encode(totpSecret.getBytes());
@ -59,7 +60,7 @@ public class TotpBean {
}
public boolean isEnabled() {
return user.getUser().isTotp();
return enabled;
}
public String getTotpSecret() {
@ -79,15 +80,7 @@ public class TotpBean {
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8");
return contextUrl + "/rest/qrcode" + "?size=246x246&contents=" + contents;
}
public UserBean getUser() {
return user;
}
public void setUser(UserBean user) {
this.user = user;
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
}
}

View file

@ -0,0 +1,63 @@
package org.keycloak.account.freemarker.model;
import org.keycloak.freemarker.Theme;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.flows.Urls;
import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UrlBean {
private String realm;
private Theme theme;
private URI baseURI;
private String referrerURI;
public UrlBean(RealmModel realm, Theme theme, URI baseURI, String referrerURI) {
this.realm = realm.getName();
this.theme = theme;
this.baseURI = baseURI;
this.referrerURI = referrerURI;
}
public String getAccessUrl() {
return Urls.accountAccessPage(baseURI, realm).toString();
}
public String getAccountUrl() {
return Urls.accountPage(baseURI, realm).toString();
}
public String getPasswordUrl() {
return Urls.accountPasswordPage(baseURI, realm).toString();
}
public String getSocialUrl() {
return Urls.accountSocialPage(baseURI, realm).toString();
}
public String getTotpUrl() {
return Urls.accountTotpPage(baseURI, realm).toString();
}
public String getTotpRemoveUrl() {
return Urls.accountTotpRemove(baseURI, realm).toString();
}
public String getLogoutUrl() {
return Urls.accountLogout(baseURI, realm).toString();
}
public String getReferrerURI() {
return referrerURI;
}
public String getResourcesPath() {
URI uri = Urls.themeRoot(baseURI);
return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName();
}
}

View file

@ -0,0 +1 @@
org.keycloak.account.freemarker.FreeMarkerAccountProvider

49
forms/common-freemarker/pom.xml Executable file
View file

@ -0,0 +1,49 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-forms</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-forms-common-freemarker</artifactId>
<name>Keycloak Forms Common FreeMarker</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,15 @@
package org.keycloak.freemarker;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerException extends Exception {
public FreeMarkerException(String message) {
super(message);
}
public FreeMarkerException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,53 @@
package org.keycloak.freemarker;
import freemarker.cache.URLTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerUtil {
public static String processTemplate(Object data, String templateName, Theme theme) throws FreeMarkerException {
Writer out = new StringWriter();
Configuration cfg = new Configuration();
try {
cfg.setTemplateLoader(new ThemeTemplateLoader(theme));
Template template = cfg.getTemplate(templateName);
template.process(data, out);
} catch (Exception e) {
throw new FreeMarkerException("Failed to process template " + templateName, e);
}
return out.toString();
}
public static class ThemeTemplateLoader extends URLTemplateLoader {
private Theme theme;
public ThemeTemplateLoader(Theme theme) {
this.theme = theme;
}
@Override
protected URL getURL(String name) {
try {
return theme.getTemplate(name);
} catch (IOException e) {
return null;
}
}
}
}

View file

@ -0,0 +1,33 @@
package org.keycloak.freemarker;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface Theme {
public enum Type { LOGIN, ACCOUNT };
public String getName();
public String getParentName();
public Type getType();
public URL getTemplate(String name) throws IOException;
public InputStream getTemplateAsStream(String name) throws IOException;
public URL getResource(String path) throws IOException;
public InputStream getResourceAsStream(String path) throws IOException;
public Properties getMessages() throws IOException;
public Properties getProperties() throws IOException;
}

View file

@ -0,0 +1,166 @@
package org.keycloak.freemarker;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.util.ProviderLoader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ThemeLoader {
private static final Logger logger = Logger.getLogger(ThemeLoader.class);
public static final String BASE = "base";
public static String DEFAULT = BASE;
public static Theme createTheme(String name, Theme.Type type) throws FreeMarkerException {
if (name == null) {
name = DEFAULT;
}
Iterable<ThemeProvider> providers = ProviderLoader.load(ThemeProvider.class);
Theme theme = findTheme(providers, name, type);
if (theme.getParentName() != null) {
List<Theme> themes = new LinkedList<Theme>();
themes.add(theme);
for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) {
theme = findTheme(providers, parentName, type);
themes.add(theme);
}
return new ExtendingTheme(themes);
} else {
return theme;
}
}
private static Theme findTheme(Iterable<ThemeProvider> providers, String name, Theme.Type type) throws FreeMarkerException {
for (ThemeProvider p : providers) {
if (p.hasTheme(name, type)) {
try {
return p.createTheme(name, type);
} catch (IOException e) {
if (name.equals(BASE)) {
throw new FreeMarkerException("Failed to create " + type.toString().toLowerCase() + " theme", e);
} else {
logger.error("Failed to create " + type.toString().toLowerCase() + " theme", e);
return findTheme(providers, BASE, type);
}
}
}
}
if (name.equals(BASE)) {
throw new FreeMarkerException(type.toString().toLowerCase() + " theme '" + name + "' not found");
} else {
logger.error(type.toString().toLowerCase() + " theme '" + name + "' not found");
return findTheme(providers, BASE, type);
}
}
public static class ExtendingTheme implements Theme {
private List<Theme> themes;
public ExtendingTheme(List<Theme> themes) {
this.themes = themes;
}
@Override
public String getName() {
return themes.get(0).getName();
}
@Override
public String getParentName() {
return themes.get(0).getParentName();
}
@Override
public Type getType() {
return themes.get(0).getType();
}
@Override
public URL getTemplate(String name) throws IOException {
for (Theme t : themes) {
URL template = t.getTemplate(name);
if (template != null) {
return template;
}
}
return null;
}
@Override
public InputStream getTemplateAsStream(String name) throws IOException {
for (Theme t : themes) {
InputStream template = t.getTemplateAsStream(name);
if (template != null) {
return template;
}
}
return null;
}
@Override
public URL getResource(String path) throws IOException {
for (Theme t : themes) {
URL resource = t.getResource(path);
if (resource != null) {
return resource;
}
}
return null;
}
@Override
public InputStream getResourceAsStream(String path) throws IOException {
for (Theme t : themes) {
InputStream resource = t.getResourceAsStream(path);
if (resource != null) {
return resource;
}
}
return null;
}
@Override
public Properties getMessages() throws IOException {
Properties messages = new Properties();
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
Properties m = itr.previous().getMessages();
if (m != null) {
messages.putAll(m);
}
}
return messages;
}
@Override
public Properties getProperties() throws IOException {
Properties properties = new Properties();
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
Properties p = itr.previous().getProperties();
if (p != null) {
properties.putAll(p);
}
}
return properties;
}
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.freemarker;
import java.io.IOException;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ThemeProvider {
public Theme createTheme(String name, Theme.Type type) throws IOException;
public Set<String> nameSet(Theme.Type type);
public boolean hasTheme(String name, Theme.Type type);
}

54
forms/common-themes/pom.xml Executable file
View file

@ -0,0 +1,54 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-forms</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-forms-common-themes</artifactId>
<name>Keycloak Login Default Theme</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-forms-common-freemarker</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,99 @@
package org.keycloak.theme;
import org.keycloak.freemarker.Theme;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClassLoaderTheme implements Theme {
private final String name;
private final String parentName;
private final Type type;
private final String templateRoot;
private final String resourceRoot;
private final String messages;
private final Properties properties;
public ClassLoaderTheme(String name, Type type) throws IOException {
this.name = name;
this.type = type;
String themeRoot = "theme/" + type.toString().toLowerCase() + "/" + name + "/";
this.templateRoot = themeRoot;
this.resourceRoot = themeRoot + "resources/";
this.messages = themeRoot + "messages/messages.properties";
this.properties = new Properties();
URL p = getClass().getClassLoader().getResource(themeRoot + "theme.properties");
if (p != null) {
properties.load(p.openStream());
this.parentName = properties.getProperty("parent");
} else {
this.parentName = null;
}
}
@Override
public String getName() {
return name;
}
@Override
public String getParentName() {
return parentName;
}
@Override
public Type getType() {
return type;
}
@Override
public URL getTemplate(String name) {
return getClass().getClassLoader().getResource(templateRoot + name);
}
@Override
public InputStream getTemplateAsStream(String name) {
return getClass().getClassLoader().getResourceAsStream(templateRoot + name);
}
@Override
public URL getResource(String path) {
return getClass().getClassLoader().getResource(resourceRoot + path);
}
@Override
public InputStream getResourceAsStream(String path) {
return getClass().getClassLoader().getResourceAsStream(resourceRoot + path);
}
@Override
public Properties getMessages() throws IOException {
Properties m = new Properties();
URL url = getClass().getClassLoader().getResource(this.messages);
if (url != null) {
m.load(url.openStream());
}
return m;
}
@Override
public Properties getProperties() {
return properties;
}
}

View file

@ -0,0 +1,55 @@
package org.keycloak.theme;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeLoader;
import org.keycloak.freemarker.ThemeProvider;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultLoginThemeProvider implements ThemeProvider {
public static final String RCUE = "rcue";
public static final String KEYCLOAK = "keycloak";
static {
ThemeLoader.DEFAULT = KEYCLOAK;
}
private static Set<String> defaultThemes = new HashSet<String>();
static {
defaultThemes.add(ThemeLoader.BASE);
defaultThemes.add(RCUE);
defaultThemes.add(KEYCLOAK);
}
@Override
public Theme createTheme(String name, Theme.Type type) throws IOException {
if (hasTheme(name, type)) {
return new ClassLoaderTheme(name, type);
} else {
return null;
}
}
@Override
public Set<String> nameSet(Theme.Type type) {
if (type == Theme.Type.LOGIN || type == Theme.Type.ACCOUNT) {
return defaultThemes;
} else {
return Collections.emptySet();
}
}
@Override
public boolean hasTheme(String name, Theme.Type type) {
return nameSet(type).contains(name);
}
}

View file

@ -0,0 +1,90 @@
package org.keycloak.theme;
import org.keycloak.freemarker.Theme;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FolderTheme implements Theme {
private String parentName;
private File themeDir;
private Type type;
private final Properties properties;
public FolderTheme(File themeDir, Type type) throws IOException {
this.themeDir = themeDir;
this.type = type;
this.properties = new Properties();
File propertiesFile = new File(themeDir, "theme.properties");
if (propertiesFile .isFile()) {
properties.load(new FileInputStream(propertiesFile));
parentName = properties.getProperty("parent");
}
}
@Override
public String getName() {
return themeDir.getName();
}
@Override
public String getParentName() {
return parentName;
}
@Override
public Type getType() {
return type;
}
@Override
public URL getTemplate(String name) throws IOException {
File file = new File(themeDir, name);
return file.isFile() ? file.toURI().toURL() : null;
}
@Override
public InputStream getTemplateAsStream(String name) throws IOException {
URL url = getTemplate(name);
return url != null ? url.openStream() : null;
}
@Override
public URL getResource(String path) throws IOException {
if (File.separatorChar != '/') {
path = path.replace('/', File.separatorChar);
}
File file = new File(themeDir, "/resources/" + path);
return file.isFile() ? file.toURI().toURL() : null;
}
@Override
public InputStream getResourceAsStream(String path) throws IOException {
URL url = getResource(path);
return url != null ? url.openStream() : null;
}
@Override
public Properties getMessages() throws IOException {
Properties m = new Properties();
File file = new File(themeDir, "messages" + File.separator + "messages.properties");
if (file.isFile()) {
m.load(new FileInputStream(file));
}
return m;
}
@Override
public Properties getProperties() {
return properties;
}
}

View file

@ -0,0 +1,72 @@
package org.keycloak.theme;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FolderThemeProvider implements ThemeProvider {
private File rootDir;
public FolderThemeProvider() {
String d = System.getProperty("keycloak.theme.dir");
if (d != null) {
rootDir = new File(d);
}
}
@Override
public Theme createTheme(String name, Theme.Type type) throws IOException {
if (hasTheme(name, type)) {
return new FolderTheme(new File(getTypeDir(type), name), type);
}
return null;
}
@Override
public Set<String> nameSet(Theme.Type type) {
File typeDir = getTypeDir(type);
if (typeDir != null) {
File[] themes = typeDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
Set<String> names = new HashSet<String>();
for (File t : themes) {
names.add(t.getName());
}
return names;
}
return Collections.emptySet();
}
private File getTypeDir(Theme.Type type) {
if (rootDir != null && rootDir.isDirectory()) {
File typeDir = new File(rootDir, type.name().toLowerCase());
if (typeDir.isDirectory()) {
return typeDir;
}
}
return null;
}
@Override
public boolean hasTheme(String name, Theme.Type type) {
File typeDir = getTypeDir(type);
return typeDir != null && new File(typeDir, name).isDirectory();
}
}

View file

@ -0,0 +1,2 @@
org.keycloak.theme.DefaultLoginThemeProvider
org.keycloak.theme.FolderThemeProvider

View file

@ -1,29 +1,27 @@
<#import "template-main.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.mainLayout active='account' bodyClass='user'; section>
<#if section = "header">
<h2 class="pull-left">Edit Account</h2>
Edit Account
<#elseif section = "content">
<p class="subtitle"><span class="required">*</span> Required fields</p>
<form action="${url.accountUrl}" method="post">
<fieldset class="border-top">
<div class="form-group">
<label for="username">${rb.getString('username')}</label>
<input type="text" id="username" name="username" disabled="disabled" value="${user.username!''}"/>
<label for="username">${rb.username}</label>
<input type="text" id="username" name="username" disabled="disabled" value="${account.username!''}"/>
</div>
<div class="form-group">
<label for="email">${rb.getString('email')}</label><span class="required">*</span>
<input type="text" id="email" name="email" autofocus value="${user.email!''}"/>
<label for="email">${rb.email}</label><span class="required">*</span>
<input type="text" id="email" name="email" autofocus value="${account.email!''}"/>
</div>
<div class="form-group">
<label for="lastName">${rb.getString('lastName')}</label><span class="required">*</span>
<input type="text" id="lastName" name="lastName" value="${user.lastName!''}"/>
<label for="lastName">${rb.lastName}</label><span class="required">*</span>
<input type="text" id="lastName" name="lastName" value="${account.lastName!''}"/>
</div>
<div class="form-group">
<label for="firstName">${rb.getString('firstName')}</label><span class="required">*</span>
<input type="text" id="firstName" name="firstName" value="${user.firstName!''}"/>
<label for="firstName">${rb.firstName}</label><span class="required">*</span>
<input type="text" id="firstName" name="firstName" value="${account.firstName!''}"/>
</div>
</fieldset>
<div class="form-actions">
@ -33,5 +31,4 @@
</div>
</form>
</#if>
</@layout.mainLayout>

View file

@ -1,5 +1,5 @@
<#-- TODO: Only a placeholder, implementation needed -->
<#import "template-main.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.mainLayout active='access' bodyClass='access'; section>
<#if section = "header">

View file

@ -1,5 +1,5 @@
<#-- TODO: Only a placeholder, implementation needed -->
<#import "template-main.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.mainLayout active='social' bodyClass='social'; section>
<#if section = "header">

View file

@ -0,0 +1,27 @@
authenticatorCode=One-time-password
email=Email
errorHeader=Error!
firstName=First name
lastName=Last name
password=Password
passwordConfirm=Password confirmation
passwordNew=New Password
successHeader=Success!
username=Username
missingFirstName=Please specify first name
missingLastName=Please specify last name
missingEmail=Please specify email
missingPassword=Please specify password.
notMatchPassword=Passwords don't match
missingTotp=Please specify authenticator code
invalidPasswordExisting=Invalid existing password
invalidPasswordConfirm=Password confirmation doesn't match
invalidTotp=Invalid authenticator code
successTotp=Google authenticator configured.
successTotpRemoved=Google authenticator removed.
accountUpdated=Your account has been updated
accountPasswordUpdated=Your password has been updated

View file

@ -1,23 +1,22 @@
<#import "template-main.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.mainLayout active='password' bodyClass='password'; section>
<#if section = "header">
Change Password
<h2 class="pull-left">Change Password</h2>
<#elseif section="content">
<p class="subtitle">All fields required</p>
<form action="${url.passwordUrl}" method="post">
<fieldset class="border-top">
<div class="form-group">
<label for="password">${rb.getString('password')}</label>
<label for="password">${rb.password}</label>
<input type="password" id="password" name="password" autofocus>
</div>
<div class="form-group">
<label for="password-new">${rb.getString('passwordNew')}</label>
<label for="password-new">${rb.passwordNew}</label>
<input type="password" id="password-new" name="password-new">
</div>
<div class="form-group">
<label for="password-confirm" class="two-lines">${rb.getString('passwordConfirm')}</label>
<label for="password-confirm" class="two-lines">${rb.passwordConfirm}</label>
<input type="password" id="password-confirm" name="password-confirm">
</div>
</fieldset>
@ -27,6 +26,5 @@
<button type="submit">Cancel</button>
</div>
</form>
</#if>
</@layout.mainLayout>

View file

@ -0,0 +1,67 @@
<#macro mainLayout active bodyClass>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Keycloak Account Management</title>
<link rel="icon" href="${url.resourcesPath}/img/favicon.ico">
<#if properties.styles?has_content>
<#list properties.styles?split(' ') as style>
<link href="${url.resourcesPath}/${style}" rel="stylesheet" />
</#list>
</#if>
<#if properties.scripts?has_content>
<#list properties.scripts?split(' ') as script>
<script type="text/javascript" src="${url.resourcesPath}/${script}"></script>
</#list>
</#if>
</head>
<body class="admin-console user ${bodyClass}">
<#if message?has_content>
<div class="feedback-aligner">
<#if message.success>
<div class="feedback success show"><p><strong>${rb.successHeader}</strong> ${message.summary}</p></div>
</#if>
<#if message.error>
<div class="feedback error show"><p><strong>${rb.errorHeader}</strong> ${message.summary}</p></div>
</#if>
</div>
</#if>
<div class="header rcue">
<div class="navbar utility">
<div class="navbar-inner clearfix container">
<h1><a href="#"><strong>Keycloak</strong> Account Management</a></h1>
<ul class="nav pull-right">
<li>
<a href="${url.logoutUrl}">Sign Out</a>
</li>
</ul>
</div>
</div>
</div><!-- End .header -->
<div class="container">
<div class="row">
<div class="bs-sidebar col-md-3 clearfix">
<ul>
<li class="<#if active=='account'>active</#if>"><a href="${url.accountUrl}">Account</a></li>
<li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li>
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li>
<#--<li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social Accounts</a></li>-->
<#--<li class="<#if active=='access'>active</#if>"><a href="${url.accessUrl}">Authorized Access</a></li>-->
</ul>
</div>
<div id="content-area" class="col-md-9" role="main">
<div id="content">
<#nested "content">
</div>
</div>
<div id="container-right-bg"></div>
</div>
</div>
</body>
</html>
</#macro>

View file

@ -1,19 +1,15 @@
<#import "template-main.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.mainLayout active='totp' bodyClass='totp'; section>
<#if section = "title">
Google Authenticator
<#elseif section = "header">
<h2 class="pull-left">
<#if totp.enabled>
<h2>Authenticators</h2>
Authenticators
<#else>
<h2>Google Authenticator Setup</h2>
Google Authenticator Setup
</#if>
</h2>
<#elseif section = "content">
<#if totp.enabled>
<#if totp.enabled>
<form>
<fieldset>
<p class="info">You have the following authenticators set up:</p>
@ -33,11 +29,13 @@
you will have to reconfigure it immediately or on the next login.
</p>
</fieldset>
<div class="form-actions">
<#if url.referrerURI??><a href="${url.referrerURI}">Back to application</a></#if>
</div>
</form>
<#else>
<#else>
<ol>
<li>
<li class="clearfix">
<p><strong>1</strong>Download the <a href="http://code.google.com/p/google-authenticator/" target="_blank">Google Authenticator app</a> in your device.</p>
</li>
<li class="clearfix">
@ -49,7 +47,7 @@
<p><strong>3</strong>Enter the one-time-password provided by Google Authenticator below and click Submit to finish the setup.</p>
<form action="${url.totpUrl}" method="post">
<div class="form-group">
<label for="totp">${rb.getString('authenticatorCode')}</label>
<label for="totp">${rb.authenticatorCode}</label>
<input type="text" id="totp" name="totp" />
<input type="hidden" id="totpSecret" name="totpSecret" value="${totp.totpSecret}" />
</div>
@ -60,7 +58,6 @@
</form>
</li>
</ol>
</#if>
</#if>
</@layout.mainLayout>

View file

@ -0,0 +1,13 @@
@IMPORT url("../../rcue/css/styles.css");
.header.rcue {
border-top: none !important;
}
.header.rcue .navbar.utility {
background: #083556 !important;
}
.header.rcue .navbar-inner {
background: #083556 !important;
}

View file

@ -0,0 +1,2 @@
parent=rcue
styles=../rcue/css/styles.css css/styles.css

View file

@ -0,0 +1,14 @@
@IMPORT url("reset.css");
@IMPORT url("../lib/bootstrap/css/bootstrap.css");
@IMPORT url("sprites.css");
@IMPORT url("http://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic");
@IMPORT url("base.css");
@IMPORT url("forms.css");
@IMPORT url("header.css");
@IMPORT url("icons.css");
@IMPORT url("tables.css");
@IMPORT url("admin-console.css");

View file

@ -0,0 +1,2 @@
parent=base
styles=css/styles.css

View file

@ -1,6 +1,6 @@
<#-- TODO: Only a placeholder, implementation needed -->
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="reset" isErrorPage=true; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
We're sorry...

View file

@ -1,5 +1,5 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="totp"; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
Google Authenticator Setup

View file

@ -1,6 +1,6 @@
<#-- TODO: Only a placeholder, implementation needed -->
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="reset oauth"; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass="oauth"; section>
<#if section = "title">
OAuth Grant
@ -11,7 +11,7 @@
<#elseif section = "form">
<div class="content-area">
<p class="instruction"><strong>${oauth.client.loginName}</strong> requests access to:</p>
<p class="instruction"><strong>${oauth.client}</strong> requests access to:</p>
<ul>
<#list oauth.realmRolesRequested as role>
<li>
@ -30,8 +30,8 @@
</ul>
<p class="terms">Keycloak Central Login and Google will use this information in accordance with their respective terms of service and privacy policies.</p>
<form class="form-actions" action="${oauth.action}" method="POST">
<input type="hidden" name="code" value="${oauth.oAuthCode}">
<form class="form-actions" action="${url.oauthAction}" method="POST">
<input type="hidden" name="code" value="${oauth.code}">
<input type="submit" class="btn-primary primary" name="accept" value="Accept">
<input type="submit" class="btn-secondary" name="cancel" value="Cancel">
</form>

View file

@ -1,20 +1,20 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="reset" isSeparator=true forceSeparator=true; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
${rb.getString('emailForgotHeader')}
${rb.emailForgotHeader}
<#elseif section = "header">
${rb.getString('emailForgotHeader')}
${rb.emailForgotHeader}
<#elseif section = "form">
<div id="form">
<p class="instruction">${rb.getString('emailInstruction')}</p>
<p class="instruction">${rb.emailInstruction}</p>
<form action="${url.loginPasswordResetUrl}" method="post">
<div>
<label for="email">${rb.getString('email')}</label><input type="text" id="email" name="email" />
<label for="email">${rb.email}</label><input type="text" id="email" name="email" />
</div>
<input class="btn-primary" type="submit" value="Submit" />
</form>

View file

@ -1,4 +1,4 @@
<#import "template-login-action.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
@ -16,7 +16,7 @@
<input id="password" name="password" value="${login.password!''}" type="hidden" />
<div>
<label for="totp">${rb.getString('authenticatorCode')}</label><input id="totp" name="totp" type="text" />
<label for="totp">${rb.authenticatorCode}</label><input id="totp" name="totp" type="text" />
</div>
<div class="aside-btn">
@ -30,7 +30,7 @@
<#elseif section = "info">
<#if realm.registrationAllowed>
<p>${rb.getString('noAccount')} <a href="${url.registrationUrl}">${rb.getString('register')}</a>.</p>
<p>${rb.noAccount} <a href="${url.registrationUrl}">${rb.register}</a>.</p>
</#if>
</#if>

View file

@ -1,22 +1,22 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="reset" isSeparator=false forceSeparator=true; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
${rb.getString('emailUpdateHeader')}
${rb.emailUpdateHeader}
<#elseif section = "header">
${rb.getString('emailUpdateHeader')}
${rb.emailUpdateHeader}
<#elseif section = "form">
<div id="form">
<form action="${url.loginUpdatePasswordUrl}" method="post">
<div>
<label for="password-new">${rb.getString('passwordNew')}</label><input type="password" id="password-new" name="password-new" />
<label for="password-new">${rb.passwordNew}</label><input type="password" id="password-new" name="password-new" />
</div>
<div>
<label for="password-confirm" class="two-lines">${rb.getString('passwordConfirm')}</label><input type="password" id="password-confirm" name="password-confirm" />
<label for="password-confirm" class="two-lines">${rb.passwordConfirm}</label><input type="password" id="password-confirm" name="password-confirm" />
</div>
<input class="btn-primary" type="submit" value="Submit" />

View file

@ -1,5 +1,5 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="" isSeparator=false forceSeparator=true; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
Update Account Information
@ -21,13 +21,13 @@
</div>
<p class="subtitle">All fields required</p>
<div>
<label for="email">${rb.getString('email')}</label><input type="text" id="email" name="email" value="${user.email!''}" />
<label for="email">${rb.email}</label><input type="text" id="email" name="email" value="${user.email!''}" />
</div>
<div>
<label for="firstName">${rb.getString('firstName')}</label><input type="text" id="firstName" name="firstName" value="${user.firstName!''}" />
<label for="firstName">${rb.firstName}</label><input type="text" id="firstName" name="firstName" value="${user.firstName!''}" />
</div>
<div>
<label for="lastName">${rb.getString('lastName')}</label><input type="text" id="lastName" name="lastName" value="${user.lastName!''}" />
<label for="lastName">${rb.lastName}</label><input type="text" id="lastName" name="lastName" value="${user.lastName!''}" />
</div>
<input class="btn-primary" type="submit" value="Submit" />

View file

@ -1,20 +1,20 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="reset" isSeparator=true forceSeparator=true; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
${rb.getString('emailUsernameForgotHeader')}
${rb.emailUsernameForgotHeader}
<#elseif section = "header">
${rb.getString('emailUsernameForgotHeader')}
${rb.emailUsernameForgotHeader}
<#elseif section = "form">
<div id="form">
<p class="instruction">${rb.getString('emailUsernameInstruction')}</p>
<p class="instruction">${rb.emailUsernameInstruction}</p>
<form action="${url.loginUsernameReminderUrl}" method="post">
<div>
<label for="email">${rb.getString('email')}</label><input type="text" id="email" name="email" />
<label for="email">${rb.email}</label><input type="text" id="email" name="email" />
</div>
<input class="btn-primary" type="submit" value="Submit" />
</form>

View file

@ -1,5 +1,5 @@
<#import "template-login-action.ftl" as layout>
<@layout.registrationLayout bodyClass="email" isSeparator=false forceSeparator=true; section>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass="email"; section>
<#if section = "title">
Email verification

View file

@ -1,4 +1,4 @@
<#import "template-login.ftl" as layout>
<#import "template.ftl" as layout>
<@layout.registrationLayout bodyClass=""; section>
<#if section = "title">
@ -13,11 +13,11 @@
<div id="form">
<form action="${url.loginAction}" method="post">
<div>
<label for="username">${rb.getString('username')}</label><input id="username" name="username" value="${login.username!''}" type="text" autofocus />
<label for="username">${rb.username}</label><input id="username" name="username" value="${login.username!''}" type="text" autofocus />
</div>
<div>
<label for="password">${rb.getString('password')}</label><input id="password" name="password" type="password" />
<label for="password">${rb.password}</label><input id="password" name="password" type="password" />
</div>
<input class="btn-primary" name="login" type="submit" value="Log In"/>
@ -29,7 +29,7 @@
<div id="info">
<#if realm.registrationAllowed>
<p>${rb.getString('noAccount')} <a href="${url.registrationUrl}">${rb.getString('register')}</a>.</p>
<p>${rb.noAccount} <a href="${url.registrationUrl}">${rb.register}</a>.</p>
</#if>
<#if realm.resetPasswordAllowed>
<p>Forgot <a href="${url.loginUsernameReminderUrl}">Username</a> / <a href="${url.loginPasswordResetUrl}">Password</a>?</p>

Some files were not shown because too many files have changed in this diff Show more