Merge remote-tracking branch 'upstream/master'
|
@ -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">
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
70
core/src/main/java/org/keycloak/util/ProviderLoader.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -41,5 +41,8 @@
|
|||
<exclude name="**/*.iml"/>
|
||||
</fileset>
|
||||
</copy>
|
||||
<copy todir="target/examples/themes" overwrite="true">
|
||||
<fileset dir="../../examples/themes"/>
|
||||
</copy>
|
||||
</target>
|
||||
</project>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
163
docbook/reference/en/en-US/modules/themes.xml
Normal 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><img src="${url.resourcesPath}/img/image.jpg"></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>
|
201
examples/themes/login/sunrise/resources/css/styles.css
Normal 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;
|
||||
}
|
BIN
examples/themes/login/sunrise/resources/img/bkgrnd.jpg
Normal file
After Width: | Height: | Size: 344 KiB |
BIN
examples/themes/login/sunrise/resources/img/logo.png
Normal file
After Width: | Height: | Size: 21 KiB |
2
examples/themes/login/sunrise/theme.properties
Normal file
|
@ -0,0 +1,2 @@
|
|||
parent=base
|
||||
styles=css/styles.css
|
49
forms/account-api/pom.xml
Executable 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>
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.keycloak.account;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public enum AccountPages {
|
||||
|
||||
ACCOUNT, PASSWORD, TOTP;
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
70
forms/account-freemarker/pom.xml
Executable 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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.account.freemarker.FreeMarkerAccountProvider
|
49
forms/common-freemarker/pom.xml
Executable 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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
org.keycloak.theme.DefaultLoginThemeProvider
|
||||
org.keycloak.theme.FolderThemeProvider
|
|
@ -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>
|
|
@ -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">
|
|
@ -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">
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
parent=rcue
|
||||
styles=../rcue/css/styles.css css/styles.css
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 513 B |
Before Width: | Height: | Size: 343 B After Width: | Height: | Size: 343 B |
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
Before Width: | Height: | Size: 410 B After Width: | Height: | Size: 410 B |
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 646 B |
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
@ -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");
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
@ -0,0 +1,2 @@
|
|||
parent=base
|
||||
styles=css/styles.css
|
|
@ -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...
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|