Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
4e66a3bf81
211 files changed
+5205
-2913
No files matched your search
@@ -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 {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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>
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,201 @@
|
||||
@import url('../../keycloak/lib/zocial/zocial.css');
|
||||
|
||||
body {
|
||||
background-color: #040507;
|
||||
background-image: url('../img/bkgrnd.jpg');
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
color: #ccc;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 50%;
|
||||
width: 550px;
|
||||
margin-left: -225px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
color: rgba(255, 255, 255, 0.08);
|
||||
text-shadow: none;
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
div.app-form {
|
||||
float: left;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
div.app-form label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
div.social-login {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.2);
|
||||
float: right;
|
||||
width: 150px;
|
||||
padding: 20px 0 200px 40px;
|
||||
}
|
||||
|
||||
div.info-area {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin-top: 40px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.info-area p {
|
||||
margin-right: 30px;
|
||||
display: inline;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password] {
|
||||
color: #ddd;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
background-color: rgba(3,70,114,0.15);
|
||||
border: 0px solid rgba(0,0,0,0.2);
|
||||
box-shadow:inset 0 0 2px 2px rgba(0,0,0,0.15);
|
||||
padding: 10px;
|
||||
width: 296px;
|
||||
}
|
||||
|
||||
input[type=text]:hover, input[type=password]:hover {
|
||||
background-color: rgba(3,70,114,0.4);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
border: none;
|
||||
|
||||
background: -webkit-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
|
||||
background: -moz-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
|
||||
background: -ms-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
|
||||
background: -o-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
|
||||
|
||||
box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
|
||||
|
||||
color: rgba(0,0,0,0.6);
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
margin-right: 10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
p.powered {
|
||||
font-size: 12px;
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
p.powered a {
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.feedback {
|
||||
box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.success {
|
||||
background-color: rgba(155,155,255,0.1);
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: rgba(255,175,0,0.1);
|
||||
}
|
||||
|
||||
div.error {
|
||||
background-color: rgba(255,0,0,0.1);
|
||||
}
|
||||
|
||||
div.feedback p {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
div.rcue-logo {
|
||||
background-image: url('../img/logo.png');
|
||||
background-repeat: no-repeat;
|
||||
height: 500px;
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
top: 30px;
|
||||
width: 500px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
div.social-login span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.social-login p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.social-login ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.social-login ul li {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
div.social-login ul li span {
|
||||
display: inline;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
a.zocial {
|
||||
border: none;
|
||||
background: -webkit-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
|
||||
background: -moz-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
|
||||
background: -ms-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
|
||||
background: -o-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
|
||||
box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
|
||||
color: rgba(0,0,0,0.6);
|
||||
width: 130px;
|
||||
text-shadow: none;
|
||||
-webkit-border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
border-radius: 0;
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 344 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,2 @@
|
||||
parent=base
|
||||
styles=css/styles.css
|
||||
Executable
+49
@@ -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);
|
||||
|
||||
}
|
||||
Executable
+70
@@ -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>
|
||||
+158
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+11
-17
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+9
-16
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.keycloak.account.freemarker.FreeMarkerAccountProvider
|
||||
Executable
+49
@@ -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>
|
||||
+15
@@ -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);
|
||||
|
||||
}
|
||||
Executable
+54
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
org.keycloak.theme.DefaultLoginThemeProvider
|
||||
org.keycloak.theme.FolderThemeProvider
|
||||
+11
-14
@@ -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
-1
@@ -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
-1
@@ -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">
|
||||
+27
@@ -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
|
||||
+6
-8
@@ -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>
|
||||
+13
-16
@@ -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>
|
||||
+13
@@ -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
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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");
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,2 @@
|
||||
parent=base
|
||||
styles=css/styles.css
|
||||
+2
-2
@@ -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...
|
||||
+2
-2
@@ -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
|
||||
+5
-5
@@ -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>
|
||||
+6
-6
@@ -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>
|
||||
+3
-3
@@ -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>
|
||||
+6
-6
@@ -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" />
|
||||
+5
-5
@@ -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" />
|
||||
+6
-6
@@ -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>
|
||||
+2
-2
@@ -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
|
||||
+4
-4
@@ -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>
|
||||
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user