diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/index.html b/admin-ui/src/main/resources/META-INF/resources/admin/index.html index 529fe857ceb..c85ab9bcd16 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/index.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/index.html @@ -3,7 +3,7 @@ - Keycloak + Keycloak Admin Console diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js index aa32945a6f5..bda811fe71f 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js @@ -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); + } }); \ No newline at end of file diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index 3ff868230a0..31f58348d7c 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -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(); }; }); diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js index d22e03b8d1b..faf05cd2b91 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js @@ -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 { diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js index 33e47a95451..b572a10519f 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js @@ -1,645 +1,649 @@ -'use strict'; - -var module = angular.module('keycloak.services', [ 'ngResource' ]); - -module.service('Dialog', function($dialog) { - var dialog = {}; - - var escapeHtml = function(str) { - var div = document.createElement('div'); - div.appendChild(document.createTextNode(str)); - return div.innerHTML; - }; - - dialog.confirmDelete = function(name, type, success) { - var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); - var msg = 'Are you sure you want to permanently delete the ' + escapeHtml(type) + ' "' + escapeHtml(name) + '"?' + - 'This action can\'t be undone.'; - var btns = [ { - result : 'cancel', - label : 'Cancel' - }, { - result : 'ok', - label : 'Delete', - cssClass : 'destructive' - } ]; - - $dialog.messageBox(title, msg, btns).open().then(function(result) { - if (result == "ok") { - success(); - } - }); - } - - dialog.confirmGenerateKeys = function(name, type, success) { - var title = 'Generate new keys for realm'; - var msg = 'Are you sure you want to permanently generate new keys for ' + name + '"?' + - 'This action can\'t be undone.'; - var btns = [ { - result : 'cancel', - label : 'Cancel' - }, { - result : 'ok', - label : 'Generate new keys', - cssClass : 'destructive' - } ]; - - $dialog.messageBox(title, msg, btns).open().then(function(result) { - if (result == "ok") { - success(); - } - }); - } - - return dialog -}); - -module.factory('Notifications', function($rootScope, $timeout) { - // time (in ms) the notifications are shown - var delay = 5000; - - var notifications = {}; - - var scheduled = null; - var schedulePop = function() { - if (scheduled) { - $timeout.cancel(scheduled); - } - - scheduled = $timeout(function() { - $rootScope.notification = null; - scheduled = null; - }, delay); - }; - - if (!$rootScope.notifications) { - $rootScope.notifications = []; - } - - notifications.message = function(type, header, message) { - $rootScope.notification = { - type : type, - header: header, - message : message - }; - - schedulePop(); - } - - notifications.info = function(message) { - notifications.message("info", "Info!", message); - }; - - notifications.success = function(message) { - notifications.message("success", "Success!", message); - }; - - notifications.error = function(message) { - notifications.message("error", "Error!", message); - }; - - notifications.warn = function(message) { - notifications.message("warn", "Warning!", message); - }; - - return notifications; -}); - -module.factory('Realm', function($resource) { - return $resource('/auth/rest/admin/realms/:id', { - id : '@realm' - }, { - update : { - method : 'PUT' - }, - create : { - method : 'POST', - params : { id : ''} - } - - }); -}); - -module.factory('User', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/users/:userId', { - realm : '@realm', - userId : '@userId' - }, { - update : { - method : 'PUT' - } - }); -}); - -module.factory('UserCredentials', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/users/:userId/credentials', { - realm : '@realm', - userId : '@userId' - }, { - update : { - method : 'PUT', - isArray : true - } - }); -}); - -module.factory('RealmRoleMapping', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/realm', { - realm : '@realm', - userId : '@userId' - }); -}); - -module.factory('ApplicationRoleMapping', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/applications/:application', { - realm : '@realm', - userId : '@userId', - application : "@application" - }); -}); - -module.factory('ApplicationRealmScopeMapping', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/realm', { - realm : '@realm', - application : '@application' - }); -}); - -module.factory('ApplicationApplicationScopeMapping', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/applications/:targetApp', { - realm : '@realm', - application : '@application', - targetApp : '@targetApp' - }); -}); - - - -module.factory('RealmRoles', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/roles', { - realm : '@realm' - }); -}); - -module.factory('RoleRealmComposites', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/realm', { - realm : '@realm', - role : '@role' - }); -}); - -module.factory('RoleApplicationComposites', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', { - realm : '@realm', - role : '@role', - application : "@application" - }); -}); - - -function roleControl($scope, realm, role, roles, applications, - ApplicationRole, RoleById, RoleRealmComposites, RoleApplicationComposites, - $http, $location, Notifications, Dialog) { - - $scope.$watch(function () { - return $location.path(); - }, function () { - $scope.path = $location.path().substring(1).split("/"); - }); - - $scope.$watch('role', function () { - if (!angular.equals($scope.role, role)) { - $scope.changed = true; - } - }, true); - - $scope.update = function () { - RoleById.update({ - realm: realm.realm, - role: role.id - }, $scope.role, function () { - $scope.changed = false; - role = angular.copy($scope.role); - Notifications.success("Your changes have been saved to the role."); - }); - }; - - $scope.reset = function () { - $scope.role = angular.copy(role); - $scope.changed = false; - }; - - if (!role.id) return; - - $scope.realmRoles = angular.copy(roles); - $scope.selectedRealmRoles = []; - $scope.selectedRealmMappings = []; - $scope.realmMappings = []; - $scope.applications = applications; - $scope.applicationRoles = []; - $scope.selectedApplicationRoles = []; - $scope.selectedApplicationMappings = []; - $scope.applicationMappings = []; - - console.log('remove self'); - for (var j = 0; j < $scope.realmRoles.length; j++) { - if ($scope.realmRoles[j].id == role.id) { - var realmRole = $scope.realmRoles[j]; - var idx = $scope.realmRoles.indexOf(realmRole); - $scope.realmRoles.splice(idx, 1); - break; - } - } - - - $scope.realmMappings = RoleRealmComposites.query({realm : realm.realm, role : role.id}, function(){ - for (var i = 0; i < $scope.realmMappings.length; i++) { - var role = $scope.realmMappings[i]; - for (var j = 0; j < $scope.realmRoles.length; j++) { - var realmRole = $scope.realmRoles[j]; - if (realmRole.id == role.id) { - var idx = $scope.realmRoles.indexOf(realmRole); - if (idx != -1) { - $scope.realmRoles.splice(idx, 1); - break; - } - } - } - } - }); - - $scope.addRealmRole = function() { - $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - $scope.selectedRealmRoles).success(function() { - for (var i = 0; i < $scope.selectedRealmRoles.length; i++) { - var role = $scope.selectedRealmRoles[i]; - var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]); - if (idx != -1) { - $scope.realmRoles.splice(idx, 1); - $scope.realmMappings.push(role); - } - } - $scope.selectRealmRoles = []; - }); - }; - - $scope.deleteRealmRole = function() { - $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() { - for (var i = 0; i < $scope.selectedRealmMappings.length; i++) { - var role = $scope.selectedRealmMappings[i]; - var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]); - if (idx != -1) { - $scope.realmMappings.splice(idx, 1); - $scope.realmRoles.push(role); - } - } - $scope.selectedRealmMappings = []; - }); - }; - - $scope.addApplicationRole = function() { - $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - $scope.selectedApplicationRoles).success(function() { - for (var i = 0; i < $scope.selectedApplicationRoles.length; i++) { - var role = $scope.selectedApplicationRoles[i]; - var idx = $scope.applicationRoles.indexOf($scope.selectedApplicationRoles[i]); - if (idx != -1) { - $scope.applicationRoles.splice(idx, 1); - $scope.applicationMappings.push(role); - } - } - $scope.selectedApplicationRoles = []; - }); - }; - - $scope.deleteApplicationRole = function() { - $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', - {data : $scope.selectedApplicationMappings, headers : {"content-type" : "application/json"}}).success(function() { - for (var i = 0; i < $scope.selectedApplicationMappings.length; i++) { - var role = $scope.selectedApplicationMappings[i]; - var idx = $scope.applicationMappings.indexOf($scope.selectedApplicationMappings[i]); - if (idx != -1) { - $scope.applicationMappings.splice(idx, 1); - $scope.applicationRoles.push(role); - } - } - $scope.selectedApplicationMappings = []; - }); - }; - - - $scope.changeApplication = function() { - $scope.applicationRoles = ApplicationRole.query({realm : realm.realm, application : $scope.compositeApp.name}, function() { - $scope.applicationMappings = RoleApplicationComposites.query({realm : realm.realm, role : role.id, application : $scope.compositeApp.name}, function(){ - for (var i = 0; i < $scope.applicationMappings.length; i++) { - var role = $scope.applicationMappings[i]; - for (var j = 0; j < $scope.applicationRoles.length; j++) { - var realmRole = $scope.applicationRoles[j]; - if (realmRole.id == role.id) { - var idx = $scope.applicationRoles.indexOf(realmRole); - if (idx != -1) { - $scope.applicationRoles.splice(idx, 1); - break; - } - } - } - } - }); - for (var j = 0; j < $scope.applicationRoles.length; j++) { - if ($scope.applicationRoles[j] == role.id) { - var appRole = $scope.applicationRoles[j]; - var idx = $scope.applicationRoles.indexof(appRole); - $scope.applicationRoles.splice(idx, 1); - break; - } - } - } - ); - }; - - - - -} - - -module.factory('Role', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/roles/:role', { - realm : '@realm', - role : '@role' - }, { - update : { - method : 'PUT' - } - }); -}); - -module.factory('RoleById', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role', { - realm : '@realm', - role : '@role' - }, { - update : { - method : 'PUT' - } - }); -}); - -module.factory('ApplicationRole', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/applications/:application/roles/:role', { - realm : '@realm', - application : "@application", - role : '@role' - }, { - update : { - method : 'PUT' - } - }); -}); - - -module.factory('Application', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/applications/:application', { - realm : '@realm', - application : '@name' - }, { - update : { - method : 'PUT' - } - }); -}); - -module.factory('ApplicationInstallation', function($resource) { - var url = '/auth/rest/admin/realms/:realm/applications/:application/installation'; - var resource = $resource('/auth/rest/admin/realms/:realm/applications/:application/installation', { - realm : '@realm', - application : '@application' - }, { - update : { - method : 'PUT' - } - }); - resource.url = function(parameters) { - return url.replace(':realm', parameters.realm).replace(':application', parameters.application); - } - return resource; -}); - -module.factory('ApplicationCredentials', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/applications/:application/credentials', { - realm : '@realm', - application : '@application' - }, { - update : { - method : 'PUT', - isArray : true - } - }); -}); - -module.factory('ApplicationOrigins', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/applications/:application/allowed-origins', { - realm : '@realm', - application : '@application' - }, { - update : { - method : 'PUT', - isArray : true - } - }); -}); - -module.factory('OAuthClient', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:id', { - realm : '@realm', - id : '@id' - }, { - update : { - method : 'PUT' - } - }); -}); - -module.factory('OAuthClientCredentials', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/credentials', { - realm : '@realm', - oauth : '@oauth' - }, { - update : { - method : 'PUT', - isArray : true - } - }); -}); - -module.factory('OAuthClientRealmScopeMapping', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/realm', { - realm : '@realm', - oauth : '@oauth' - }); -}); - -module.factory('OAuthClientApplicationScopeMapping', function($resource) { - return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/applications/:targetApp', { - realm : '@realm', - oauth : '@oauth', - targetApp : '@targetApp' - }); -}); - -module.factory('OAuthClientInstallation', function($resource) { - var url = '/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation'; - var resource = $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation', { - realm : '@realm', - oauth : '@oauth' - }, { - update : { - method : 'PUT' - } - }); - resource.url = function(parameters) { - return url.replace(':realm', parameters.realm).replace(':oauth', parameters.oauth); - } - return resource; -}); - - -module.factory('Current', function(Realm, $route) { - var current = {}; - - current.realms = {}; - current.realm = null; - current.applications = {}; - current.application = null; - - current.refresh = function() { - current.realm = null; - current.realms = Realm.query(null, function(realms) { - if ($route.current.params.realm) { - for (var i = 0; i < realms.length; i++) { - if (realms[i].realm == $route.current.params.realm) { - current.realm = realms[i]; - } - } - } - }); - } - - current.refresh(); - - return current; -}); - -module.factory('TimeUnit', function() { - var t = {}; - - t.autoUnit = function(time) { - var unit = 'Seconds'; - if (time % 60 == 0) { - unit = 'Minutes'; - time = time / 60; - } - if (time % 60 == 0) { - unit = 'Hours'; - time = time / 60; - } - if (time % 24 == 0) { - unit = 'Days' - time = time / 24; - } - return unit; - } - - t.toSeconds = function(time, unit) { - switch (unit) { - case 'Seconds': return time; - case 'Minutes': return time * 60; - case 'Hours': return time * 360; - case 'Days': return time * 86400; - default: throw 'invalid unit ' + unit; - } - } - - t.toUnit = function(time, unit) { - switch (unit) { - case 'Seconds': return time; - case 'Minutes': return Math.ceil(time / 60); - case 'Hours': return Math.ceil(time / 360); - case 'Days': return Math.ceil(time / 86400); - default: throw 'invalid unit ' + unit; - } - } - - t.convert = function(time, from, to) { - var seconds = t.toSeconds(time, from); - return t.toUnit(seconds, to); - } - - return t; -}); - - -module.factory('PasswordPolicy', function() { - var p = {}; - - p.policyMessages = { - length: "Minimal password length (integer type). Default value is 8.", - digits: "Minimal number (integer type) of digits in password. Default value is 1.", - lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.", - upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.", - specialChars: "Minimal number (integer type) of special characters in password. Default value is 1." - } - - p.allPolicies = [ - { name: 'length', value: 8 }, - { name: 'digits', value: 1 }, - { name: 'lowerCase', value: 1 }, - { name: 'upperCase', value: 1 }, - { name: 'specialChars', value: 1 } - ]; - - p.parse = function(policyString) { - var policies = []; - - if (!policyString || policyString.length == 0){ - return policies; - } - - var policyArray = policyString.split(" and "); - - for (var i = 0; i < policyArray.length; i ++){ - var policyToken = policyArray[i]; - var re = /(\w+)\(*(\d*)\)*/; - - var policyEntry = re.exec(policyToken); - - policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) }); - - } - - return policies; - }; - - p.toString = function(policies) { - if (!policies || policies.length == 0) { - return null; - } - - var policyString = ""; - - for (var i in policies){ - policyString += policies[i].name; - if ( policies[i].value ){ - policyString += '(' + policies[i].value + ')'; - } - policyString += " and "; - } - - policyString = policyString.substring(0, policyString.length - 5); - - return policyString; - }; - - return p; +'use strict'; + +var module = angular.module('keycloak.services', [ 'ngResource' ]); + +module.service('Dialog', function($dialog) { + var dialog = {}; + + var escapeHtml = function(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + }; + + dialog.confirmDelete = function(name, type, success) { + var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); + var msg = 'Are you sure you want to permanently delete the ' + escapeHtml(type) + ' "' + escapeHtml(name) + '"?' + + 'This action can\'t be undone.'; + var btns = [ { + result : 'cancel', + label : 'Cancel' + }, { + result : 'ok', + label : 'Delete', + cssClass : 'destructive' + } ]; + + $dialog.messageBox(title, msg, btns).open().then(function(result) { + if (result == "ok") { + success(); + } + }); + } + + dialog.confirmGenerateKeys = function(name, type, success) { + var title = 'Generate new keys for realm'; + var msg = 'Are you sure you want to permanently generate new keys for ' + name + '"?' + + 'This action can\'t be undone.'; + var btns = [ { + result : 'cancel', + label : 'Cancel' + }, { + result : 'ok', + label : 'Generate new keys', + cssClass : 'destructive' + } ]; + + $dialog.messageBox(title, msg, btns).open().then(function(result) { + if (result == "ok") { + success(); + } + }); + } + + return dialog +}); + +module.factory('Notifications', function($rootScope, $timeout) { + // time (in ms) the notifications are shown + var delay = 5000; + + var notifications = {}; + + var scheduled = null; + var schedulePop = function() { + if (scheduled) { + $timeout.cancel(scheduled); + } + + scheduled = $timeout(function() { + $rootScope.notification = null; + scheduled = null; + }, delay); + }; + + if (!$rootScope.notifications) { + $rootScope.notifications = []; + } + + notifications.message = function(type, header, message) { + $rootScope.notification = { + type : type, + header: header, + message : message + }; + + schedulePop(); + } + + notifications.info = function(message) { + notifications.message("info", "Info!", message); + }; + + notifications.success = function(message) { + notifications.message("success", "Success!", message); + }; + + notifications.error = function(message) { + notifications.message("error", "Error!", message); + }; + + notifications.warn = function(message) { + notifications.message("warn", "Warning!", message); + }; + + return notifications; +}); + +module.factory('Realm', function($resource) { + return $resource('/auth/rest/admin/realms/:id', { + id : '@realm' + }, { + update : { + method : 'PUT' + }, + create : { + method : 'POST', + params : { id : ''} + } + + }); +}); + +module.factory('ServerInfo', function($resource) { + return $resource('/auth/rest/admin/serverinfo'); +}); + +module.factory('User', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/users/:userId', { + realm : '@realm', + userId : '@userId' + }, { + update : { + method : 'PUT' + } + }); +}); + +module.factory('UserCredentials', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/users/:userId/credentials', { + realm : '@realm', + userId : '@userId' + }, { + update : { + method : 'PUT', + isArray : true + } + }); +}); + +module.factory('RealmRoleMapping', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/realm', { + realm : '@realm', + userId : '@userId' + }); +}); + +module.factory('ApplicationRoleMapping', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/applications/:application', { + realm : '@realm', + userId : '@userId', + application : "@application" + }); +}); + +module.factory('ApplicationRealmScopeMapping', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/realm', { + realm : '@realm', + application : '@application' + }); +}); + +module.factory('ApplicationApplicationScopeMapping', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/applications/:targetApp', { + realm : '@realm', + application : '@application', + targetApp : '@targetApp' + }); +}); + + + +module.factory('RealmRoles', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/roles', { + realm : '@realm' + }); +}); + +module.factory('RoleRealmComposites', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/realm', { + realm : '@realm', + role : '@role' + }); +}); + +module.factory('RoleApplicationComposites', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', { + realm : '@realm', + role : '@role', + application : "@application" + }); +}); + + +function roleControl($scope, realm, role, roles, applications, + ApplicationRole, RoleById, RoleRealmComposites, RoleApplicationComposites, + $http, $location, Notifications, Dialog) { + + $scope.$watch(function () { + return $location.path(); + }, function () { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.$watch('role', function () { + if (!angular.equals($scope.role, role)) { + $scope.changed = true; + } + }, true); + + $scope.update = function () { + RoleById.update({ + realm: realm.realm, + role: role.id + }, $scope.role, function () { + $scope.changed = false; + role = angular.copy($scope.role); + Notifications.success("Your changes have been saved to the role."); + }); + }; + + $scope.reset = function () { + $scope.role = angular.copy(role); + $scope.changed = false; + }; + + if (!role.id) return; + + $scope.realmRoles = angular.copy(roles); + $scope.selectedRealmRoles = []; + $scope.selectedRealmMappings = []; + $scope.realmMappings = []; + $scope.applications = applications; + $scope.applicationRoles = []; + $scope.selectedApplicationRoles = []; + $scope.selectedApplicationMappings = []; + $scope.applicationMappings = []; + + console.log('remove self'); + for (var j = 0; j < $scope.realmRoles.length; j++) { + if ($scope.realmRoles[j].id == role.id) { + var realmRole = $scope.realmRoles[j]; + var idx = $scope.realmRoles.indexOf(realmRole); + $scope.realmRoles.splice(idx, 1); + break; + } + } + + + $scope.realmMappings = RoleRealmComposites.query({realm : realm.realm, role : role.id}, function(){ + for (var i = 0; i < $scope.realmMappings.length; i++) { + var role = $scope.realmMappings[i]; + for (var j = 0; j < $scope.realmRoles.length; j++) { + var realmRole = $scope.realmRoles[j]; + if (realmRole.id == role.id) { + var idx = $scope.realmRoles.indexOf(realmRole); + if (idx != -1) { + $scope.realmRoles.splice(idx, 1); + break; + } + } + } + } + }); + + $scope.addRealmRole = function() { + $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', + $scope.selectedRealmRoles).success(function() { + for (var i = 0; i < $scope.selectedRealmRoles.length; i++) { + var role = $scope.selectedRealmRoles[i]; + var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]); + if (idx != -1) { + $scope.realmRoles.splice(idx, 1); + $scope.realmMappings.push(role); + } + } + $scope.selectRealmRoles = []; + }); + }; + + $scope.deleteRealmRole = function() { + $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', + {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() { + for (var i = 0; i < $scope.selectedRealmMappings.length; i++) { + var role = $scope.selectedRealmMappings[i]; + var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]); + if (idx != -1) { + $scope.realmMappings.splice(idx, 1); + $scope.realmRoles.push(role); + } + } + $scope.selectedRealmMappings = []; + }); + }; + + $scope.addApplicationRole = function() { + $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', + $scope.selectedApplicationRoles).success(function() { + for (var i = 0; i < $scope.selectedApplicationRoles.length; i++) { + var role = $scope.selectedApplicationRoles[i]; + var idx = $scope.applicationRoles.indexOf($scope.selectedApplicationRoles[i]); + if (idx != -1) { + $scope.applicationRoles.splice(idx, 1); + $scope.applicationMappings.push(role); + } + } + $scope.selectedApplicationRoles = []; + }); + }; + + $scope.deleteApplicationRole = function() { + $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites', + {data : $scope.selectedApplicationMappings, headers : {"content-type" : "application/json"}}).success(function() { + for (var i = 0; i < $scope.selectedApplicationMappings.length; i++) { + var role = $scope.selectedApplicationMappings[i]; + var idx = $scope.applicationMappings.indexOf($scope.selectedApplicationMappings[i]); + if (idx != -1) { + $scope.applicationMappings.splice(idx, 1); + $scope.applicationRoles.push(role); + } + } + $scope.selectedApplicationMappings = []; + }); + }; + + + $scope.changeApplication = function() { + $scope.applicationRoles = ApplicationRole.query({realm : realm.realm, application : $scope.compositeApp.name}, function() { + $scope.applicationMappings = RoleApplicationComposites.query({realm : realm.realm, role : role.id, application : $scope.compositeApp.name}, function(){ + for (var i = 0; i < $scope.applicationMappings.length; i++) { + var role = $scope.applicationMappings[i]; + for (var j = 0; j < $scope.applicationRoles.length; j++) { + var realmRole = $scope.applicationRoles[j]; + if (realmRole.id == role.id) { + var idx = $scope.applicationRoles.indexOf(realmRole); + if (idx != -1) { + $scope.applicationRoles.splice(idx, 1); + break; + } + } + } + } + }); + for (var j = 0; j < $scope.applicationRoles.length; j++) { + if ($scope.applicationRoles[j] == role.id) { + var appRole = $scope.applicationRoles[j]; + var idx = $scope.applicationRoles.indexof(appRole); + $scope.applicationRoles.splice(idx, 1); + break; + } + } + } + ); + }; + + + + +} + + +module.factory('Role', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/roles/:role', { + realm : '@realm', + role : '@role' + }, { + update : { + method : 'PUT' + } + }); +}); + +module.factory('RoleById', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role', { + realm : '@realm', + role : '@role' + }, { + update : { + method : 'PUT' + } + }); +}); + +module.factory('ApplicationRole', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/roles/:role', { + realm : '@realm', + application : "@application", + role : '@role' + }, { + update : { + method : 'PUT' + } + }); +}); + + +module.factory('Application', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application', { + realm : '@realm', + application : '@name' + }, { + update : { + method : 'PUT' + } + }); +}); + +module.factory('ApplicationInstallation', function($resource) { + var url = '/auth/rest/admin/realms/:realm/applications/:application/installation'; + var resource = $resource('/auth/rest/admin/realms/:realm/applications/:application/installation', { + realm : '@realm', + application : '@application' + }, { + update : { + method : 'PUT' + } + }); + resource.url = function(parameters) { + return url.replace(':realm', parameters.realm).replace(':application', parameters.application); + } + return resource; +}); + +module.factory('ApplicationCredentials', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/credentials', { + realm : '@realm', + application : '@application' + }, { + update : { + method : 'PUT', + isArray : true + } + }); +}); + +module.factory('ApplicationOrigins', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/applications/:application/allowed-origins', { + realm : '@realm', + application : '@application' + }, { + update : { + method : 'PUT', + isArray : true + } + }); +}); + +module.factory('OAuthClient', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:id', { + realm : '@realm', + id : '@id' + }, { + update : { + method : 'PUT' + } + }); +}); + +module.factory('OAuthClientCredentials', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/credentials', { + realm : '@realm', + oauth : '@oauth' + }, { + update : { + method : 'PUT', + isArray : true + } + }); +}); + +module.factory('OAuthClientRealmScopeMapping', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/realm', { + realm : '@realm', + oauth : '@oauth' + }); +}); + +module.factory('OAuthClientApplicationScopeMapping', function($resource) { + return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/applications/:targetApp', { + realm : '@realm', + oauth : '@oauth', + targetApp : '@targetApp' + }); +}); + +module.factory('OAuthClientInstallation', function($resource) { + var url = '/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation'; + var resource = $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation', { + realm : '@realm', + oauth : '@oauth' + }, { + update : { + method : 'PUT' + } + }); + resource.url = function(parameters) { + return url.replace(':realm', parameters.realm).replace(':oauth', parameters.oauth); + } + return resource; +}); + + +module.factory('Current', function(Realm, $route) { + var current = {}; + + current.realms = {}; + current.realm = null; + current.applications = {}; + current.application = null; + + current.refresh = function() { + current.realm = null; + current.realms = Realm.query(null, function(realms) { + if ($route.current.params.realm) { + for (var i = 0; i < realms.length; i++) { + if (realms[i].realm == $route.current.params.realm) { + current.realm = realms[i]; + } + } + } + }); + } + + current.refresh(); + + return current; +}); + +module.factory('TimeUnit', function() { + var t = {}; + + t.autoUnit = function(time) { + var unit = 'Seconds'; + if (time % 60 == 0) { + unit = 'Minutes'; + time = time / 60; + } + if (time % 60 == 0) { + unit = 'Hours'; + time = time / 60; + } + if (time % 24 == 0) { + unit = 'Days' + time = time / 24; + } + return unit; + } + + t.toSeconds = function(time, unit) { + switch (unit) { + case 'Seconds': return time; + case 'Minutes': return time * 60; + case 'Hours': return time * 360; + case 'Days': return time * 86400; + default: throw 'invalid unit ' + unit; + } + } + + t.toUnit = function(time, unit) { + switch (unit) { + case 'Seconds': return time; + case 'Minutes': return Math.ceil(time / 60); + case 'Hours': return Math.ceil(time / 360); + case 'Days': return Math.ceil(time / 86400); + default: throw 'invalid unit ' + unit; + } + } + + t.convert = function(time, from, to) { + var seconds = t.toSeconds(time, from); + return t.toUnit(seconds, to); + } + + return t; +}); + + +module.factory('PasswordPolicy', function() { + var p = {}; + + p.policyMessages = { + length: "Minimal password length (integer type). Default value is 8.", + digits: "Minimal number (integer type) of digits in password. Default value is 1.", + lowerCase: "Minimal number (integer type) of lowercase characters in password. Default value is 1.", + upperCase: "Minimal number (integer type) of uppercase characters in password. Default value is 1.", + specialChars: "Minimal number (integer type) of special characters in password. Default value is 1." + } + + p.allPolicies = [ + { name: 'length', value: 8 }, + { name: 'digits', value: 1 }, + { name: 'lowerCase', value: 1 }, + { name: 'upperCase', value: 1 }, + { name: 'specialChars', value: 1 } + ]; + + p.parse = function(policyString) { + var policies = []; + + if (!policyString || policyString.length == 0){ + return policies; + } + + var policyArray = policyString.split(" and "); + + for (var i = 0; i < policyArray.length; i ++){ + var policyToken = policyArray[i]; + var re = /(\w+)\(*(\d*)\)*/; + + var policyEntry = re.exec(policyToken); + + policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) }); + + } + + return policies; + }; + + p.toString = function(policies) { + if (!policies || policies.length == 0) { + return null; + } + + var policyString = ""; + + for (var i in policies){ + policyString += policies[i].name; + if ( policies[i].value ){ + policyString += '(' + policies[i].value + ')'; + } + policyString += " and "; + } + + policyString = policyString.substring(0, policyString.length - 5); + + return policyString; + }; + + return p; }); \ No newline at end of file diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html index 2ebeb3e9a67..859aea19552 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html @@ -1,7 +1,7 @@
- \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl new file mode 100644 index 00000000000..210e9bbfb22 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl @@ -0,0 +1,67 @@ +<#macro mainLayout active bodyClass> + + + + + Keycloak Account Management + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + + + + + <#if message?has_content> +
+ <#if message.success> + + + <#if message.error> + + +
+ + +
+ +
+ +
+
+ + +
+
+ <#nested "content"> +
+
+
+
+
+ + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl b/forms/common-themes/src/main/resources/theme/account/base/totp.ftl similarity index 84% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl rename to forms/common-themes/src/main/resources/theme/account/base/totp.ftl index cad065e9136..70dadf156fa 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/totp.ftl @@ -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"> - +

<#if totp.enabled> -

Authenticators

+ Authenticators <#else> -

Google Authenticator Setup

+ Google Authenticator Setup + - <#elseif section = "content"> - - <#if totp.enabled> + <#if totp.enabled>

You have the following authenticators set up:

@@ -33,11 +29,13 @@ you will have to reconfigure it immediately or on the next login.

+
+ <#if url.referrerURI??>Back to application +
- - <#else> + <#else>
    -
  1. +
  2. 1Download the Google Authenticator app in your device.

  3. @@ -49,7 +47,7 @@

    3Enter the one-time-password provided by Google Authenticator below and click Submit to finish the setup.

    - +
    @@ -60,7 +58,6 @@
- - + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/account/keycloak/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/account/keycloak/resources/css/styles.css new file mode 100644 index 00000000000..2df50dfcf30 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/keycloak/resources/css/styles.css @@ -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; +} diff --git a/forms/common-themes/src/main/resources/theme/account/keycloak/theme.properties b/forms/common-themes/src/main/resources/theme/account/keycloak/theme.properties new file mode 100644 index 00000000000..9898eaefdb6 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/keycloak/theme.properties @@ -0,0 +1,2 @@ +parent=rcue +styles=../rcue/css/styles.css css/styles.css \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/admin-console.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/admin-console.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/admin-console.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/admin-console.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/base.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/base.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/base.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/base.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/forms.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/forms.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/header.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/header.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/header.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/header.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/icons.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/icons.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/icons.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/icons.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/customer-login-screen-bg.jpg b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/customer-login-screen-bg.jpg similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/customer-login-screen-bg.jpg rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/customer-login-screen-bg.jpg diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-arrow-down.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-error-arrow-down.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-arrow-down.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-error-arrow-down.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-sign.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-error-sign.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-sign.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-error-sign.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-arrow-down.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-success-arrow-down.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-arrow-down.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-success-arrow-down.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-sign.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-success-sign.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-success-sign.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-success-sign.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-warning-sign.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-warning-sign.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-warning-sign.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-warning-sign.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-separator.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-separator.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-separator.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-separator.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-social-separator.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-social-separator.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-social-separator.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-social-separator.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-social.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-social.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-social.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-social.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/register-login-bg.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/register-login-bg.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/register-login-bg.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/register-login-bg.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/sprites-white.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/sprites-white.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/sprites-white.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/sprites-white.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/login-register.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/login-register.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/reset.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/reset.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/reset.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/reset.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/sprites.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/sprites.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/sprites.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/sprites.css diff --git a/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/styles.css new file mode 100644 index 00000000000..41a9d55c669 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/styles.css @@ -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"); \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/tables.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/tables.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/tables.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/css/tables.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/img/favicon.ico b/forms/common-themes/src/main/resources/theme/account/rcue/resources/img/favicon.ico similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/img/favicon.ico rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/img/favicon.ico diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/img/keycloak-logo.png b/forms/common-themes/src/main/resources/theme/account/rcue/resources/img/keycloak-logo.png similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/img/keycloak-logo.png rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/img/keycloak-logo.png diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/img/login-screen-background.jpg b/forms/common-themes/src/main/resources/theme/account/rcue/resources/img/login-screen-background.jpg similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/img/login-screen-background.jpg rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/img/login-screen-background.jpg diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap-theme.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap-theme.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap-theme.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap-theme.css diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap-theme.min.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap-theme.min.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap-theme.min.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap-theme.min.css diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap.css diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap.min.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap.min.css similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/css/bootstrap.min.css rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap.min.css diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.eot b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.eot rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.eot diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.svg b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.svg rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.svg diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.woff b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/fonts/glyphicons-halflings-regular.woff rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.woff diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/js/bootstrap.js b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/js/bootstrap.js similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/js/bootstrap.js rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/js/bootstrap.js diff --git a/forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/js/bootstrap.min.js b/forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/js/bootstrap.min.js similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/lib/bootstrap/js/bootstrap.min.js rename to forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/js/bootstrap.min.js diff --git a/forms/common-themes/src/main/resources/theme/account/rcue/theme.properties b/forms/common-themes/src/main/resources/theme/account/rcue/theme.properties new file mode 100644 index 00000000000..298aa65ac23 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/rcue/theme.properties @@ -0,0 +1,2 @@ +parent=base +styles=css/styles.css diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl b/forms/common-themes/src/main/resources/theme/login/base/error.ftl similarity index 80% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl rename to forms/common-themes/src/main/resources/theme/login/base/error.ftl index fd3fc1ebee3..78b8284cbea 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/error.ftl @@ -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... diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl similarity index 93% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl index 97ff4adf472..a3872aed8cb 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl @@ -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 diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl similarity index 79% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl index bacf277e418..389a9348860 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl @@ -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">
-

${oauth.client.loginName} requests access to:

+

${oauth.client} requests access to:

Keycloak Central Login and Google will use this information in accordance with their respective terms of service and privacy policies.

-
- + +
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl similarity index 52% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl index 4633b4db6be..226c04b38ac 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl @@ -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">
-

${rb.getString('emailInstruction')}

+

${rb.emailInstruction}

- +
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl similarity index 77% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl index 0d884f6d4dd..4416e57700c 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl @@ -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 @@
- +
@@ -30,7 +30,7 @@ <#elseif section = "info"> <#if realm.registrationAllowed> -

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

+

${rb.noAccount} ${rb.register}.

diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl similarity index 52% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl index 955d4df01d2..7738cd7573e 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl @@ -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">
- +
- +
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl similarity index 62% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl index 5904fdf031c..a235ec3ac46 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl @@ -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 @@

All fields required

- +
- +
- +
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-username-reminder.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl similarity index 51% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-username-reminder.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl index a00d87eb970..32d6c79d47d 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-username-reminder.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-username-reminder.ftl @@ -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">
-

${rb.getString('emailUsernameInstruction')}

+

${rb.emailUsernameInstruction}

- +
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl similarity index 81% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl index cc83a85ea17..3785ba36898 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl @@ -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 diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl b/forms/common-themes/src/main/resources/theme/login/base/login.ftl similarity index 67% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl rename to forms/common-themes/src/main/resources/theme/login/base/login.ftl index e16e9500503..98cefa40ac1 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login.ftl @@ -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 @@
- +
- +
@@ -29,7 +29,7 @@
<#if realm.registrationAllowed> -

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

+

${rb.noAccount} ${rb.register}.

<#if realm.resetPasswordAllowed>

Forgot Username / Password?

diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties similarity index 100% rename from forms/src/main/resources/org/keycloak/forms/messages.properties rename to forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties diff --git a/forms/common-themes/src/main/resources/theme/login/base/register.ftl b/forms/common-themes/src/main/resources/theme/login/base/register.ftl new file mode 100755 index 00000000000..35af01ddd26 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/base/register.ftl @@ -0,0 +1,44 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout bodyClass="register" ; section> + + <#if section = "title"> + + ${rb.registerWith} ${realm.name} + + <#elseif section = "header"> + + ${rb.registerWith} ${realm.name} + + <#elseif section = "form"> + + +

${rb.allRequired}

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

${rb.alreadyHaveAccount} ${rb.logIn}.

+ + + + \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl b/forms/common-themes/src/main/resources/theme/login/base/template.ftl similarity index 50% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl rename to forms/common-themes/src/main/resources/theme/login/base/template.ftl index 50ac51a19c0..f5a3799ada4 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/template.ftl @@ -4,24 +4,22 @@ - - <#nested "title"> - - - - + <#nested "title"> + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + - <#if (template.themeConfig.logo)?has_content> -

- Keycloak -

- +

@@ -36,19 +34,9 @@
-

Application login area

- <#if message?has_content && message.error> - - - <#if message?has_content && message.success> - - <#if template.themeConfig['displayPoweredBy']> -

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

- +

+ ${rb.poweredByKeycloak} +

<#nested "content"> diff --git a/forms/common-themes/src/main/resources/theme/login/keycloak/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/login/keycloak/resources/css/styles.css new file mode 100644 index 00000000000..5988a486ae2 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/keycloak/resources/css/styles.css @@ -0,0 +1,4 @@ +body.rcue-login-register { + background: #083556; + background-image: none; +} diff --git a/forms/common-themes/src/main/resources/theme/login/keycloak/theme.properties b/forms/common-themes/src/main/resources/theme/login/keycloak/theme.properties new file mode 100644 index 00000000000..9898eaefdb6 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/keycloak/theme.properties @@ -0,0 +1,2 @@ +parent=rcue +styles=../rcue/css/styles.css css/styles.css \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/base.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/base.css new file mode 100644 index 00000000000..f258d8f1e20 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/base.css @@ -0,0 +1,52 @@ +* { + -moz-box-sizing: border-box; + -o-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: "Open Sans", sans-serif; +} +body { + height: 100%; + width: 100%; + font-family: "Open Sans", sans-serif; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: normal; + font-family: "Overpass", sans-serif; +} +a { + color: #0099d3; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +/* Styles from Gabriel */ +strong { + font-weight: bold; +} +.hidden { + display: none; +} +.feedback.show { + display: inline-block !important; +} +.pull-right { + float: right; +} +.block { + display: block; +} +a:focus { + outline: 0 none; +} +.clear-font-size { + font-size: 1em; +} diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/forms.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/forms.css new file mode 100644 index 00000000000..c51693c4cf6 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/forms.css @@ -0,0 +1,515 @@ +fieldset { + border: none; +} +fieldset.border-top { + border-color: #E9E8E8; + border-style: solid; + border-width: 1px 0 0; + padding-top: 2em; +} +*::-moz-placeholder, +::-webkit-input-placeholder { + color: #838383; + font-style: italic; +} +input[type="text"], +input[type="password"], +input[type="email"] { + font-size: 1.1em; + padding: 0 0.545454545454545em; + min-width: 18.1818181818182em; + height: 2.36363636363636em; + border: 1px #b6b6b6 solid; + border-radius: 2px; + box-shadow: inset 0px 2px 2px rgba(0, 0, 0, 0.1); + color: #333; +} +input[type="text"]:hover, +input[type="password"]:hover, +input[type="email"]:hover { + border-color: #62afdb; +} +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus { + border-color: #62afdb; + box-shadow: #62afdb 0 0 5px; +} +input[type="text"].error, +input[type="password"].error, +input[type="email"].error { + border-color: #ba1212; + transition: all 0.33s ease-in-out; + -moz-transition: all 0.33s ease-in-out; + -webkit-transition: all 0.33s ease-in-out; +} +input[type="text"].error:focus, +input[type="password"].error:focus, +input[type="email"].error:focus { + box-shadow: 0 0 5px #ba1212; +} +input[type="button"], +button, +input[type="submit"], +a.button { + font-size: 1.3em; + padding: 0.30769230769231em 1.07692307692308em; + border-width: 1px; + border-radius: 2px; + color: #fff; + font-weight: bold; + letter-spacing: 0.04em; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15); +} +input[type="button"].btn-primary, +button.btn-primary, +input[type="submit"].btn-primary, +a.button.btn-primary { + background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3)); + border-color: #21799e; + border-style: solid; +} +input[type="button"].btn-primary:hover, +input[type="submit"].btn-primary:hover, +button.btn-primary:hover, +a.button.btn-primary:hover, +input[type="button"].btn-primary:focus, +input[type="submit"].btn-primary:focus, +button.btn-primary:focus, +a.button.btn-primary:focus { + background-color: #009BD3; +} +input[type="button"].btn-primary:active, +input[type="submit"].btn-primary:active, +button.btn-primary:active, +a.button.btn-primary:active { + background-color: #0099d4; +} +input[type="button"].disabled, +input[type="submit"].disabled, +button.disabled, +a.button.disabled { + border-color: #cfcdcd; + color: #838383; + background-color: transparent; + background-image: none; + box-shadow: none; + font-weight: normal; + letter-spacing: 0.06363636363636em; +} + +input[type="button"].btn-secondary, +button.btn-secondary, +input[type="submit"].btn-secondary, +a.button.btn-secondary { + background-color: #EEEEEE; + border-color: #BBBBBB; + color: #4D5258; +} + +input[type="button"].disabled:hover, +input[type="submit"].disabled:hover, +button.disabled:hover, +a.button.disabled:hover { + cursor: default; +} +input[type="button"].disabled:active, +input[type="submit"].disabled:active, +button.disabled:active, +a.button.disabled:active { + box-shadow: none; +} +input[type="button"]:hover, +input[type="submit"]:hover, +button:hover, +a.button:hover, +input[type="button"]:focus, +input[type="submit"]:focus, +button:focus, +a.button:focus { + background-image: none; + cursor: pointer; +} +input[type="button"]:active, +input[type="submit"]:active, +button:active, +a.button:active { + background-image: none; + cursor: pointer; + box-shadow: inset 0 0 5px 2px rgba(0, 0, 0, 0.25); +} +input[type="checkbox"] { + margin-right: 0.5em; +} +/* Code from Hylke */ +button, +a.button { + border-color: #21799e; + background-image: linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed)); + color: #fff; + padding: 4px 14px; + border: 1px #bbb solid; + border-radius: 2px; + color: #4d5258; + font-weight: bold; + font-size: 1.1em; + letter-spacing: 0.4px; + cursor: pointer; + padding-top: 0; + padding-bottom: 0; + line-height: 2.18181818181818em; +} +input[type='submit'].primary, +button.primary { + border-color: #21799e; + background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3)); + color: #fff; +} +button.primary:hover, +button.primary:focus { + background-color: #009BD3; +} +button.primary:enabled:active { + background-color: #0099d4; + box-shadow: inset 0 0 5px 3px #0074ae; +} +/* Code from Gabriel */ +.rcue-login-register.register .two-fields input[type="text"] { + width: 121px; + min-width: 0; +} +.rcue-login-register.register .two-fields input + input { + margin-left: 10px; +} +.search-comp { + position: relative; + display: inline-block; + font-size: 0.90909090909091em; +} +.search-comp input[type="text"] { + padding-right: 2.45454545454545em; +} +.search-comp .icon-search { + position: absolute; + right: 0.2em; + top: 0.4em; + opacity: 0.5; +} +.search-comp .icon-search:hover { + opacity: 1; + -webkit-transition: ease-in-out opacity 0.25s; + -moz-transition: ease-in-out opacity 0.25s; + -o-transition: ease-in-out opacity 0.25s; + transition: ease-in-out opacity 0.25s; +} +.search-comp .icon-search + .tooltip { + width: 20em; + font-weight: normal; +} +.feedback-aligner { + position: absolute; + top: 1.5em; + text-align: center; + width: 100%; + height: 0; + z-index: 100; +} +.feedback-aligner .feedback { + position: relative; + display: inline-block; + text-align: left; + border-width: 1px; +} +.feedback-aligner .feedback p { + border-width: 1px; +} +.feedback { + position: absolute; + opacity: 0; + transition: opacity 0.33s ease-in-out; + -moz-transition: opacity 0.33s ease-in-out; + -webkit-transition: opacity 0.33s ease-in-out; +} +.feedback p { + padding: 0.90909090909091em 3.63636363636364em; + border-style: solid; + border-width: 1px 1px 0px 1px; + background-repeat: no-repeat; + background-position: 1.2em center; + font-size: 1.1em; + line-height: 1.4em; + border-radius: 2px; + color: #4d5258; + margin-bottom: 0; +} +.feedback.show { + opacity: 1; +} +.feedback.bottom-left { + background-position: left bottom; + background-repeat: no-repeat; + padding-bottom: 1em; +} +.feedback.bottom-left p { + background-position: 1.27272727272727em center; +} +.feedback.error { + background-image: url(../img/feedback-error-arrow-down.png); +} +.feedback.error p { + border-color: #b91415; + background-image: url(../img/feedback-error-sign.png); + background-color: #f8e7e7; +} +.feedback.success { + background-image: url(../img/feedback-success-arrow-down.png); +} +.feedback.success p { + border-color: #4b9e39; + background-image: url(../img/feedback-success-sign.png); + background-color: #e4f1e1; +} +.feedback.warning { + background-image: url(../img/feedback-warning-arrow-down.png); +} +.feedback.warning p { + border-color: #f17528; + background-image: url(../img/feedback-warning-sign.png); + background-color: #fef1e9; +} +button, +a.button { + background-color: #eeeeee; +} +a.button { + display: inline-block; +} +a.button:hover { + color: #4D5258; + text-decoration: none; +} +button[class^="icon-"] { + border: none; + box-shadow: none; + background-color: transparent; + padding: 0; + line-height: 1em; +} + +legend { + font-size: 1em; + border-width: 1px 0 0 0; + border-style: solid; + border-color: #e9e8e8; + padding-top: 2em; + display: block; + margin-bottom: 0; + padding-bottom: 0.8em; + cursor: pointer; +} +legend .icon-collapse { + vertical-align: baseline; +} +legend .text { + font-weight: bold; + font-size: 1.25em; +} + +.form-group { + display: block; + margin-bottom: 1em; + position: relative; +} +.form-group > label { + font-size: 1.1em; + font-weight: 300; + width: 10em; + margin-right: 0.90909090909091em; + margin-bottom: 0; + float: left; + margin-top: 0.45454545454545em; +} +.form-group > label.two-lines { + margin-top: -2px; +} +.form-group > label + span { + font-size: 1.1em; + display: inline-block; + margin-top: 0.454545454545455em; +} +.form-group > label + .onoffswitch { + float: left; +} +.form-group > label.pull-left { + margin-top: 4px; +} +.form-group .required { + position: absolute; + left: 10em; + font-size: 1.1em; + color: #CB2915; +} +legend + .form-group { + padding-top: 1em; +} +legend + table { + margin-top: 1em; +} +.code { + font-family: Courier, monospace; +} +.onoffswitch { + -moz-user-select: none; + height: 26px; + position: relative; + width: 62px; +} +.onoffswitch .onoffswitch-checkbox { + display: none; +} +.onoffswitch .onoffswitch-label { + border: 1px solid #bbb; + border-radius: 2px; + cursor: pointer; + display: block; + overflow: hidden; + width: 62px; +} +.onoffswitch .onoffswitch-inner { + display: block; + margin-left: -100%; + transition: margin 0.3s ease-in 0s; + width: 200%; +} +.onoffswitch .onoffswitch-inner > span { + -moz-box-sizing: border-box; + color: white; + float: left; + font-size: 11px; + font-family: "Open Sans", sans-serif; + font-weight: bold; + height: 24px; + line-height: 24px; + padding: 0; + width: 50%; +} +.onoffswitch .onoffswitch-switch { + background-image: linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed)); + border: 1px solid #aaa; + border-radius: 2px; + bottom: 0; + margin: 0; + position: absolute; + right: 39px; + top: 0; + transition: all 0.3s ease-in 0s; + -webkit-transition: all 0.3s ease-in 0s; + width: 23px; +} +.onoffswitch .onoffswitch-inner .onoffswitch-active { + background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3)); + color: #FFFFFF; + padding-left: 10px; +} +.onoffswitch .onoffswitch-inner .onoffswitch-inactive { + background: linear-gradient(#fefefe, #e8e8e8) repeat scroll 0 0 transparent; + color: #4d5258; + padding-right: 10px; + text-align: right; +} +.onoffswitch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; +} +.onoffswitch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 0; +} +input[type="text"].tiny, +input[type="password"].tiny, +input[type="email"].tiny { + min-width: 40px; + width: 40px; +} +.select-rcue:hover { + border-color: #62afdb; +} +.select-rcue:focus { + border-color: #62afdb; + box-shadow: #62afdb 0 0 5px; +} +.select-rcue.error { + border-color: #ba1212; + background-color: #f8e7e7; + transition: all 0.33s ease-in-out; + -moz-transition: all 0.33s ease-in-out; + -webkit-transition: all 0.33s ease-in-out; +} +.select-rcue.error:focus { + box-shadow: 0 0 5px #ba1212; +} +.select-rcue select { + height: 30px; + line-height: 30px; + margin-top: -2px; + margin-left: -2px; + font-size: 1.1em; + padding: 5px 0.545454545454545em; + background-color: transparent; + border: none; + width: 150%; + font-family: "Open Sans", sans-serif; +} +.select-rcue option { + line-height: 2em; + padding-left: 0.90909090909091em; +} +.select-rcue option:hover { + background-color: #d5ecf9; +} + +.input-group input + .select-rcue { + border-radius: 0 2px 2px 0; + border-left: 0; + display: inline-block; +} +.input-select .input-group input { + float: left; +} + +.form-actions { + float: right; + margin-top: 3em; + margin-bottom: 5em; +} +.form-actions .primary { + float: right; + margin-left: 0.90909090909091em; +} +.form-actions a { + font-size: 1.1em; + margin-right: 0.90909090909091em; +} diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css new file mode 100644 index 00000000000..89222dbf735 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css @@ -0,0 +1,436 @@ +body { + font-size: 62.5%; + min-height: 60em; + min-width: 120em; +} + +body.rcue-login-register { + background-image: url("../img/login-screen-background.jpg"); +} + +div.rcue-logo { + background-image: url("../img/keycloak-logo.png"); + background-repeat: no-repeat; + height: 37px; + width: 150px; + position: absolute; + top: 5em; + right: 6.4em; +} + +.rcue-login-register { + background-color: #1D2226; + background-position: top left; + background-size: auto; + background-repeat: no-repeat; + color: #fff; + /* Login area */ + + /* Social login area */ + + /* Info area */ + +} + +.rcue-login-register h1 a { + position: absolute; + top: 5em; + right: 6.4em; +} +.rcue-login-register .content { + position: absolute; + bottom: 10%; + width: 100%; + min-width: 76em; +} +.rcue-login-register h2 { + padding-left: 4.34782608695652em; + font-family: "Overpass", sans-serif; + font-size: 2.3em; + font-weight: 100; + text-transform: uppercase; + letter-spacing: 0.005em; +} +.rcue-login-register h2 strong { + font-weight: bold; +} +.rcue-login-register .background-area { + border-top: 0.1em rgba(255, 255, 255, 0.05) solid; + border-bottom: 0.1em rgba(255, 255, 255, 0.05) solid; + background-color: rgba(0, 0, 0, 0.3); + padding: 3em 0 3em 10em; + margin-top: 2.7em; + width: 100%; + min-width: 120em; +} +.rcue-login-register .form-area.separator, +.rcue-login-register .form-area.social, +.rcue-login-register .form-area.social.separator { + background-repeat: no-repeat; + background-position: 42.7em center; +} +.rcue-login-register .form-area.separator { + background-image: url(../img/login-register-separator.png); + background-position: 43.2em center; +} +.rcue-login-register .form-area.social { + background-image: url(../img/login-register-social.png); +} +.rcue-login-register .form-area.social.separator { + background-image: url(../img/login-register-social-separator.png); +} +.rcue-login-register .background-area .section { + float: left; + padding: 0 4.5em 0 4.6em; + width: auto; + position: relative; +} +.rcue-login-register .background-area .section, +.rcue-login-register .background-area .social .section { + padding-top: 1.5em; + padding-bottom: 1.5em; +} +.rcue-login-register .background-area .section h3 { + display: none; +} +.rcue-login-register .background-area .section:first-child { + padding-right: 4.5em; +} +.rcue-login-register .section > p { + font-size: 1.3em; + margin-bottom: 1.53846153846154em; + line-height: 1.3em; +} +.rcue-login-register .section.app-form { + padding-left: 0; + position: relative; +} +.rcue-login-register form > div { + margin-bottom: 1em; +} +.rcue-login-register label, +.rcue-login-register .social-login > p { + display: inline-block; + font-size: 1.4em; + font-weight: 400; +} +.rcue-login-register label { + width: 8.21428571428571em; +} +.rcue-login-register label.two-lines { + float: left; + margin-top: -0.14285714285714em; + line-height: 1.1em; +} +.rcue-login-register input[type="text"], +.rcue-login-register input[type="password"] { + width: 24.7272727272727em; + /* 272px */ + +} +.rcue-login-register form > div.aside-btn { + float: left; + font-size: 1.1em; + margin-left: 10.4545454545454em; + margin-top: 0.90909090909091em; + margin-bottom: 0; +} +.rcue-login-register form > div.aside-btn label { + font-size: 1em; + width: auto; +} +.rcue-login-register form > div.aside-btn input[type="checkbox"] { + margin-bottom: 0.54545454545455em; + /* 6px */ +} +.rcue-login-register form > input[type="button"], +.rcue-login-register form > input[type="submit"]{ + float: right; + margin-top: 0.76923076923077em; + margin-left: 0.90909090909091em; + /* 10px */ + +} +.rcue-login-register p.subtitle { + font-size: 1.1em; + color: #999; + position: absolute; + right: 4.09090909090909em; + top: -0.636363636363636em; +} +.rcue-login-register .feedback.bottom-left { + left: 35.7em; + bottom: 17em; + min-width: 35em; +} +.rcue-login-register input.error[type="text"], +.rcue-login-register input.error[type="password"], +.rcue-login-register input.error[type="email"] { + background-color: #F8E7E7; +} +.rcue-login-register .section.social-login > span { + display: none; +} +.rcue-login-register .section.social-login > p { + float: left; + margin-top: 0.28571428571429em; + /* 14px */ + + width: 6.78571428571429em; + /* 95px */ + +} +.rcue-login-register .section.social-login > ul { + float: left; +} +.rcue-login-register .section.social-login li { + margin-bottom: 2em; +} +.rcue-login-register .section.social-login li:last-child { + margin-bottom: 0; +} +.rcue-login-register .section.info-area { + padding-right: 0; +} +.rcue-login-register .section.info-area p, +.rcue-login-register .section.info-area li { + font-size: 1.4em; + margin-bottom: 1.64285714285714em; +} +.rcue-login-register .section.info-area li { + color: #999; + margin-bottom: 1em; +} +.rcue-login-register .section.info-area li:last-child { + margin-bottom: 0; +} +@media screen and (min-width: 1280px) { + .rcue-login-register { + background-size: 100% auto; + } +} +/* Social buttons */ +.zocial, +a.zocial { + padding: 0; + line-height: 2.3em; + height: 2.3em; + width: 131px; + border-radius: 2px; + box-shadow: none; + background-image: none; + text-shadow: none; +} +.zocial .text, +a.zocial .text { + font-size: 1.2em; + line-height: 1.25em; + text-align: center; + display: block; + font-family: "Open Sans", sans-serif; + font-weight: normal; + border-left: 1px solid rgba(0, 0, 0, 0.15); + margin-left: 3em; + /* 36 px */ + + margin-top: 0.25em; + /* 3px */ + +} +.zocial:hover, +a.zocial:hover, +.zocial:active, +a.zocial:active, +.zocial:focus, +a.zocial:focus { + text-decoration: none; + background-image: none; +} +.zocial:hover, +a.zocial:hover { + background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); +} +.zocial:before, +a.zocial:before { + margin: 0; + padding: 0; + box-shadow: none; + border: none; + width: 3em; + /* 36px */ + +} +.zocial.facebook:before { + width: 2.66666666666667em; + /* 32px */ +} +/* Register page */ +.rcue-login-register.register label { + width: 7.5em; + /* 105px */ + +} +.rcue-login-register.register input[type="text"], +.rcue-login-register.register input[type="email"], +.rcue-login-register.register input[type="password"] { + width: 22.9090909090909em; + /* 252px */ + +} +.rcue-login-register.register form > div.aside-btn { + margin-left: 9.54545454545454em; + /* 105px */ + + width: 12.5454545454546em; + /* 138px */ + +} +.rcue-login-register.register form > div.aside-btn p { + line-height: 1.3em; +} +.rcue-login-register p.powered { + font-size: 1.1em; + margin-top: 1.27272727272727em; + text-align: right; + margin-right: 5.81818181818182em; +} +.rcue-login-register p.powered a { + color: #666; +} +.rcue-login-register p.powered a:hover { + color: #0099D3; +} +/* Forgot Password page */ +.rcue-login-register.reset .background-area .section.app-form { + width: 43.2em; +} +.rcue-login-register.oauth .form-actions { + margin-bottom: 0; + margin-top: 2em; +} +.rcue-login-register .background-area .content-area { + width: 50em; +} +.rcue-login-register .background-area .content-area ul { + border-bottom: 1px solid #34393C; + margin-bottom: 2em; +} +.rcue-login-register .background-area .content-area ul li { + border-top: 1px solid #34393C; + padding: 2em; + position: relative; +} +.rcue-login-register .background-area .content-area ul li span { + font-size: 1.3em; + line-height: 1.3em; +} + +.rcue-login-register .background-area .content-area ul li span:first-child { + padding-right: 11.5384615384615em; +} + +.rcue-login-register .background-area .content-area ul li span.parent { + position: absolute; + left: 26em; + top: 1.53846153846154em; + width: 12.3076923076923em; +} + +.rcue-login-register .background-area .content-area ul li span.icon-info { + float: right; + margin-top: 0.5em; +} +.rcue-login-register .background-area .content-area p.terms { + color: #999999; + font-size: 1.1em; + line-height: 1.3em; +} + +.rcue-login-register.reset p.subtitle { + margin-bottom: 10px; + position: inherit; + text-align: right; +} + +.rcue-login-register .background-area p.instruction { + font-size: 1.3em; + line-height: 1.3em; + margin-bottom: 1.53846em; +} + +.rcue-login-register .background-area p.instruction.instruction.second { + color: #999999; +} +.rcue-login-register .background-area p.instruction + .instruction.second { + margin-top: -1.23077em; +} + +.rcue-login-register .background-area a.link-right { + float: right; + font-size: 1.3em; +} + +.rcue-login-register.totp .form-area { + background-image: none; +} +.rcue-login-register.reset .form-area p.instruction { + font-size: 1.3em; + line-height: 1.3em; + margin-bottom: 1.81818181818182em; +} +.rcue-login-register.totp { + min-height: 0; +} +.rcue-login-register.totp ol li { + margin-bottom: 3em; + width: 100%; +} +.rcue-login-register.totp ol li p { + font-size: 1.3em; + margin-bottom: 1.92307692307692em; +} +.rcue-login-register.totp ol li p strong { + text-indent: -1em; + float: left; + font-size: 1.84615384615385em; + font-weight: normal; + margin-top: -0.20833333333333em; + color: #999; +} +.rcue-login-register.totp ol li img { + border: 7px solid #fff; + width: 150px; +} +.rcue-login-register.totp ol li .code { + font-size: 1.3em; + margin-left: 1.53846153846154em; +} +.rcue-login-register.totp ol li form { + width: 357px; +} +.rcue-login-register.totp ol li form input[type="text"] { + width: 22em; +} +.rcue-login-register.totp ol li form input[type="submit"] { + float: right; +} + +.rcue-login-register.totp ol li:last-child { + margin-bottom: 0; +} +.rcue-login-register.totp .content { + position: inherit; + margin-top: 16em; +} +.rcue-login-register.email .background-area .section { + width: 41.2em; +} +.rcue-login-register.email .background-area .section.email { + width: 45.8em; +} +.rcue-login-register.email label { + width: 6.78571428571429em; +} +.rcue-login-register.email .feedback.bottom-left { + left: 38.3em; +} diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/reset.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/reset.css new file mode 100644 index 00000000000..7f0b5b6b086 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/reset.css @@ -0,0 +1,71 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* Clearfix */ + +.clearfix:after { + content: "."; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; +} + +.clearfix { + display: inline-block; +} + +html[xmlns] .clearfix { + display: block; +} + +* html .clearfix { + height: 1%; +} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/styles.css new file mode 100644 index 00000000000..2dab25a839d --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/styles.css @@ -0,0 +1,12 @@ +@IMPORT url("reset.css"); +@IMPORT url("base.css"); +@IMPORT url("forms.css"); +@IMPORT url("../lib/zocial/zocial.css"); +@IMPORT url("login-register.css"); + +.zocial.google { + background-color: #dd4b39 !important; +} +.zocial.google:before { + content: "+" !important; +} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg new file mode 100644 index 00000000000..1bbfc951f34 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/favicon.ico b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/favicon.ico new file mode 100644 index 00000000000..8864f005430 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/favicon.ico differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png new file mode 100644 index 00000000000..6f2d9d2aeb1 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png new file mode 100644 index 00000000000..0dd500445d7 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png new file mode 100644 index 00000000000..03cc0c45dcc Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png new file mode 100644 index 00000000000..640bd71cab7 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png new file mode 100644 index 00000000000..6f2d9d2aeb1 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png new file mode 100644 index 00000000000..f9392a356fd Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/keycloak-logo.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/keycloak-logo.png new file mode 100644 index 00000000000..45c51c251c5 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/keycloak-logo.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png new file mode 100644 index 00000000000..d626a45f65a Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png new file mode 100644 index 00000000000..3f089294187 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png new file mode 100644 index 00000000000..e86d738d6c8 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg new file mode 100644 index 00000000000..a50a2fc346e Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png new file mode 100644 index 00000000000..7ddd4ad848f Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png differ diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/sprites-white.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/sprites-white.png new file mode 100755 index 00000000000..cf70cf16684 Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/sprites-white.png differ diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.eot b/forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.eot similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.eot rename to forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.eot diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.svg b/forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.svg similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.svg rename to forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.svg diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.ttf b/forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.ttf similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.ttf rename to forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.ttf diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.woff b/forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.woff similarity index 100% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.woff rename to forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.woff diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial.css similarity index 99% rename from forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.css rename to forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial.css index 768aff1d165..0d53c0b4d23 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.css +++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial.css @@ -3,16 +3,16 @@ font-family: 'zocial'; font-style: normal; font-weight: normal; - src: url('./zocial-regular-webfont.eot'); - src: url('./zocial-regular-webfont.eot?#iefix') format('embedded-opentype'), url('./zocial-regular-webfont.woff') format('woff'), url('./zocial-regular-webfont.ttf') format('truetype'), url('./zocial-regular-webfont.svg#zocialregular') format('svg'); + src: url('zocial-regular-webfont.eot'); + src: url('zocial-regular-webfont.eot?#iefix') format('embedded-opentype'), url('zocial-regular-webfont.woff') format('woff'), url('zocial-regular-webfont.ttf') format('truetype'), url('zocial-regular-webfont.svg#zocialregular') format('svg'); } @font-face { font-family: 'zocial'; - src: url('./zocial-regular-webfont.eot'); + src: url('zocial-regular-webfont.eot'); } @font-face { font-family: 'zocial'; - src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), url('./zocial-regular-webfont.ttf') format('truetype'), url('./zocial-regular-webfont.svg#zocialregular') format('svg'); + src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), url('zocial-regular-webfont.ttf') format('truetype'), url('zocial-regular-webfont.svgocialregular') format('svg'); font-weight: normal; font-style: normal; } diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/theme.properties b/forms/common-themes/src/main/resources/theme/login/rcue/theme.properties new file mode 100644 index 00000000000..298aa65ac23 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/rcue/theme.properties @@ -0,0 +1,2 @@ +parent=base +styles=css/styles.css diff --git a/forms/login-api/pom.xml b/forms/login-api/pom.xml new file mode 100755 index 00000000000..ed4d5f24876 --- /dev/null +++ b/forms/login-api/pom.xml @@ -0,0 +1,49 @@ + + + + keycloak-forms + org.keycloak + 1.0-alpha-2-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-login-api + Keycloak Login API + + + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.jboss.resteasy + jaxrs-api + + + org.jboss.resteasy + resteasy-jaxrs + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java new file mode 100644 index 00000000000..c693be289cd --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java @@ -0,0 +1,49 @@ +package org.keycloak.login; + +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public interface LoginForms { + + public Response createResponse(UserModel.RequiredAction action); + + public Response createLogin(); + + public Response createPasswordReset(); + + public Response createUsernameReminder(); + + public Response createLoginTotp(); + + public Response createRegistration(); + + public Response createErrorPage(); + + public Response createOAuthGrant(); + + public LoginForms setAccessCode(String accessCodeId, String accessCode); + + public LoginForms setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested); + + public LoginForms setError(String message); + + public LoginForms setSuccess(String message); + + public LoginForms setWarning(String message); + + public LoginForms setUser(UserModel user); + + public LoginForms setClient(UserModel client); + + public LoginForms setFormData(MultivaluedMap formData); + + public LoginForms setStatus(Response.Status status); + +} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java new file mode 100644 index 00000000000..817db88f1e9 --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java @@ -0,0 +1,17 @@ +package org.keycloak.login; + +import java.util.ServiceLoader; + +/** + * @author Stian Thorgersen + */ +public class LoginFormsLoader { + + private LoginFormsLoader() { + } + + public static LoginFormsProvider load() { + return ServiceLoader.load(LoginFormsProvider.class).iterator().next(); + } + +} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java new file mode 100644 index 00000000000..f0d1300c89b --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsPages.java @@ -0,0 +1,10 @@ +package org.keycloak.login; + +/** + * @author Stian Thorgersen + */ +public enum LoginFormsPages { + + LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_USERNAME_REMINDER, REGISTER, ERROR, LOGIN_UPDATE_PROFILE; + +} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java new file mode 100644 index 00000000000..d8e61b4fde3 --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -0,0 +1,15 @@ +package org.keycloak.login; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.models.RealmModel; + +import javax.ws.rs.core.UriInfo; + +/** + * @author Stian Thorgersen + */ +public interface LoginFormsProvider { + + public LoginForms createForms(RealmModel realm, HttpRequest request, UriInfo uriInfo); + +} diff --git a/forms/login-freemarker/pom.xml b/forms/login-freemarker/pom.xml new file mode 100755 index 00000000000..3922a3df7d0 --- /dev/null +++ b/forms/login-freemarker/pom.xml @@ -0,0 +1,75 @@ + + + + keycloak-forms + org.keycloak + 1.0-alpha-2-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-login-freemarker + Keycloak Login FreeMarker + + + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-forms-common-freemarker + ${project.version} + + + org.keycloak + keycloak-login-api + ${project.version} + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.keycloak + keycloak-services + ${project.version} + + + org.keycloak + keycloak-social-core + ${project.version} + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + org.freemarker + freemarker + + + org.jboss + jboss-vfs + 3.2.2.Final + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java new file mode 100644 index 00000000000..30b15faf2e2 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java @@ -0,0 +1,273 @@ +package org.keycloak.login.freemarker; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.freemarker.FreeMarkerException; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeLoader; +import org.keycloak.login.LoginForms; +import org.keycloak.login.LoginFormsPages; +import org.keycloak.login.freemarker.model.LoginBean; +import org.keycloak.login.freemarker.model.MessageBean; +import org.keycloak.login.freemarker.model.OAuthGrantBean; +import org.keycloak.login.freemarker.model.ProfileBean; +import org.keycloak.login.freemarker.model.RealmBean; +import org.keycloak.login.freemarker.model.RegisterBean; +import org.keycloak.login.freemarker.model.SocialBean; +import org.keycloak.login.freemarker.model.TotpBean; +import org.keycloak.login.freemarker.model.UrlBean; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.email.EmailException; +import org.keycloak.services.email.EmailSender; +import org.keycloak.services.messages.Messages; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerLoginForms implements LoginForms { + + private static final Logger logger = Logger.getLogger(FreeMarkerLoginForms.class); + + private String message; + private String accessCodeId; + private String accessCode; + private Response.Status status = Response.Status.OK; + private List realmRolesRequested; + private MultivaluedMap resourceRolesRequested; + + public static enum MessageType {SUCCESS, WARNING, ERROR} + + private MessageType messageType = MessageType.ERROR; + + private MultivaluedMap formData; + + private RealmModel realm; + + // TODO Remove + private HttpRequest request; + + private UserModel user; + + private UserModel client; + + private UriInfo uriInfo; + + FreeMarkerLoginForms(RealmModel realm, org.jboss.resteasy.spi.HttpRequest request, UriInfo uriInfo) { + this.realm = realm; + this.request = request; + this.uriInfo = uriInfo; + } + + public Response createResponse(UserModel.RequiredAction action) { + String actionMessage; + LoginFormsPages page; + + switch (action) { + case CONFIGURE_TOTP: + actionMessage = Messages.ACTION_WARN_TOTP; + page = LoginFormsPages.LOGIN_CONFIG_TOTP; + break; + case UPDATE_PROFILE: + actionMessage = Messages.ACTION_WARN_PROFILE; + page = LoginFormsPages.LOGIN_UPDATE_PROFILE; + break; + case UPDATE_PASSWORD: + actionMessage = Messages.ACTION_WARN_PASSWD; + page = LoginFormsPages.LOGIN_UPDATE_PASSWORD; + break; + case VERIFY_EMAIL: + try { + new EmailSender(realm.getSmtpConfig()).sendEmailVerification(user, realm, accessCodeId, uriInfo); + } catch (EmailException e) { + return setError("emailSendError").createErrorPage(); + } + + actionMessage = Messages.ACTION_WARN_EMAIL; + page = LoginFormsPages.LOGIN_VERIFY_EMAIL; + break; + default: + return Response.serverError().build(); + } + + if (message == null) { + setWarning(actionMessage); + } + + return createResponse(page); + } + + private Response createResponse(LoginFormsPages page) { + MultivaluedMap queryParameterMap = uriInfo.getQueryParameters(); + + String requestURI = uriInfo.getBaseUri().getPath(); + UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); + + for (String k : queryParameterMap.keySet()) { + uriBuilder.replaceQueryParam(k, queryParameterMap.get(k).toArray()); + } + + if (accessCode != null) { + uriBuilder.replaceQueryParam("code", accessCode); + } + + Map attributes = new HashMap(); + + Theme theme; + try { + theme = ThemeLoader.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN); + } 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(); + } + + if (message != null) { + attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType)); + } + + URI baseUri = uriBuilder.build(); + + if (realm != null) { + attributes.put("realm", new RealmBean(realm)); + attributes.put("social", new SocialBean(realm, baseUri)); + attributes.put("url", new UrlBean(realm, theme, baseUri)); + } + + attributes.put("login", new LoginBean(formData)); + + switch (page) { + case LOGIN_CONFIG_TOTP: + attributes.put("totp", new TotpBean(user, baseUri)); + break; + case LOGIN_UPDATE_PROFILE: + attributes.put("user", new ProfileBean(user)); + break; + case REGISTER: + attributes.put("register", new RegisterBean(formData)); + break; + case OAUTH_GRANT: + attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested)); + 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(); + } + } + + public Response createLogin() { + return createResponse(LoginFormsPages.LOGIN); + } + + public Response createPasswordReset() { + return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD); + } + + public Response createUsernameReminder() { + return createResponse(LoginFormsPages.LOGIN_USERNAME_REMINDER); + } + + public Response createLoginTotp() { + return createResponse(LoginFormsPages.LOGIN_TOTP); + } + + public Response createRegistration() { + return createResponse(LoginFormsPages.REGISTER); + } + + public Response createErrorPage() { + setStatus(Response.Status.INTERNAL_SERVER_ERROR); + return createResponse(LoginFormsPages.ERROR); + } + + public Response createOAuthGrant() { + return createResponse(LoginFormsPages.OAUTH_GRANT); + } + + public FreeMarkerLoginForms setError(String message) { + this.message = message; + this.messageType = MessageType.ERROR; + return this; + } + + public FreeMarkerLoginForms setSuccess(String message) { + this.message = message; + this.messageType = MessageType.SUCCESS; + return this; + } + + public FreeMarkerLoginForms setWarning(String message) { + this.message = message; + this.messageType = MessageType.WARNING; + return this; + } + + public FreeMarkerLoginForms setUser(UserModel user) { + this.user = user; + return this; + } + + public FreeMarkerLoginForms setClient(UserModel client) { + this.client = client; + return this; + } + + public FreeMarkerLoginForms setFormData(MultivaluedMap formData) { + this.formData = formData; + return this; + } + + @Override + public LoginForms setAccessCode(String accessCodeId, String accessCode) { + this.accessCodeId = accessCodeId; + this.accessCode = accessCode; + return this; + } + + @Override + public LoginForms setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested) { + this.realmRolesRequested = realmRolesRequested; + this.resourceRolesRequested = resourceRolesRequested; + return this; + } + + @Override + public LoginForms setStatus(Response.Status status) { + this.status = status; + return this; + } + +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java new file mode 100644 index 00000000000..b15cadae8dc --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -0,0 +1,20 @@ +package org.keycloak.login.freemarker; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.login.LoginForms; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.RealmModel; + +import javax.ws.rs.core.UriInfo; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { + + @Override + public LoginForms createForms(RealmModel realm, HttpRequest request, UriInfo uriInfo) { + return new FreeMarkerLoginForms(realm, request, uriInfo); + } + +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java new file mode 100644 index 00000000000..ad96e3864bb --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java @@ -0,0 +1,39 @@ +package org.keycloak.login.freemarker; + +import org.keycloak.login.LoginFormsPages; + +/** + * @author Stian Thorgersen + */ +public class Templates { + + public static String getTemplate(LoginFormsPages page) { + switch (page) { + case LOGIN: + return "login.ftl"; + case LOGIN_TOTP: + return "login-totp.ftl"; + case LOGIN_CONFIG_TOTP: + return "login-config-totp.ftl"; + case LOGIN_VERIFY_EMAIL: + return "login-verify-email.ftl"; + case OAUTH_GRANT: + return "login-oauth-grant.ftl"; + case LOGIN_RESET_PASSWORD: + return "login-reset-password.ftl"; + case LOGIN_UPDATE_PASSWORD: + return "login-update-password.ftl"; + case LOGIN_USERNAME_REMINDER: + return "login-username-reminder.ftl"; + case REGISTER: + return "register.ftl"; + case ERROR: + return "error.ftl"; + case LOGIN_UPDATE_PROFILE: + return "login-update-profile.ftl"; + default: + throw new IllegalArgumentException(); + } + } + +} diff --git a/forms/src/main/java/org/keycloak/forms/LoginBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/LoginBean.java similarity index 78% rename from forms/src/main/java/org/keycloak/forms/LoginBean.java rename to forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/LoginBean.java index 4e4184e3ac8..d54f5b460c7 100755 --- a/forms/src/main/java/org/keycloak/forms/LoginBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/LoginBean.java @@ -19,29 +19,20 @@ * 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; - -import org.keycloak.forms.model.RequiredCredential; +package org.keycloak.login.freemarker.model; import javax.ws.rs.core.MultivaluedMap; -import java.util.LinkedList; -import java.util.List; /** * @author Stian Thorgersen */ public class LoginBean { - private RealmBean realm; - private String username; private String password; - public LoginBean(RealmBean realm, MultivaluedMap formData){ - - this.realm = realm; - + public LoginBean(MultivaluedMap formData){ if (formData != null) { username = formData.getFirst("username"); password = formData.getFirst("password"); @@ -56,12 +47,4 @@ public class LoginBean { return password; } - public RealmBean getRealm() { - return realm; - } - - public void setRealm(RealmBean realm) { - this.realm = realm; - } - } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java new file mode 100644 index 00000000000..a6b36d4678e --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java @@ -0,0 +1,60 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.login.freemarker.model; + +import org.keycloak.login.freemarker.FreeMarkerLoginForms; + +/** + * @author Stian Thorgersen + */ +public class MessageBean { + + private String summary; + + private FreeMarkerLoginForms.MessageType type; + + public MessageBean(String message, FreeMarkerLoginForms.MessageType type) { + this.summary = message; + this.type = type; + } + + public String getSummary() { + return summary; + } + + public String getType() { + return this.type.toString().toLowerCase(); + } + + public boolean isSuccess() { + return FreeMarkerLoginForms.MessageType.SUCCESS.equals(this.type); + } + + public boolean isWarning() { + return FreeMarkerLoginForms.MessageType.WARNING.equals(this.type); + } + + public boolean isError() { + return FreeMarkerLoginForms.MessageType.ERROR.equals(this.type); + } + +} \ No newline at end of file diff --git a/forms/src/main/java/org/keycloak/forms/OAuthGrantBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java similarity index 70% rename from forms/src/main/java/org/keycloak/forms/OAuthGrantBean.java rename to forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java index 560c37a1219..4c04f12a484 100755 --- a/forms/src/main/java/org/keycloak/forms/OAuthGrantBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java @@ -19,7 +19,7 @@ * 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.login.freemarker.model; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; @@ -33,55 +33,34 @@ import java.util.List; */ public class OAuthGrantBean { - private MultivaluedMap resourceRolesRequested; private List realmRolesRequested; + private MultivaluedMap resourceRolesRequested; + private String code; private UserModel client; private String oAuthCode; private String action; - public String getAction() { - return action; + public OAuthGrantBean(String code, UserModel client, List realmRolesRequested, MultivaluedMap resourceRolesRequested) { + this.code = code; + this.client = client; + this.realmRolesRequested = realmRolesRequested; + this.resourceRolesRequested = resourceRolesRequested; } - public void setAction(String action) { - this.action = action; - } - - public List getResourceNames(){ - return new ArrayList(resourceRolesRequested.keySet()); + public String getCode() { + return code; } public MultivaluedMap getResourceRolesRequested() { return resourceRolesRequested; } - public void setResourceRolesRequested(MultivaluedMap resourceRolesRequested) { - this.resourceRolesRequested = resourceRolesRequested; - } - public List getRealmRolesRequested() { return realmRolesRequested; } - public void setRealmRolesRequested(List realmRolesRequested) { - this.realmRolesRequested = realmRolesRequested; - } - - public UserModel getClient() { - return client; - } - - public void setClient(UserModel client) { - this.client = client; - } - - // lowercase "o" needed for FM template to access the property - public String getoAuthCode() { - return oAuthCode; - } - - public void setoAuthCode(String oAuthCode) { - this.oAuthCode = oAuthCode; + public String getClient() { + return client.getLoginName(); } } diff --git a/forms/src/main/java/org/keycloak/forms/UserBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java similarity index 68% rename from forms/src/main/java/org/keycloak/forms/UserBean.java rename to forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java index 1662623ca49..bb34c46696e 100755 --- a/forms/src/main/java/org/keycloak/forms/UserBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java @@ -19,50 +19,31 @@ * 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.login.freemarker.model; import org.keycloak.models.UserModel; /** * @author Stian Thorgersen */ -public class UserBean { +public class ProfileBean { private UserModel user; - private String firstName; - private String lastName; - private String email; - private String username; - - public UserBean(UserModel user){ + public ProfileBean(UserModel user){ this.user = user; - if (user != null){ - this.firstName = user.getFirstName(); - this.lastName = user.getLastName(); - this.username = user.getLoginName(); - this.email = user.getEmail(); - } } public String getFirstName() { - return firstName; + return user.getFirstName(); } public String getLastName() { - return lastName; - } - - public String getUsername() { - return username; + return user.getLastName(); } public String getEmail() { - return email; - } - - UserModel getUser() { - return user; + return user.getEmail(); } } diff --git a/forms/src/main/java/org/keycloak/forms/RealmBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java similarity index 82% rename from forms/src/main/java/org/keycloak/forms/RealmBean.java rename to forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java index 322580ca9b1..8a71244ffd8 100755 --- a/forms/src/main/java/org/keycloak/forms/RealmBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java @@ -19,9 +19,8 @@ * 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.login.freemarker.model; -import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; /** @@ -31,30 +30,14 @@ public class RealmBean { private RealmModel realm; - private boolean saas; - - public RealmBean(RealmModel realmModel) { realm = realmModel; - saas = Constants.ADMIN_REALM.equals(realmModel.getId()); - } - - public String getId() { - return realm.getId(); } public String getName() { return realm.getName(); } - public RealmModel getRealm() { - return realm; - } - - public boolean isSaas() { - return saas; - } - public boolean isSocial() { return realm.isSocial(); } diff --git a/forms/src/main/java/org/keycloak/forms/RegisterBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RegisterBean.java similarity index 97% rename from forms/src/main/java/org/keycloak/forms/RegisterBean.java rename to forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RegisterBean.java index d801a422058..362951e78ae 100755 --- a/forms/src/main/java/org/keycloak/forms/RegisterBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RegisterBean.java @@ -19,7 +19,7 @@ * 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.login.freemarker.model; import javax.ws.rs.core.MultivaluedMap; import java.util.HashMap; @@ -33,7 +33,6 @@ public class RegisterBean { private Map formData = new HashMap(); public RegisterBean(MultivaluedMap formData) { - this.formData = new HashMap(); if (formData != null) { diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/SocialBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/SocialBean.java new file mode 100755 index 00000000000..c9f1feee8d8 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/SocialBean.java @@ -0,0 +1,93 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.login.freemarker.model; + +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.flows.Urls; +import org.keycloak.social.SocialLoader; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class SocialBean { + + private boolean displaySocial; + + private List providers; + + public SocialBean(RealmModel realm, URI baseURI) { + Map socialConfig = realm.getSocialConfig(); + if (realm.isSocial() && !socialConfig.isEmpty()) { + displaySocial = true; + providers = new LinkedList(); + + UriBuilder socialLoginUrlBuilder = UriBuilder.fromUri(Urls.socialRedirectToProviderAuth(baseURI, realm.getName())); + for (org.keycloak.social.SocialProvider p : SocialLoader.load()) { + if (socialConfig.containsKey(p.getId() + ".key") && socialConfig.containsKey(p.getId() + ".secret")) { + String loginUrl = socialLoginUrlBuilder.replaceQueryParam("provider_id", p.getId()).build().toString(); + providers.add(new SocialProvider(p.getId(), p.getName(), loginUrl)); + } + } + } + } + + public List getProviders() { + return providers; + } + + public boolean isDisplaySocialProviders() { + return displaySocial; + } + + public static class SocialProvider { + + private String id; + private String name; + private String loginUrl; + + public SocialProvider(String id, String name, String loginUrl) { + this.id = id; + this.name = name; + this.loginUrl = loginUrl; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getLoginUrl() { + return loginUrl; + } + + } + +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java new file mode 100755 index 00000000000..bfc8d86cd6d --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java @@ -0,0 +1,86 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.login.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; + +/** + * @author Stian Thorgersen + */ +public class TotpBean { + + private String totpSecret; + private String totpSecretEncoded; + private boolean enabled; + private String contextUrl; + + public TotpBean(UserModel user, URI baseUri) { + this.enabled = user.isTotp(); + this.contextUrl = baseUri.getPath(); + + totpSecret = randomString(20); + totpSecretEncoded = Base32.encode(totpSecret.getBytes()); + } + + private static String randomString(int length) { + String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890"; + Random r = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + char c = chars.charAt(r.nextInt(chars.length())); + sb.append(c); + } + return sb.toString(); + } + + public boolean isEnabled() { + return enabled; + } + + public String getTotpSecret() { + return totpSecret; + } + + public String getTotpSecretEncoded() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < totpSecretEncoded.length(); i += 4) { + sb.append(totpSecretEncoded.substring(i, i + 4 < totpSecretEncoded.length() ? i + 4 : totpSecretEncoded.length())); + if (i + 4 < totpSecretEncoded.length()) { + sb.append(" "); + } + } + return sb.toString(); + } + + public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException { + String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8"); + return contextUrl + "qrcode" + "?size=246x246&contents=" + contents; + } + +} + diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java new file mode 100755 index 00000000000..38e78630420 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java @@ -0,0 +1,97 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.login.freemarker.model; + +import org.keycloak.freemarker.Theme; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.TokenService; +import org.keycloak.services.resources.flows.Urls; + +import java.net.URI; + +/** + * @author Stian Thorgersen + */ +public class UrlBean { + + private URI baseURI; + + private Theme theme; + + private String realm; + + public UrlBean(RealmModel realm, Theme theme, URI baseURI) { + this.realm = realm.getName(); + this.theme = theme; + this.baseURI = baseURI; + } + + public String getLoginAction() { + return Urls.realmLoginAction(baseURI, realm).toString(); + } + + public String getLoginUrl() { + return Urls.realmLoginPage(baseURI, realm).toString(); + } + + public String getRegistrationAction() { + return Urls.realmRegisterAction(baseURI, realm).toString(); + } + + public String getRegistrationUrl() { + return Urls.realmRegisterPage(baseURI, realm).toString(); + } + + public String getLoginUpdatePasswordUrl() { + return Urls.loginActionUpdatePassword(baseURI, realm).toString(); + } + + public String getLoginUpdateTotpUrl() { + return Urls.loginActionUpdateTotp(baseURI, realm).toString(); + } + + public String getLoginUpdateProfileUrl() { + return Urls.loginActionUpdateProfile(baseURI, realm).toString(); + } + + public String getLoginPasswordResetUrl() { + return Urls.loginPasswordReset(baseURI, realm).toString(); + } + + public String getLoginUsernameReminderUrl() { + return Urls.loginUsernameReminder(baseURI, realm).toString(); + } + + public String getLoginEmailVerificationUrl() { + return Urls.loginActionEmailVerification(baseURI, realm).toString(); + } + + public String getOauthAction() { + return Urls.realmOauthAction(baseURI, realm).toString(); + } + + public String getResourcesPath() { + URI uri = Urls.themeRoot(baseURI); + return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName(); + } + +} diff --git a/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider new file mode 100644 index 00000000000..ae28fdb717f --- /dev/null +++ b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider @@ -0,0 +1 @@ +org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider \ No newline at end of file diff --git a/forms/pom.xml b/forms/pom.xml index f444dee5e42..9471ddf3c7a 100755 --- a/forms/pom.xml +++ b/forms/pom.xml @@ -7,54 +7,19 @@ ../pom.xml 4.0.0 + pom keycloak-forms Keycloak Forms - - - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-model-api - ${project.version} - - - org.keycloak - keycloak-services - ${project.version} - - - org.keycloak - keycloak-social-core - ${project.version} - - - org.jboss.resteasy - resteasy-jaxrs - provided - - - org.freemarker - freemarker - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.6 - 1.6 - - - - + + common-freemarker + common-themes + account-api + account-freemarker + login-api + login-freemarker + diff --git a/forms/src/main/java/org/keycloak/forms/SocialBean.java b/forms/src/main/java/org/keycloak/forms/SocialBean.java deleted file mode 100755 index 939aa2953df..00000000000 --- a/forms/src/main/java/org/keycloak/forms/SocialBean.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.forms; - -import org.keycloak.forms.model.SocialProvider; -import org.keycloak.services.resources.flows.Urls; - -import javax.ws.rs.core.UriBuilder; -import java.net.URI; -import java.util.LinkedList; -import java.util.List; - -/** - * @author Stian Thorgersen - */ -public class SocialBean { - - private RealmBean realm; - - private RegisterBean registerBean; - - private UrlBean url; - - private List providers; - - public SocialBean(RealmBean realm, List providers, RegisterBean registerBean, UrlBean url) { - this.realm = realm; - this.registerBean = registerBean; - this.url = url; - - URI baseURI = url.getBaseURI(); - - UriBuilder socialLoginUrlBuilder = UriBuilder.fromUri(Urls.socialRedirectToProviderAuth(baseURI, realm.getId())); - - this.providers = new LinkedList(); - for (org.keycloak.social.SocialProvider p : providers) { - String loginUrl = socialLoginUrlBuilder.replaceQueryParam("provider_id", p.getId()).build().toString(); - this.providers.add(new SocialProvider(p.getId(), p.getName(), loginUrl)); - } - } - - public List getProviders() { - return providers; - } - - // Display panel with social providers just in case that social is enabled for realm, but we are not in the middle of registration with social - public boolean isDisplaySocialProviders() { - return realm.isSocial() && !providers.isEmpty(); - } - - public RealmBean getRealm() { - return realm; - } - - public void setRealm(RealmBean realm) { - this.realm = realm; - } - - public UrlBean getUrl() { - return url; - } - - public void setUrl(UrlBean url) { - this.url = url; - } - - public RegisterBean getRegisterBean() { - return registerBean; - } - - public void setRegisterBean(RegisterBean registerBean) { - this.registerBean = registerBean; - } -} diff --git a/forms/src/main/java/org/keycloak/forms/TemplateBean.java b/forms/src/main/java/org/keycloak/forms/TemplateBean.java deleted file mode 100755 index 468f7963538..00000000000 --- a/forms/src/main/java/org/keycloak/forms/TemplateBean.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.forms; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Stian Thorgersen - */ -public class TemplateBean { - - private RealmBean realm; - - private String theme = "default"; - - private String themeUrl; - - private Map themeConfig; - - private String formsPath; - - - public TemplateBean(String contextPath) { - formsPath = contextPath + "/forms"; - - // TODO Get theme name from realm - theme = "default"; - themeUrl = formsPath + "/theme/" + theme; - - themeConfig = new HashMap(); - - themeConfig.put("styles", themeUrl + "/styles.css"); - - themeConfig.put("logo", themeUrl + "/img/keycloak-logo.png"); - themeConfig.put("background", themeUrl + "/img/login-screen-background.jpg"); - themeConfig.put("displayPoweredBy", true); - } - - public String getFormsPath() { - return formsPath; - } - - public Map getThemeConfig() { - return themeConfig; - } - - public String getTheme() { - return theme; - } - - public String getThemeUrl() { - return themeUrl; - } - - public RealmBean getRealm() { - return realm; - } - - public void setRealm(RealmBean realm) { - this.realm = realm; - } - -} diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java deleted file mode 100755 index 36ab144770f..00000000000 --- a/forms/src/main/java/org/keycloak/forms/UrlBean.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.forms; - -import org.keycloak.services.resources.flows.Urls; - -import java.net.URI; - -/** - * @author Stian Thorgersen - */ -public class UrlBean { - - private URI baseURI; - - private RealmBean realm; - - private String referrerURI; - - public UrlBean(RealmBean realm, URI baseURI, String referrerURI){ - this.realm = realm; - this.baseURI = baseURI; - this.referrerURI = referrerURI; - } - - protected String getRealmIdentifier() { - return realm.getName(); - } - - public RealmBean getRealm() { - return realm; - } - - public void setRealm(RealmBean realm) { - this.realm = realm; - } - - public String getAccessUrl() { - return Urls.accountAccessPage(baseURI, getRealmIdentifier()).toString(); - } - - public String getAccountUrl() { - return Urls.accountPage(baseURI, getRealmIdentifier()).toString(); - } - - URI getBaseURI() { - return baseURI; - } - - public String getLoginAction() { - return Urls.realmLoginAction(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginUrl() { - return Urls.realmLoginPage(baseURI, getRealmIdentifier()).toString(); - } - - public String getPasswordUrl() { - return Urls.accountPasswordPage(baseURI, getRealmIdentifier()).toString(); - } - - public String getRegistrationAction() { - if (realm.isSaas()) { - return Urls.saasRegisterAction(baseURI).toString(); - } else { - return Urls.realmRegisterAction(baseURI, getRealmIdentifier()).toString(); - } - } - - public String getRegistrationUrl() { - return Urls.realmRegisterPage(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginUpdatePasswordUrl() { - return Urls.loginActionUpdatePassword(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginUpdateTotpUrl() { - return Urls.loginActionUpdateTotp(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginUpdateProfileUrl() { - return Urls.loginActionUpdateProfile(baseURI, getRealmIdentifier()).toString(); - } - - public String getSocialUrl() { - return Urls.accountSocialPage(baseURI, getRealmIdentifier()).toString(); - } - - public String getTotpUrl() { - return Urls.accountTotpPage(baseURI, getRealmIdentifier()).toString(); - } - - public String getTotpRemoveUrl() { - return Urls.accountTotpRemove(baseURI, getRealmIdentifier()).toString(); - } - - public String getLogoutUrl() { - return Urls.accountLogout(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginPasswordResetUrl() { - return Urls.loginPasswordReset(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginUsernameReminderUrl() { - return Urls.loginUsernameReminder(baseURI, getRealmIdentifier()).toString(); - } - - public String getLoginEmailVerificationUrl() { - return Urls.loginActionEmailVerification(baseURI, getRealmIdentifier()).toString(); - } - - public String getReferrerURI() { - return referrerURI; - } - -} diff --git a/forms/src/main/java/org/keycloak/forms/model/Property.java b/forms/src/main/java/org/keycloak/forms/model/Property.java deleted file mode 100644 index 9c7434f6c10..00000000000 --- a/forms/src/main/java/org/keycloak/forms/model/Property.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.keycloak.forms.model; - -public class Property { - - private String name; - private String value; - - public Property(String name, String value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public String getValue() { - return value; - } - -} diff --git a/forms/src/main/java/org/keycloak/forms/model/RequiredCredential.java b/forms/src/main/java/org/keycloak/forms/model/RequiredCredential.java deleted file mode 100644 index 20fe971f6f9..00000000000 --- a/forms/src/main/java/org/keycloak/forms/model/RequiredCredential.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.keycloak.forms.model; - -public class RequiredCredential { - - public String type; - public boolean secret; - public String formLabel; - - public RequiredCredential(String type, boolean secure, String formLabel) { - this.type = type; - this.secret = secure; - this.formLabel = formLabel; - } - - public String getName() { - return type; - } - - public String getLabel() { - return formLabel; - } - - public String getInputType() { - return secret ? "password" : "text"; - } - -} \ No newline at end of file diff --git a/forms/src/main/java/org/keycloak/forms/model/SocialProvider.java b/forms/src/main/java/org/keycloak/forms/model/SocialProvider.java deleted file mode 100644 index 28d74f99a62..00000000000 --- a/forms/src/main/java/org/keycloak/forms/model/SocialProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.keycloak.forms.model; - -public class SocialProvider { - - private String id; - private String name; - private String loginUrl; - - public SocialProvider(String id, String name, String loginUrl) { - this.id = id; - this.name = name; - this.loginUrl = loginUrl; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public String getLoginUrl() { - return loginUrl; - } - -} \ No newline at end of file diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java deleted file mode 100755 index 83b0f12aefa..00000000000 --- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.service; - -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; -import org.jboss.resteasy.logging.Logger; -import org.keycloak.forms.LoginBean; -import org.keycloak.forms.MessageBean; -import org.keycloak.forms.OAuthGrantBean; -import org.keycloak.forms.RealmBean; -import org.keycloak.forms.RegisterBean; -import org.keycloak.forms.SocialBean; -import org.keycloak.forms.TemplateBean; -import org.keycloak.forms.TotpBean; -import org.keycloak.forms.UrlBean; -import org.keycloak.forms.UserBean; -import org.keycloak.models.ApplicationModel; -import org.keycloak.services.FormService; -import org.keycloak.services.resources.flows.Pages; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; - -/** - * @author Viliam Rockai - */ -public class FormServiceImpl implements FormService { - - private static final Logger log = Logger.getLogger(FormServiceImpl.class); - - private static final String ID = "FormServiceId"; - private static final String BUNDLE = "org.keycloak.forms.messages"; - private final Map commandMap = new HashMap(); - - public FormServiceImpl(){ - commandMap.put(Pages.LOGIN, new CommandLogin()); - commandMap.put(Pages.ERROR, new CommandLogin()); - commandMap.put(Pages.REGISTER, new CommandRegister()); - commandMap.put(Pages.ACCOUNT, new CommandCommon()); - commandMap.put(Pages.LOGIN_UPDATE_PROFILE, new CommandCommon()); - commandMap.put(Pages.PASSWORD, new CommandCommon()); - commandMap.put(Pages.LOGIN_RESET_PASSWORD, new CommandCommon()); - commandMap.put(Pages.LOGIN_USERNAME_REMINDER, new CommandCommon()); - commandMap.put(Pages.LOGIN_UPDATE_PASSWORD, new CommandCommon()); - commandMap.put(Pages.ACCESS, new CommandCommon()); - commandMap.put(Pages.SOCIAL, new CommandCommon()); - commandMap.put(Pages.TOTP, new CommandTotp()); - commandMap.put(Pages.LOGIN_CONFIG_TOTP, new CommandTotp()); - commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp()); - commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandCommon()); - commandMap.put(Pages.OAUTH_GRANT, new CommandOAuthGrant()); - } - - public String getId(){ - return ID; - } - - public String process(String pageId, FormServiceDataBean dataBean){ - Map attributes = new HashMap(); - - ResourceBundle rb = ResourceBundle.getBundle(BUNDLE); - attributes.put("rb", rb); - - if (dataBean.getMessage() != null){ - attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType(), rb)); - } - - attributes.put("template", new TemplateBean(dataBean.getContextPath())); - - if (commandMap.containsKey(pageId)){ - commandMap.get(pageId).exec(attributes, dataBean); - } - - return processFmTemplate(pageId, attributes); - } - - private String processFmTemplate(String temp, Map input) { - - Writer out = new StringWriter(); - Configuration cfg = new Configuration(); - - try { - cfg.setClassForTemplateLoading(FormServiceImpl.class,"/META-INF/resources/forms/theme/default"); - Template template = cfg.getTemplate(temp); - - template.process(input, out); - } catch (IOException e) { - log.error("Failed to load the template " + temp, e); - } catch (TemplateException e) { - log.error("Failed to process template " + temp, e); - } - - return out.toString(); - } - - private class CommandCommon { - protected RealmBean realm; - protected UrlBean url; - protected UserBean user; - protected LoginBean login; - - public void exec(Map attributes, FormServiceDataBean dataBean) { - realm = new RealmBean(dataBean.getRealm()); - - String referrer = dataBean.getQueryParam("referrer"); - String referrerUri = null; - if (referrer != null) { - for (ApplicationModel a : dataBean.getRealm().getApplications()) { - if (a.getName().equals(referrer)) { - referrerUri = a.getBaseUrl(); - break; - } - } - } - - url = new UrlBean(realm, dataBean.getBaseURI(), referrerUri); - user = new UserBean(dataBean.getUserModel()); - login = new LoginBean(realm, dataBean.getFormData()); - - attributes.put("realm", realm); - attributes.put("url", url); - attributes.put("user", user); - attributes.put("login", login); - } - } - - private class CommandTotp extends CommandCommon { - public void exec(Map attributes, FormServiceDataBean dataBean) { - super.exec(attributes, dataBean); - - attributes.put("totp", new TotpBean(user, dataBean.getContextPath())); - } - } - - private class CommandLoginTotp extends CommandCommon { - public void exec(Map attributes, FormServiceDataBean dataBean) { - super.exec(attributes, dataBean); - - RegisterBean register = new RegisterBean(dataBean.getFormData()); - SocialBean social = new SocialBean(realm, dataBean.getSocialProviders(), register, url); - attributes.put("social", social); - } - } - - private class CommandLogin extends CommandCommon { - public void exec(Map attributes, FormServiceDataBean dataBean) { - super.exec(attributes, dataBean); - - RegisterBean register = new RegisterBean(dataBean.getFormData()); - SocialBean social = new SocialBean(realm, dataBean.getSocialProviders(), register, url); - attributes.put("social", social); - } - } - - private class CommandRegister extends CommandCommon { - public void exec(Map attributes, FormServiceDataBean dataBean) { - super.exec(attributes, dataBean); - - RegisterBean register = new RegisterBean(dataBean.getFormData()); - attributes.put("register", register); - - SocialBean social = new SocialBean(realm, dataBean.getSocialProviders(), register, url); - attributes.put("social", social); - } - } - - private class CommandOAuthGrant extends CommandCommon { - public void exec(Map attributes, FormServiceDataBean dataBean) { - super.exec(attributes, dataBean); - - OAuthGrantBean oauth = new OAuthGrantBean(); - oauth.setAction(dataBean.getOAuthAction()); - oauth.setResourceRolesRequested(dataBean.getOAuthResourceRolesRequested()); - oauth.setClient(dataBean.getOAuthClient()); - oauth.setoAuthCode(dataBean.getOAuthCode()); - oauth.setRealmRolesRequested(dataBean.getOAuthRealmRolesRequested()); - - attributes.put("oauth", oauth); - } - } - -} \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.less b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.less deleted file mode 100755 index 9ea4ddcf2f6..00000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.less +++ /dev/null @@ -1,281 +0,0 @@ -@charset "UTF-8"; - -@zocialPath: '.'; - -@font-face { - font-family: 'zocial'; - font-style: normal; - font-weight: normal; - src: url('@{zocialPath}/zocial-regular-webfont.eot'); - src: url('@{zocialPath}/zocial-regular-webfont.eot?#iefix') format('embedded-opentype'), - url('@{zocialPath}/zocial-regular-webfont.woff') format('woff'), - url('@{zocialPath}/zocial-regular-webfont.ttf') format('truetype'), - url('@{zocialPath}/zocial-regular-webfont.svg#zocialregular') format('svg'); -} - -@font-face { - font-family: 'zocial'; - src: url('@{zocialPath}/zocial-regular-webfont.eot'); -} - -@font-face { - font-family: 'zocial'; - src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), - url('@{zocialPath}/zocial-regular-webfont.ttf') format('truetype'), - url('@{zocialPath}/zocial-regular-webfont.svg#zocialregular') format('svg'); - font-weight: normal; - font-style: normal; -} - -.zocial { - // Gradients - .gradient (@color1, @color2, @pos2, @color3, @pos3, @color4, @startAt) { - background-image: -moz-linear-gradient(center @startAt, @color1, @color2 @pos2, @color3 @pos3, @color4); - background-image: -ms-linear-gradient(center @startAt, @color1, @color2 @pos2, @color3 @pos3, @color4); - background-image: -o-linear-gradient(center @startAt, @color1, @color2 @pos2, @color3 @pos3, @color4); - background-image: -webkit-gradient(linear, left top, left bottom, from(@color1), color-stop(@pos2, @color2), color-stop(@pos3, @color3), to(@color4)); - background-image: -webkit-linear-gradient(center @startAt, @color1, @color2 @pos2, @color3 @pos3, @color4); - background-image: linear-gradient(center @startAt, @color1, @color2 @pos2, @color3 @pos3, @color4); - } - .gradient (@color1, @color2, @pos2, @color3, @pos3, @color4) { - .gradient(@color1, @color2, @pos2, @color3, @pos3, @color4, top); - } - - // Drop shadows - .box-shadow(@shadow) { - -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 - -moz-box-shadow: @shadow; - box-shadow: @shadow; - } - - .gradient(rgba(255,255,255,.1), rgba(255,255,255,.05), 49%, rgba(0,0,0,.05), 51%, rgba(0,0,0,.1)); - - &, a& { - position: relative; - display: inline-block; - padding: 0 .95em 0 0; - color: #fff; - font: bold 100%/2.1 "Lucida Grande", Tahoma, sans-serif; - text-align: center; - text-decoration: none; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); - border: 1px solid #777; - border-color: rgba(0,0,0,0.2); - border-bottom-color: #333; - border-bottom-color: rgba(0, 0, 0, 0.4); - .box-shadow(~"inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9)"); - white-space: nowrap; - cursor: pointer; - - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - - -webkit-border-radius: .3em; - -moz-border-radius: .3em; - border-radius: .3em; - } - - &:before { - content: ""; - border-right: 0.075em solid rgba(0,0,0,0.1); - float: left; - font: 120%/1.65 zocial; - font-style: normal; - font-weight: normal; - margin: 0 0.5em 0 0; - padding: 0 0.5em; - text-align: center; - text-decoration: none; - text-transform: none; - - .box-shadow(0.075em 0 0 rgba(255,255,255,0.25)); - - -webkit-font-smoothing: antialiased; - } - - &:hover, &:focus { - background-image: -moz-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: -ms-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: -o-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.15)), color-stop(49%, rgba(255,255,255,.15)), color-stop(51%, rgba(0,0,0,.1)), to(rgba(0,0,0,.15))); - background-image: -webkit-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - } - - &:active { - outline: none; /* outline is visible on :focus */ - .gradient(rgba(255,255,255,.1), rgba(255,255,255,0), 30%, transparent, 50%, rgba(0,0,0,.1), bottom); - } - - &.icon { - max-width: 2.4em; - max-height: 2.15em; - padding-left: 0; - padding-right: 0; - white-space: nowrap; - overflow: hidden; - - &:before { - width: 2em; - height: 2em; - padding: 0; - border: none; - .box-shadow(none); - } - } - - &.acrobat, - &.bitcoin, - &.cloudapp, - &.dropbox, - &.email, - &.eventful, - &.github, - &.gmail, - &.instapaper, - &.itunes, - &.ninetyninedesigns, - &.openid, - &.plancast, - &.pocket, - &.posterous, - &.reddit, - &.secondary, - &.stackoverflow, - &.viadeo, - &.weibo, - &.wikipedia { - border: 1px solid #aaa; - border-color: rgba(0,0,0,0.3); - border-bottom-color: #777; - border-bottom-color: rgba(0,0,0,0.5); - text-shadow: 0 1px 0 rgba(255,255,255,0.8); - .box-shadow(~"inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5)"); - - &:focus, &:hover { - .gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.2), 49%, rgba(0,0,0,0.05), 51%, rgba(0,0,0,0.15)); - } - - &:active { - .gradient(rgba(255,255,255,0), rgba(255,255,255,0), 30%, rgba(0,0,0,0), 50%, rgba(0,0,0,0.1)) - } - } - - /* Button icon and color */ - /* Icon characters are stored in unicode private area */ - &.acrobat { &:before { content: "\00E3"; color: #FB0000; } background-color: #fff; color: #000; } - &.amazon { &:before { content: "a"; } background-color: #ffad1d; color: #030037; text-shadow: 0 1px 0 rgba(255,255,255,0.5); } - &.android { &:before { content: "&"; } background-color: #a4c639; } - &.angellist { &:before { content: "\00D6"; } background-color: #000; } - &.aol { &:before { content: "\""; } background-color: #f00; } - &.appnet { &:before { content: "\00E1"; } background-color: #3178bd; } - &.appstore { &:before { content: "A"; } background-color: #000; } - &.bitcoin { &:before { content: "2"; color: #f7931a; } background-color: #efefef; color: #4d4d4d; } - &.bitbucket{ &:before { content: "\00E9"; } background-color: #205081; } - &.blogger { &:before { content: "B"; } background-color: #ee5a22; } - &.buffer { &:before { content: "\00E5"; } background-color: #232323; } - &.call { &:before { content: "7"; } background-color: #008000; } - &.cal { &:before { content: "."; } background-color: #d63538; } - &.cart { &:before { content: "\00C9"; } background-color: #333; } - &.chrome { &:before { content: "["; } background-color: #006cd4; } - &.cloudapp { &:before { content: "c"; } background-color: #fff; color: #312c2a; } - &.creativecommons { &:before { content: "C"; } background-color: #000; } - &.delicious { &:before { content: "#"; } background-color: #3271cb; } - &.digg { &:before { content: ";"; } background-color: #164673; } - &.disqus { &:before { content: "Q"; } background-color: #5d8aad; } - &.dribbble { &:before { content: "D"; } background-color: #ea4c89; } - &.dropbox { &:before { content: "d"; color: #1f75cc; } background-color: #fff; color: #312c2a; } - &.drupal { &:before { content: "\00E4"; color: #fff; } background-color: #0077c0; color: #fff; } - &.dwolla { &:before { content: "\00E0"; } background-color: #e88c02; } - &.email { &:before { content: "]"; color: #312c2a; } background-color: #f0f0eb; color: #312c2a; } - &.eventasaurus { &:before { content: "v"; color: #9de428; } background-color: #192931; color: #fff; } - &.eventbrite { &:before { content: "|"; } background-color: #ff5616; } - &.eventful { &:before { content: "'"; color: #0066CC; } background-color: #fff; color: #47ab15; } - &.evernote { &:before { content: "E"; } background-color: #6bb130; color: #fff; } - &.facebook { &:before { content: "f"; } background-color: #4863ae; } - &.fivehundredpx { &:before { content: "0"; color: #29b6ff; } background-color: #333; } - &.flattr { &:before { content: "%"; } background-color: #8aba42; } - &.flickr { &:before { content: "F"; } background-color: #ff0084; } - &.forrst { &:before { content: ":"; color: #50894f; } background-color: #1e360d; } - &.foursquare { &:before { content: "4"; } background-color: #44a8e0; } - &.github { &:before { content: "g"; } background-color: #fbfbfb; color: #050505; } - &.gmail { &:before { content: "m"; color: #f00; } background-color: #efefef; color: #222; } - &.google { &:before { content: "G"; } background-color: #4e6cf7; } - &.googleplay { &:before { content: "h"; } background-color: #000; } - &.googleplus { &:before { content: "+"; } background-color: #dd4b39; } - &.gowalla { &:before { content: "@"; } background-color: #ff720a; } - &.grooveshark { &:before { content: "8"; } background-color: #111; color:#eee; } - &.guest { &:before { content: "?"; } background-color: #1b4d6d; } - &.html5 { &:before { content: "5"; } background-color: #ff3617; } - &.ie { &:before { content: "6"; } background-color: #00a1d9; } - &.instapaper { &:before { content: "I"; } background-color: #eee; color: #222; } - &.instagram { &:before { content: "\00DC"; } background-color: #3f729b; } - &.intensedebate { &:before { content: "{"; } background-color: #0099e1; } - &.itunes { &:before { content: "i"; color: #1a6dd2; } background-color: #efefeb; color: #312c2a; } - &.klout { &:before { content: "K"; } background-color: #e34a25; } - &.lanyrd { &:before { content: "-"; } background-color: #2e6ac2; } - &.lastfm { &:before { content: "l"; } background-color: #dc1a23; } - &.lego { &:before { content: "\00EA"; color: #fff900; } background-color: #fb0000; } - &.linkedin { &:before { content: "L"; } background-color: #0083a8; } - &.lkdto { &:before { content: "\00EE"; } background-color: #7c786f; } - &.logmein { &:before { content: "\00EB"; } background-color: #000; } - &.macstore { &:before { content: "^"; } background-color: #007dcb; } - &.meetup { &:before { content: "M"; } background-color: #ff0026; } - &.myspace { &:before { content: "_"; } background-color: #000; } - &.ninetyninedesigns { &:before { content: "9"; color: #f50; } background-color: #fff; color: #072243; } - &.openid { &:before { content: "o"; color: #ff921d; } background-color: #f5f5f5; color: #333; } - &.opentable { &:before { content: "\00C7"; } background-color: #990000; } - &.paypal { &:before { content: "$"; } background-color: #fff; color: #32689a; text-shadow: 0 1px 0 rgba(255,255,255,0.5); } - &.pinboard { &:before { content: "n"; } background-color: blue; } - &.pinterest { &:before { content: "1"; } background-color: #c91618; } - &.plancast { &:before { content: "P"; } background-color: #e7ebed; color: #333; } - &.pocket { &:before { content: "\00E7"; color: #ee4056; } background-color: #fff; color: #777; } - &.plurk { &:before { content: "j"; } background-color: #cf682f; } - &.podcast { &:before { content: "`"; } background-color: #9365ce; } - &.posterous { &:before { content: "~"; } background-color: #ffd959; color: #bc7134; } - &.print { &:before { content: "\00D1"; } background-color: #f0f0eb; color: #222; text-shadow: 0 1px 0 rgba(255,255,255,0.8); } - &.quora { &:before { content: "q"; } background-color: #a82400; } - &.reddit { &:before { content: ">"; color: red; } background-color: #fff; color: #222; } - &.rss { &:before { content: "R"; } background-color: #ff7f25; } - &.scribd { &:before { content: "}"; color: #00d5ea; } background-color: #231c1a; } - &.skype { &:before { content: "S"; } background-color: #00a2ed; } - &.smashing { &:before { content: "*"; } background-color: #ff4f27; } - &.songkick { &:before { content: "k"; } background-color: #ff0050; } - &.soundcloud { &:before { content: "s"; } background-color: #ff4500; } - &.spotify { &:before { content: "="; } background-color: #60af00; } - &.stackoverflow { &:before { content: "\00EC"; color: #ff7a15; } background-color: #fff; color: #555; } - &.statusnet { &:before { content: "\00E2"; color: #fff; } background-color: #829d25; } - &.steam { &:before { content: "b"; } background-color: #000; } - &.stripe { &:before { content: "\00A3"; } background-color: #2f7ed6; } - &.stumbleupon { &:before { content: "/"; } background-color: #eb4924; } - &.tumblr { &:before { content: "t"; } background-color: #374a61; } - &.twitter { &:before { content: "T"; } background-color: #46c0fb; } - &.viadeo { &:before { content: "H"; color: #f59b20; } background-color: #fff; color: #000; } - &.vimeo { &:before { content: "V"; } background-color: #00a2cd; } - &.vk { &:before { content: "N"; } background-color: #45688E; } - &.weibo { &:before { content: "J"; color: #e6162d; } background-color: #faf6f1; color: #000; } - &.wikipedia { &:before { content: ","; } background-color: #fff; color: #000; } - &.windows { &:before { content: "W"; } background-color: #0052a4; color: #fff; } - &.wordpress { &:before { content: "w"; } background-color: #464646; } - &.xing { &:before { content: "X"; } background-color: #0A5D5E; } - &.yahoo { &:before { content: "Y"; } background-color: #a200c2; } - &.ycombinator { &:before { content: "\00ED"; } background-color: #ff6600; } - &.yelp { &:before { content: "y"; } background-color: #e60010; } - &.youtube { &:before { content: "U"; } background-color: #f00; } - - /* - The Miscellaneous Buttons - These button have no icons and can be general purpose buttons while ensuring consistent button style - Credit to @guillermovs for suggesting - */ - &.primary, &.secondary { margin: 0.1em 0; padding: 0 1em; &:before { display: none; } } - &.primary { background-color: #333; } - &.secondary { background-color: #f0f0eb; color: #222; text-shadow: 0 1px 0 rgba(255,255,255,0.8); } -} - -/* Any browser-specific adjustments */ -button:-moz-focus-inner { - border: 0; - padding: 0; -} diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl deleted file mode 100755 index c4a092bcbae..00000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.ftl +++ /dev/null @@ -1,44 +0,0 @@ -<#import "template-login.ftl" as layout> -<@layout.registrationLayout bodyClass="register" ; section> - - <#if section = "title"> - - ${rb.getString('registerWith')} ${realm.name} - - <#elseif section = "header"> - - ${rb.getString('registerWith')} ${realm.name} - - <#elseif section = "form"> - -
-

${rb.getString('allRequired')}

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

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

- - - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/styles.css b/forms/src/main/resources/META-INF/resources/forms/theme/default/styles.css deleted file mode 100644 index 2bd23a0778a..00000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/styles.css +++ /dev/null @@ -1,13 +0,0 @@ -@IMPORT url("css/reset.css"); -@IMPORT url("css/base.css"); -@IMPORT url("css/forms.css"); -@IMPORT url("css/zocial/zocial.css"); -@IMPORT url("css/login-register.css"); -@IMPORT url("//fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic"); - -.zocial.google { - background-color: #dd4b39 !important; -} -.zocial.google:before { - content: "+" !important; -} \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl deleted file mode 100644 index bfacefd4581..00000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl +++ /dev/null @@ -1,84 +0,0 @@ -<#macro registrationLayout bodyClass isErrorPage=false isSeparator=false forceSeparator=false> - - - - - - - <#nested "title"> - - - - - - - - - <#if (template.themeConfig.logo)?has_content> -

- Keycloak -

- - -
-

- <#nested "header"> -

- -
- <#if !forceSeparator && realm?has_content> - <#assign drawSeparator = realm.registrationAllowed> - <#else> - <#assign drawSeparator = isSeparator> - -
-
- <#if !isErrorPage && message?has_content> - <#if message.error> - - <#elseif message.success> - - - - -

Application login area

- <#nested "form"> -
- -
-

Info area

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

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

- -
- - <#nested "content"> - - - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl deleted file mode 100644 index d141a40586f..00000000000 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl +++ /dev/null @@ -1,78 +0,0 @@ -<#macro mainLayout active bodyClass> - - - - - Edit Account - <#nested "title"> - - - - - - - - - - - - - - - - - - - - - - - - - <#if message?has_content> - - - -
- -
- -
-
- - -
-
-

<#nested "header">

- <#nested "content"> -
-
-
-
-
- - - \ No newline at end of file diff --git a/forms/src/main/resources/META-INF/services/org.keycloak.services.FormService b/forms/src/main/resources/META-INF/services/org.keycloak.services.FormService deleted file mode 100644 index 482f65db4ff..00000000000 --- a/forms/src/main/resources/META-INF/services/org.keycloak.services.FormService +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.service.FormServiceImpl \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index ac75c7a5426..8ec3e0a6e7a 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -161,4 +161,13 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa Set getRealmRoleMappings(UserModel user); Set getRealmScopeMappings(UserModel user); + + String getLoginTheme(); + + void setLoginTheme(String name); + + String getAccountTheme(); + + void setAccountTheme(String name); + } diff --git a/model/api/src/main/test/java/org/keycloak/models/PasswordPolicyTest.java b/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java similarity index 100% rename from model/api/src/main/test/java/org/keycloak/models/PasswordPolicyTest.java rename to model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index b14fa97b33b..965cbbee4fb 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1096,4 +1096,25 @@ public class RealmAdapter implements RealmModel { return r.getId().equals(getId()); } + @Override + public String getLoginTheme() { + return realm.getLoginTheme(); + } + + @Override + public void setLoginTheme(String name) { + realm.setLoginTheme(name); + em.flush(); + } + + @Override + public String getAccountTheme() { + return realm.getAccountTheme(); + } + + @Override + public void setAccountTheme(String name) { + realm.setAccountTheme(name); + em.flush(); + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 58c1f17d4cf..4bfbcffb813 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -50,6 +50,9 @@ public class RealmEntity { @Column(length = 2048) protected String privateKeyPem; + protected String loginTheme; + protected String accountTheme; + @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true) @JoinTable(name="USER_REQUIRED_CREDENTIALS") Collection requiredCredentials = new ArrayList(); @@ -274,5 +277,21 @@ public class RealmEntity { public void setPasswordPolicy(String passwordPolicy) { this.passwordPolicy = passwordPolicy; } + + public String getLoginTheme() { + return loginTheme; + } + + public void setLoginTheme(String theme) { + this.loginTheme = theme; + } + + public String getAccountTheme() { + return accountTheme; + } + + public void setAccountTheme(String theme) { + this.accountTheme = theme; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java index 6d1460133f0..0d67e34d4f2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java @@ -21,7 +21,7 @@ import java.util.Collection; @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public abstract class RoleEntity { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private String id; private String name; diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java index 4b0b0054779..d4c1624e270 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java @@ -1014,4 +1014,26 @@ public class RealmAdapter implements RealmModel { realm.setPasswordPolicy(policy.toString()); updateRealm(); } + + @Override + public String getLoginTheme() { + return realm.getLoginTheme(); + } + + @Override + public void setLoginTheme(String name) { + realm.setLoginTheme(name); + updateRealm(); + } + + @Override + public String getAccountTheme() { + return realm.getAccountTheme(); + } + + @Override + public void setAccountTheme(String name) { + realm.setAccountTheme(name); + updateRealm(); + } } diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java index 03c346665d1..497add81c8b 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java @@ -27,6 +27,8 @@ public class RealmData extends AbstractPartition { private Map smtpConfig; private Map socialConfig; private String passwordPolicy; + private String loginTheme; + private String accountTheme; public RealmData() { super(null); @@ -185,4 +187,22 @@ public class RealmData extends AbstractPartition { public void setPasswordPolicy(String passwordPolicy) { this.passwordPolicy = passwordPolicy; } + + @AttributeProperty + public String getLoginTheme() { + return loginTheme; + } + + public void setLoginTheme(String theme) { + this.loginTheme = theme; + } + + @AttributeProperty + public String getAccountTheme() { + return accountTheme; + } + + public void setAccountTheme(String theme) { + this.accountTheme = theme; + } } diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java index fe0a448fbd4..4b6abb6a7c4 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java @@ -62,6 +62,8 @@ public class RealmEntity implements Serializable { @AttributeValue @Lob private HashMap socialConfig; + @AttributeValue + private String theme; public PartitionTypeEntity getPartitionTypeEntity() { @@ -191,4 +193,12 @@ public class RealmEntity implements Serializable { public void setSocialConfig(HashMap socialConfig) { this.socialConfig = socialConfig; } + + public String getTheme() { + return theme; + } + + public void setTheme(String theme) { + this.theme = theme; + } } diff --git a/server/pom.xml b/server/pom.xml index db1bf9ed7e8..f07ec2aaae3 100755 --- a/server/pom.xml +++ b/server/pom.xml @@ -66,7 +66,32 @@ org.keycloak - keycloak-forms + keycloak-forms-common-freemarker + ${project.version} + + + org.keycloak + keycloak-forms-common-themes + ${project.version} + + + org.keycloak + keycloak-account-api + ${project.version} + + + org.keycloak + keycloak-account-freemarker + ${project.version} + + + org.keycloak + keycloak-login-api + ${project.version} + + + org.keycloak + keycloak-login-freemarker ${project.version} diff --git a/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java b/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java index 13aa2052d69..fdcdaa4e57f 100755 --- a/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java +++ b/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java @@ -10,6 +10,7 @@ import org.keycloak.util.JsonSerialization; import javax.servlet.ServletContext; import javax.ws.rs.core.Context; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -31,6 +32,14 @@ public class KeycloakServerApplication extends KeycloakApplication { session.getTransaction().commit(); } + String themeDir = System.getProperty("keycloak.theme.dir"); + if (themeDir == null) { + String jbossConfigDir = System.getProperty("jboss.server.config.dir"); + if (jbossConfigDir != null) { + themeDir = jbossConfigDir + File.separator + "themes"; + System.setProperty("keycloak.theme.dir", themeDir); + } + } } public void importRealm(KeycloakSession session, RealmRepresentation rep) { diff --git a/server/src/main/resources/META-INF/persistence.xml b/server/src/main/resources/META-INF/persistence.xml index d15233c86ec..8055b831418 100755 --- a/server/src/main/resources/META-INF/persistence.xml +++ b/server/src/main/resources/META-INF/persistence.xml @@ -5,18 +5,16 @@ java:jboss/datasources/ExampleDS org.keycloak.models.jpa.entities.ApplicationEntity - org.keycloak.models.jpa.entities.ApplicationScopeMappingEntity - org.keycloak.models.jpa.entities.ApplicationUserRoleMappingEntity org.keycloak.models.jpa.entities.CredentialEntity org.keycloak.models.jpa.entities.OAuthClientEntity org.keycloak.models.jpa.entities.RealmEntity - org.keycloak.models.jpa.entities.RealmScopeMappingEntity - org.keycloak.models.jpa.entities.RealmUserRoleMappingEntity org.keycloak.models.jpa.entities.RequiredCredentialEntity - org.keycloak.models.jpa.entities.RoleEntity + org.keycloak.models.jpa.entities.ApplicationRoleEntity + org.keycloak.models.jpa.entities.RealmRoleEntity org.keycloak.models.jpa.entities.SocialLinkEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity + org.keycloak.models.jpa.entities.UserScopeMappingEntity true diff --git a/services/pom.xml b/services/pom.xml index 7502bbe8c5b..d97d6b40c0e 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -30,6 +30,22 @@ ${project.version} provided + + org.keycloak + keycloak-forms-common-freemarker + ${project.version} + provided + + + org.keycloak + keycloak-account-api + ${project.version} + + + org.keycloak + keycloak-login-api + ${project.version} + org.keycloak keycloak-model-api diff --git a/services/src/main/java/org/keycloak/services/FormService.java b/services/src/main/java/org/keycloak/services/FormService.java deleted file mode 100755 index 89593724e8b..00000000000 --- a/services/src/main/java/org/keycloak/services/FormService.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.services; - -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.services.resources.flows.FormFlows; -import org.keycloak.social.SocialLoader; -import org.keycloak.social.SocialProvider; - -import javax.ws.rs.core.MultivaluedMap; -import java.net.URI; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -/** - * @author Viliam Rockai - */ -public interface FormService { - - String getId(); - - public String process(String pageId, FormServiceDataBean data); - - public static class FormServiceDataBean { - - private RealmModel realm; - private UserModel userModel; - private String message; - - private FormFlows.MessageType messageType; - - private MultivaluedMap formData; - private Map queryParams; - private URI baseURI; - - private List socialProviders; - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - private String code; - - public String getContextPath() { - return contextPath; - } - - public void setContextPath(String contextPath) { - this.contextPath = contextPath; - } - - private String contextPath; - - public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap formData, Map queryParams, String message) { - this.realm = realm; - this.userModel = userModel; - this.formData = formData; - this.queryParams = queryParams; - this.message = message; - - socialProviders = new LinkedList(); - if (realm != null) { - Map socialConfig = realm.getSocialConfig(); - if (socialConfig != null) { - for (SocialProvider p : SocialLoader.load()) { - if (socialConfig.containsKey(p.getId() + ".key") && socialConfig.containsKey(p.getId() + ".secret")) { - socialProviders.add(p); - } - } - } - } - } - - public URI getBaseURI() { - return baseURI; - } - - public void setBaseURI(URI baseURI) { - this.baseURI = baseURI; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public MultivaluedMap getFormData() { - return formData; - } - - public Map getQueryParams() { - return queryParams; - } - - - public String getQueryParam(String key) { - return queryParams != null ? queryParams.get(key) : null; - } - - - public void setFormData(MultivaluedMap formData) { - this.formData = formData; - } - - public RealmModel getRealm() { - return realm; - } - - public RealmModel setRealm(RealmModel realm) { - return realm; - } - - public UserModel getUserModel() { - return userModel; - } - - public void setUserModel(UserModel userModel) { - this.userModel = userModel; - } - - public List getSocialProviders() { - return socialProviders; - } - - public FormFlows.MessageType getMessageType() { - return messageType; - } - - public void setMessageType(FormFlows.MessageType messageType) { - this.messageType = messageType; - } - - /* OAuth Part */ - private MultivaluedMap oAuthResourceRolesRequested; - private List oAuthRealmRolesRequested; - private UserModel oAuthClient; - private String oAuthCode; - private String oAuthAction; - - public String getOAuthAction() { - return oAuthAction; - } - - public void setOAuthAction(String action) { - this.oAuthAction = action; - } - - public MultivaluedMap getOAuthResourceRolesRequested() { - return oAuthResourceRolesRequested; - } - - public void setOAuthResourceRolesRequested(MultivaluedMap resourceRolesRequested) { - this.oAuthResourceRolesRequested = resourceRolesRequested; - } - - public List getOAuthRealmRolesRequested() { - return oAuthRealmRolesRequested; - } - - public void setOAuthRealmRolesRequested(List realmRolesRequested) { - this.oAuthRealmRolesRequested = realmRolesRequested; - } - - public UserModel getOAuthClient() { - return oAuthClient; - } - - public void setOAuthClient(UserModel client) { - this.oAuthClient = client; - } - - public String getOAuthCode() { - return oAuthCode; - } - - public void setOAuthCode(String oAuthCode) { - this.oAuthCode = oAuthCode; - } - - } -} diff --git a/services/src/main/java/org/keycloak/services/email/EmailSender.java b/services/src/main/java/org/keycloak/services/email/EmailSender.java index ea76c3d8ce7..694157ace23 100755 --- a/services/src/main/java/org/keycloak/services/email/EmailSender.java +++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java @@ -103,9 +103,9 @@ public class EmailSender { } } - public void sendEmailVerification(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) throws EmailException { + public void sendEmailVerification(UserModel user, RealmModel realm, String accessCodeId, UriInfo uriInfo) throws EmailException { UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode.getId()); + builder.queryParam("key", accessCodeId); URI uri = builder.build(realm.getName()); diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index f557310274d..78ee93877a8 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -55,6 +55,9 @@ public class ApplianceBootstrap { realm.setRegistrationAllowed(false); manager.generateRealmKeys(realm); + realm.setLoginTheme("keycloak"); + realm.setAccountTheme("keycloak"); + ApplicationModel adminConsole = realm.addApplication(Constants.ADMIN_CONSOLE_APPLICATION); adminConsole.setEnabled(true); UserCredentialModel adminConsolePassword = new UserCredentialModel(); diff --git a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java index 7b145238e14..a706360ade6 100755 --- a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java +++ b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java @@ -73,6 +73,8 @@ public class ModelToRepresentation { rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); rep.setSmtpServer(realm.getSmtpConfig()); rep.setSocialProviders(realm.getSocialConfig()); + rep.setAccountTheme(realm.getAccountTheme()); + rep.setLoginTheme(realm.getLoginTheme()); if (realm.getPasswordPolicy() != null) { rep.setPasswordPolicy(realm.getPasswordPolicy().toString()); } diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 3c9a1cd0fe7..9afb3ea2949 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -121,6 +121,8 @@ public class RealmManager { if (rep.getRequiredApplicationCredentials() != null) { realm.updateRequiredApplicationCredentials(rep.getRequiredApplicationCredentials()); } + realm.setLoginTheme(rep.getLoginTheme()); + realm.setAccountTheme(rep.getAccountTheme()); realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); @@ -198,6 +200,8 @@ public class RealmManager { newRealm.setPrivateKeyPem(rep.getPrivateKey()); newRealm.setPublicKeyPem(rep.getPublicKey()); } + newRealm.setLoginTheme(rep.getLoginTheme()); + newRealm.setAccountTheme(rep.getAccountTheme()); Map userMap = new HashMap(); diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index ef86c574e1c..e63262a603a 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -24,6 +24,9 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.AbstractOAuthClient; +import org.keycloak.account.Account; +import org.keycloak.account.AccountLoader; +import org.keycloak.account.AccountPages; import org.keycloak.jaxrs.JaxrsOAuthClient; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; @@ -37,14 +40,11 @@ import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; -import org.keycloak.services.resources.flows.FormFlows; -import org.keycloak.services.resources.flows.Pages; import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; import javax.ws.rs.*; import javax.ws.rs.core.*; -import javax.ws.rs.ext.Providers; import java.net.URI; import java.util.List; @@ -68,9 +68,6 @@ public class AccountService { @Context private UriInfo uriInfo; - @Context - private Providers providers; - private AuthenticationManager authManager = new AuthenticationManager(); private ApplicationModel application; @@ -83,28 +80,28 @@ public class AccountService { this.tokenManager = tokenManager; } - private Response forwardToPage(String path, String template) { + private Response forwardToPage(String path, AccountPages page) { AuthenticationManager.Auth auth = getAuth(false); if (auth != null) { if (!hasAccess(auth)) { return noAccess(); } - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(auth.getUser()); + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); String referrer = getReferrer(); if (referrer != null) { - forms.setQueryParam("referrer", referrer); + account.setReferrer(referrer); } - return forms.forwardToForm(template); + return account.createResponse(page); } else { return login(path); } } private Response noAccess() { - return Flows.forms(realm, request, uriInfo).setError("No access").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("No access").createErrorPage(); } @Path("/") @@ -118,7 +115,7 @@ public class AccountService { public Response accountPage() { List types = headers.getAcceptableMediaTypes(); if (types.contains(MediaType.WILDCARD_TYPE) || (types.contains(MediaType.TEXT_HTML_TYPE))) { - return forwardToPage(null, Pages.ACCOUNT); + return forwardToPage(null, AccountPages.ACCOUNT); } else if (types.contains(MediaType.APPLICATION_JSON_TYPE)) { AuthenticationManager.Auth auth = getAuth(true); if (!hasAccess(auth, Constants.ACCOUNT_PROFILE_ROLE)) { @@ -130,28 +127,16 @@ public class AccountService { } } - @Path("social") - @GET - public Response socialPage() { - return forwardToPage("social", Pages.SOCIAL); - } - @Path("totp") @GET public Response totpPage() { - return forwardToPage("totp", Pages.TOTP); + return forwardToPage("totp", AccountPages.TOTP); } @Path("password") @GET public Response passwordPage() { - return forwardToPage("password", Pages.PASSWORD); - } - - @Path("access") - @GET - public Response accessPage() { - return forwardToPage("access", Pages.ACCESS); + return forwardToPage("password", AccountPages.PASSWORD); } @Path("/") @@ -165,16 +150,18 @@ public class AccountService { UserModel user = auth.getUser(); + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); + String error = Validation.validateUpdateProfileForm(formData); if (error != null) { - return Flows.forms(realm, request, uriInfo).setUser(user).setError(error).forwardToAccount(); + return account.setError(error).createResponse(AccountPages.ACCOUNT); } user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); user.setEmail(formData.getFirst("email")); - return Flows.forms(realm, request, uriInfo).setUser(user).setSuccess("accountUpdated").forwardToAccount(); + return account.setSuccess("accountUpdated").createResponse(AccountPages.ACCOUNT); } @Path("totp-remove") @@ -186,9 +173,10 @@ public class AccountService { } UserModel user = auth.getUser(); - user.setTotp(false); - return Flows.forms(realm, request, uriInfo).setSuccess("successTotpRemoved").setUser(user).forwardToTotp(); + + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); + return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP); } @Path("totp") @@ -205,11 +193,12 @@ public class AccountService { String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); + if (Validation.isEmpty(totp)) { - return forms.setError(Messages.MISSING_TOTP).forwardToTotp(); + return account.setError(Messages.MISSING_TOTP).createResponse(AccountPages.TOTP); } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { - return forms.setError(Messages.INVALID_TOTP).forwardToTotp(); + return account.setError(Messages.INVALID_TOTP).createResponse(AccountPages.TOTP); } UserCredentialModel credentials = new UserCredentialModel(); @@ -219,7 +208,7 @@ public class AccountService { user.setTotp(true); - return Flows.forms(realm, request, uriInfo).setSuccess("successTotp").setUser(user).forwardToTotp(); + return account.setSuccess("successTotp").createResponse(AccountPages.TOTP); } @Path("password") @@ -233,27 +222,27 @@ public class AccountService { UserModel user = auth.getUser(); - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); String password = formData.getFirst("password"); String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); if (Validation.isEmpty(passwordNew)) { - return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); + return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); } else if (!passwordNew.equals(passwordConfirm)) { - return forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword(); + return account.setError(Messages.INVALID_PASSWORD_CONFIRM).createResponse(AccountPages.PASSWORD); } if (Validation.isEmpty(password)) { - return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); + return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); } else if (!realm.validatePassword(user, password)) { - return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword(); + return account.setError(Messages.INVALID_PASSWORD_EXISTING).createResponse(AccountPages.PASSWORD); } String error = Validation.validatePassword(formData, realm.getPasswordPolicy()); if (error != null) { - return forms.setError(error).forwardToPassword(); + return account.setError(error).createResponse(AccountPages.PASSWORD); } UserCredentialModel credentials = new UserCredentialModel(); @@ -262,7 +251,7 @@ public class AccountService { realm.updateCredential(user, credentials); - return Flows.forms(realm, request, uriInfo).setUser(user).setSuccess("accountPasswordUpdated").forwardToPassword(); + return account.setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD); } @Path("login-redirect") diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index e41cfc6128d..6c89c12d163 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -48,6 +48,7 @@ public class KeycloakApplication extends Application { singletons.add(new SocialResource(tokenManager, new SocialRequestManager())); classes.add(SkeletonKeyContextResolver.class); classes.add(QRCodeResource.class); + classes.add(ThemeResource.class); setupDefaultRealm(); } diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index 327c02dd5d6..b38df737056 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -23,6 +23,7 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.login.LoginForms; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.RealmModel; @@ -38,7 +39,6 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; -import org.keycloak.services.resources.flows.FormFlows; import org.keycloak.services.validation.Validation; import javax.ws.rs.Consumes; @@ -97,7 +97,7 @@ public class RequiredActionsService { String error = Validation.validateUpdateProfileForm(formData); if (error != null) { - return Flows.forms(realm, request, uriInfo).setError(error).forwardToAction(RequiredAction.UPDATE_PROFILE); + return Flows.forms(realm, request, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); } user.setFirstName(formData.getFirst("firstName")); @@ -124,11 +124,11 @@ public class RequiredActionsService { String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); + LoginForms loginForms = Flows.forms(realm, request, uriInfo).setUser(user); if (Validation.isEmpty(totp)) { - return forms.setError(Messages.MISSING_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP); + return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { - return forms.setError(Messages.INVALID_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP); + return loginForms.setError(Messages.INVALID_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); } UserCredentialModel credentials = new UserCredentialModel(); @@ -161,16 +161,16 @@ public class RequiredActionsService { String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); + LoginForms loginForms = Flows.forms(realm, request, uriInfo).setUser(user); if (Validation.isEmpty(passwordNew)) { - return forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); + return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); } else if (!passwordNew.equals(passwordConfirm)) { - return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); + return loginForms.setError(Messages.NOTMATCH_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); } String error = realm.getPasswordPolicy().validate(passwordNew); if (error != null) { - return forms.setError(error).forwardToAction(RequiredAction.UPDATE_PASSWORD); + return loginForms.setError(error).createResponse(RequiredAction.UPDATE_PASSWORD); } UserCredentialModel credentials = new UserCredentialModel(); @@ -186,11 +186,7 @@ public class RequiredActionsService { accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD); } - if (accessCode != null) { - return redirectOauth(user, accessCode); - } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); - } + return redirectOauth(user, accessCode); } @@ -217,8 +213,8 @@ public class RequiredActionsService { return unauthorized(); } - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(accessCode.getUser()) - .forwardToAction(RequiredAction.VERIFY_EMAIL); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser()) + .createResponse(RequiredAction.VERIFY_EMAIL); } } @@ -231,9 +227,9 @@ public class RequiredActionsService { || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) { return unauthorized(); } - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToAction(RequiredAction.UPDATE_PASSWORD); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); } else { - return Flows.forms(realm, request, uriInfo).forwardToPasswordReset(); + return Flows.forms(realm, request, uriInfo).createPasswordReset(); } } @@ -260,7 +256,7 @@ public class RequiredActionsService { UserModel user = realm.getUserByEmail(email); if (user == null) { - return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset(); + return Flows.forms(realm, request, uriInfo).setError("emailError").createPasswordReset(); } Set requiredActions = new HashSet(user.getRequiredActions()); @@ -274,17 +270,17 @@ public class RequiredActionsService { new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); } catch (EmailException e) { logger.error("Failed to send password reset email", e); - return Flows.forms(realm, request, uriInfo).setError("emailSendError").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage(); } - return Flows.forms(realm, request, uriInfo).setSuccess("emailSent").forwardToPasswordReset(); + return Flows.forms(realm, request, uriInfo).setSuccess("emailSent").createPasswordReset(); } @Path("username-reminder") @GET public Response usernameReminder() { - return Flows.forms(realm, request, uriInfo).forwardToUsernameReminder(); + return Flows.forms(realm, request, uriInfo).createUsernameReminder(); } @Path("username-reminder") @@ -306,21 +302,21 @@ public class RequiredActionsService { UserModel user = realm.getUserByEmail(email); if (user == null) { - return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToUsernameReminder(); + return Flows.forms(realm, request, uriInfo).setError("emailError").createUsernameReminder(); } try { new EmailSender(realm.getSmtpConfig()).sendUsernameReminder(user); } catch (EmailException e) { logger.error("Failed to send username reminder email", e); - return Flows.forms(realm, request, uriInfo).setError("emailSendError").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage(); } - return Flows.forms(realm, request, uriInfo).setSuccess("emailUsernameSent").forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setSuccess("emailUsernameSent").createLogin(); } private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { - String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE); + String code = uriInfo.getQueryParameters().getFirst("code"); if (code == null) { logger.debug("getAccessCodeEntry code as not in query param"); return null; @@ -373,8 +369,8 @@ public class RequiredActionsService { Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) - .forwardToAction(requiredActions.iterator().next()); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) + .createResponse(requiredActions.iterator().next()); } else { logger.debug("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); @@ -384,7 +380,7 @@ public class RequiredActionsService { } private Response unauthorized() { - return Flows.forms(realm, request, uriInfo).setError("Unauthorized request").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("Unauthorized request").createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index 770d38641df..a4efab29f77 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -22,7 +22,6 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.models.KeycloakSession; @@ -38,7 +37,6 @@ import org.keycloak.services.resources.flows.Urls; import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthRequest; import org.keycloak.social.RequestDetails; -import org.keycloak.social.SocialConstants; import org.keycloak.social.SocialLoader; import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProviderConfig; @@ -46,19 +44,13 @@ import org.keycloak.social.SocialProviderException; import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.social.SocialUser; -import javax.ws.rs.Consumes; import javax.ws.rs.GET; -import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; -import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; @@ -67,7 +59,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.UUID; /** * @author Stian Thorgersen @@ -204,7 +195,7 @@ public class SocialResource { SocialProvider provider = SocialLoader.load(providerId); if (provider == null) { - return Flows.forms(realm, request, uriInfo).setError("Social provider not found").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("Social provider not found").createErrorPage(); } String key = realm.getSocialConfig().get(providerId + ".key"); @@ -216,16 +207,16 @@ public class SocialResource { UserModel client = realm.getUser(clientId); if (client == null) { logger.warn("Unknown login requester: " + clientId); - return Flows.forms(realm, request, uriInfo).setError("Unknown login requester.").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("Unknown login requester.").createErrorPage(); } if (!client.isEnabled()) { logger.warn("Login requester not enabled."); - return Flows.forms(realm, request, uriInfo).setError("Login requester not enabled.").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("Login requester not enabled.").createErrorPage(); } redirectUri = TokenService.verifyRedirectUri(redirectUri, client); if (redirectUri == null) { - return Flows.forms(realm, request, uriInfo).setError("Invalid redirect_uri.").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); } try { @@ -240,7 +231,7 @@ public class SocialResource { return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build(); } catch (Throwable t) { - return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java new file mode 100755 index 00000000000..4d06466d485 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java @@ -0,0 +1,44 @@ +package org.keycloak.services.resources; + +import org.jboss.resteasy.logging.Logger; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeLoader; + +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Stian Thorgersen + */ +@Path("/theme") +public class ThemeResource { + + private static final Logger logger = Logger.getLogger(ThemeResource.class); + + private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap(); + + @GET + @Path("/{themType}/{themeName}/{path:.*}") + public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) { + try { + Theme theme = ThemeLoader.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase())); + InputStream resource = theme.getResourceAsStream(path); + if (resource != null) { + return Response.ok(resource).type(mimeTypes.getContentType(path)).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } catch (Exception e) { + logger.warn("Failed to get theme resource", e); + return Response.serverError().build(); + } + } + + +} diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index 15beb20031f..79992d64068 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -7,7 +7,6 @@ import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; -import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; @@ -228,8 +227,7 @@ public class TokenService { UserModel user = realm.getUser(username); if (user == null){ - return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData) - .forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); } isTotpConfigurationRequired(user); @@ -242,13 +240,11 @@ public class TokenService { case ACTIONS_REQUIRED: return oauth.processAccessCode(scopeParam, state, redirect, client, user); case ACCOUNT_DISABLED: - return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData) - .forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); case MISSING_TOTP: - return Flows.forms(realm, request, uriInfo).setFormData(formData).forwardToLoginTotp(); + return Flows.forms(realm, request, uriInfo).setFormData(formData).createLoginTotp(); default: - return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData) - .forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); } } @@ -319,14 +315,14 @@ public class TokenService { } if (error != null) { - return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).forwardToRegistration(); + return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration(); } String username = formData.getFirst("username"); UserModel user = realm.getUser(username); if (user != null) { - return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).forwardToRegistration(); + return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); } user = realm.addUser(username); @@ -517,8 +513,8 @@ public class TokenService { if (prompt != null && prompt.equals("none")) { return oauth.redirectError(client, "access_denied", state, redirect); } - logger.info("forwardToLogin() now..."); - return Flows.forms(realm, request, uriInfo).forwardToLogin(); + logger.info("createLogin() now..."); + return Flows.forms(realm, request, uriInfo).createLogin(); } @Path("registrations") @@ -560,7 +556,7 @@ public class TokenService { authManager.expireIdentityCookie(realm, uriInfo); - return Flows.forms(realm, request, uriInfo).forwardToRegistration(); + return Flows.forms(realm, request, uriInfo).createRegistration(); } @Path("logout") diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java index a2cdc286523..c5ded4d16de 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java @@ -24,11 +24,8 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.TokenService; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; -import org.keycloak.util.KeycloakUriBuilder; -import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; -import javax.ws.rs.ForbiddenException; import javax.ws.rs.GET; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.NotFoundException; @@ -200,6 +197,30 @@ public class AdminService { return adminResource; } + @Path("serverinfo") + public ServerInfoAdminResource getServerInfo(@Context final HttpHeaders headers) { + RealmManager realmManager = new RealmManager(session); + RealmModel saasRealm = getAdminstrationRealm(realmManager); + if (saasRealm == null) + throw new NotFoundException(); + UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers); + if (admin == null) { + throw new NotAuthorizedException("Bearer"); + } + ApplicationModel adminConsole = saasRealm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); + if (adminConsole == null) { + throw new NotFoundException(); + } + RoleModel adminRole = adminConsole.getRole(Constants.ADMIN_CONSOLE_ADMIN_ROLE); + if (!saasRealm.hasRole(admin, adminRole)) { + logger.warn("not a Realm admin"); + throw new NotAuthorizedException("Bearer"); + } + ServerInfoAdminResource adminResource = new ServerInfoAdminResource(); + resourceContext.initResource(adminResource); + return adminResource; + } + @Path("login") @GET @NoCache @@ -226,7 +247,7 @@ public class AdminService { public Response errorOnLoginRedirect(@QueryParam ("error") String message) { RealmManager realmManager = new RealmManager(session); RealmModel realm = getAdminstrationRealm(realmManager); - return Flows.forms(realm, request, uriInfo).setError(message).forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError(message).createErrorPage(); } protected Response redirectOnLoginError(String message) { @@ -373,13 +394,11 @@ public class AdminService { NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo); return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build(); case ACCOUNT_DISABLED: - return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData) - .forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); case ACTIONS_REQUIRED: return oauth.processAccessCode(null, "n", contextRoot(uriInfo).path(adminPath).build().toString(), adminConsoleUser, user); default: - return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData) - .forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java new file mode 100644 index 00000000000..04cda2133e4 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -0,0 +1,68 @@ +package org.keycloak.services.resources.admin; + +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.social.SocialProvider; +import org.keycloak.util.ProviderLoader; + +import javax.ws.rs.GET; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class ServerInfoAdminResource { + + @GET + public ServerInfoRepresentation getInfo() { + ServerInfoRepresentation info = new ServerInfoRepresentation(); + setSocialProviders(info); + setThemes(info); + return info; + } + + private void setThemes(ServerInfoRepresentation info) { + Iterable providers = ProviderLoader.load(ThemeProvider.class); + info.themes = new HashMap>(); + for (Theme.Type type : Theme.Type.values()) { + List themes = new LinkedList(); + for (ThemeProvider p : providers) { + themes.addAll(p.nameSet(type)); + } + Collections.sort(themes); + info.themes.put(type.toString().toLowerCase(), themes); + } + } + + private void setSocialProviders(ServerInfoRepresentation info) { + info.socialProviders = new LinkedList(); + for (SocialProvider p : ProviderLoader.load(SocialProvider.class)) { + info.socialProviders.add(p.getId()); + } + Collections.sort(info.socialProviders); + } + + public static class ServerInfoRepresentation { + + private Map> themes; + + private List socialProviders; + + public ServerInfoRepresentation() { + } + + public Map> getThemes() { + return themes; + } + + public List getSocialProviders() { + return socialProviders; + } + + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java index e46632a9e5e..5df9493629b 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java @@ -22,6 +22,8 @@ package org.keycloak.services.resources.flows; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.login.LoginForms; +import org.keycloak.login.LoginFormsLoader; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; @@ -36,8 +38,8 @@ public class Flows { private Flows() { } - public static FormFlows forms(RealmModel realm, HttpRequest request, UriInfo uriInfo) { - return new FormFlows(realm, request, uriInfo); + public static LoginForms forms(RealmModel realm, HttpRequest request, UriInfo uriInfo) { + return LoginFormsLoader.load().createForms(realm, request, uriInfo); } public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, diff --git a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java deleted file mode 100755 index d92e40326e7..00000000000 --- a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.services.resources.flows; - -import org.jboss.resteasy.spi.HttpRequest; -import org.jboss.resteasy.spi.ResteasyUriInfo; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserModel.RequiredAction; -import org.keycloak.services.FormService; -import org.keycloak.services.email.EmailException; -import org.keycloak.services.email.EmailSender; -import org.keycloak.services.managers.AccessCodeEntry; -import org.keycloak.services.messages.Messages; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; - -/** - * @author Stian Thorgersen - */ -public class FormFlows { - - public static final String CODE = "code"; - - private String message; - - public static enum MessageType {SUCCESS, WARNING, ERROR}; - private MessageType messageType = MessageType.ERROR; - - private MultivaluedMap formData; - - private Map queryParams; - - private RealmModel realm; - - private HttpRequest request; - private UserModel userModel; - - private AccessCodeEntry accessCode; - private UriInfo uriInfo; - - FormFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo) { - this.realm = realm; - this.request = request; - this.uriInfo = uriInfo; - } - - public Response forwardToAction(RequiredAction action) { - - switch (action) { - case CONFIGURE_TOTP: - return forwardToActionForm(Pages.LOGIN_CONFIG_TOTP, Messages.ACTION_WARN_TOTP); - case UPDATE_PROFILE: - return forwardToActionForm(Pages.LOGIN_UPDATE_PROFILE, Messages.ACTION_WARN_PROFILE); - case UPDATE_PASSWORD: - return forwardToActionForm(Pages.LOGIN_UPDATE_PASSWORD, Messages.ACTION_WARN_PASSWD); - case VERIFY_EMAIL: - try { - new EmailSender(realm.getSmtpConfig()).sendEmailVerification(userModel, realm, accessCode, uriInfo); - } catch (EmailException e) { - return setError("emailSendError").forwardToErrorPage(); - } - return forwardToActionForm(Pages.LOGIN_VERIFY_EMAIL, Messages.ACTION_WARN_EMAIL); - default: - return Response.serverError().build(); - } - } - - public Response forwardToAccess() { - return forwardToForm(Pages.ACCESS); - } - - public Response forwardToAccount() { - return forwardToForm(Pages.ACCOUNT); - } - - private Response forwardToForm(String template, FormService.FormServiceDataBean formDataBean, Response.Status status) { - - // Getting URI needed by form processing service - ResteasyUriInfo uriInfo = request.getUri(); - MultivaluedMap queryParameterMap = uriInfo.getQueryParameters(); - - String requestURI = uriInfo.getBaseUri().getPath(); - UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); - - for(String k : queryParameterMap.keySet()){ - uriBuilder.replaceQueryParam(k, queryParameterMap.get(k).toArray()); - } - - if (accessCode != null) { - uriBuilder.replaceQueryParam(CODE, accessCode.getCode()); - } - - if (queryParams != null) { - for (Map.Entry q : queryParams.entrySet()) { - uriBuilder.replaceQueryParam(q.getKey(), q.getValue()); - } - } - - URI baseURI = uriBuilder.build(); - formDataBean.setBaseURI(baseURI); - - // TODO find a better way to obtain contextPath - // Getting context path by removing "rest/" substring from the BaseUri path - formDataBean.setContextPath(requestURI.substring(0, requestURI.length() - 6)); - - // Find the service and process relevant template - Iterator itr = ServiceLoader.load(FormService.class).iterator(); - - while (itr.hasNext()) { - FormService provider = itr.next(); - if (provider.getId().equals("FormServiceId")) - return Response.status(status).type(MediaType.TEXT_HTML).entity(provider.process(template, formDataBean)).build(); - } - - return Response.status(status).entity("form provider not found").build(); - } - - public Response forwardToForm(String template) { - - FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, queryParams, message); - formDataBean.setMessageType(messageType); - - return forwardToForm(template, formDataBean, Response.Status.OK); - } - - public Response forwardToForm(String template, Response.Status status) { - - FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, queryParams, message); - formDataBean.setMessageType(messageType); - - return forwardToForm(template, formDataBean, status); - } - - private Response forwardToActionForm(String template, String warningSummary) { - - // If no other message is set, notify user about required action in the warning window - // so it's clear that this is a req. action form not a login form - if (message == null){ - messageType = MessageType.WARNING; - message = warningSummary; - } - - return forwardToForm(template); - } - - public Response forwardToLogin() { - return forwardToForm(Pages.LOGIN); - } - - public Response forwardToPasswordReset() { - return forwardToForm(Pages.LOGIN_RESET_PASSWORD); - } - - public Response forwardToUsernameReminder() { - return forwardToForm(Pages.LOGIN_USERNAME_REMINDER); - } - - public Response forwardToLoginTotp() { - return forwardToForm(Pages.LOGIN_TOTP); - } - - public Response forwardToPassword() { - return forwardToForm(Pages.PASSWORD); - } - - public Response forwardToRegistration() { - return forwardToForm(Pages.REGISTER); - } - - public Response forwardToSocial() { - return forwardToForm(Pages.SOCIAL); - } - - public Response forwardToTotp() { - return forwardToForm(Pages.TOTP); - } - - public Response forwardToErrorPage() { - return forwardToForm(Pages.ERROR, Response.Status.INTERNAL_SERVER_ERROR); - } - - public Response forwardToOAuthGrant(){ - - FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, queryParams, message); - - formDataBean.setOAuthRealmRolesRequested((List) request.getAttribute("realmRolesRequested")); - formDataBean.setOAuthResourceRolesRequested((MultivaluedMap) request.getAttribute("resourceRolesRequested")); - formDataBean.setOAuthClient((UserModel)request.getAttribute("client")); - formDataBean.setOAuthCode((String)request.getAttribute("code")); - formDataBean.setOAuthAction((String)request.getAttribute("action")); - - return forwardToForm(Pages.OAUTH_GRANT, formDataBean, Response.Status.OK); - } - - public FormFlows setAccessCode(AccessCodeEntry accessCode) { - this.accessCode = accessCode; - return this; - } - - public FormFlows setQueryParam(String key, String value) { - if (queryParams == null) { - queryParams = new HashMap(); - } - queryParams.put(key, value); - return this; - } - - public FormFlows setError(String message) { - this.message = message; - this.messageType = MessageType.ERROR; - return this; - } - - public FormFlows setSuccess(String message) { - this.message = message; - this.messageType = MessageType.SUCCESS; - return this; - } - - public FormFlows setWarning(String message) { - this.message = message; - this.messageType = MessageType.WARNING; - return this; - } - - public FormFlows setUser(UserModel userModel) { - this.userModel = userModel; - return this; - } - - public FormFlows setFormData(MultivaluedMap formData) { - this.formData = formData; - return this; - } - -} diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index 0ed81dc594b..a18540f41f2 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -103,14 +103,16 @@ public class OAuthFlows { if (!requiredActions.isEmpty()) { accessCode.setRequiredActions(new HashSet(requiredActions)); accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) - .forwardToAction(user.getRequiredActions().iterator().next()); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) + .createResponse(user.getRequiredActions().iterator().next()); } if (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) { accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - return oauthGrantPage(accessCode, client); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()). + setAccessRequest(accessCode.getRealmRolesRequested(), accessCode.getResourceRolesRequested()). + setClient(client).createOAuthGrant(); } if (redirect != null) { @@ -120,18 +122,8 @@ public class OAuthFlows { } } - public Response oauthGrantPage(AccessCodeEntry accessCode, UserModel client) { - request.setAttribute("realmRolesRequested", accessCode.getRealmRolesRequested()); - request.setAttribute("resourceRolesRequested", accessCode.getResourceRolesRequested()); - request.setAttribute("client", client); - request.setAttribute("action", TokenService.processOAuthUrl(uriInfo).build(realm.getName()).toString()); - request.setAttribute("code", accessCode.getCode()); - - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToOAuthGrant(); - } - public Response forwardToSecurityFailure(String message) { - return Flows.forms(realm, request, uriInfo).setError(message).forwardToErrorPage(); + return Flows.forms(realm, request, uriInfo).setError(message).createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java deleted file mode 100644 index 72d10e52b29..00000000000 --- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.services.resources.flows; - -/** - * @author Stian Thorgersen - */ -public class Pages { - - public final static String ACCESS = "access.ftl"; - - public final static String ACCOUNT = "account.ftl"; - - public final static String LOGIN = "login.ftl"; - - public final static String LOGIN_TOTP = "login-totp.ftl"; - - public final static String LOGIN_CONFIG_TOTP = "login-config-totp.ftl"; - - public final static String LOGIN_VERIFY_EMAIL = "login-verify-email.ftl"; - - public final static String OAUTH_GRANT = "login-oauth-grant.ftl"; - - public final static String PASSWORD = "password.ftl"; - - public final static String LOGIN_RESET_PASSWORD = "login-reset-password.ftl"; - - public final static String LOGIN_UPDATE_PASSWORD = "login-update-password.ftl"; - - public final static String LOGIN_USERNAME_REMINDER = "login-username-reminder.ftl"; - - public final static String REGISTER = "register.ftl"; - - public final static String ERROR = "error.ftl"; - - public final static String SOCIAL = "social.ftl"; - - public final static String TOTP = "totp.ftl"; - - public final static String LOGIN_UPDATE_PROFILE = "login-update-profile.ftl"; - -} diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java index 7859c785954..23fa68e5a9c 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -22,6 +22,7 @@ package org.keycloak.services.resources.flows; import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.ThemeResource; import org.keycloak.services.resources.admin.AdminService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RequiredActionsService; @@ -29,6 +30,7 @@ import org.keycloak.services.resources.SocialResource; import org.keycloak.services.resources.TokenService; import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; import java.net.URI; /** @@ -128,6 +130,10 @@ public class Urls { return tokenBase(baseUri).path(TokenService.class, "registerPage").build(realmId); } + public static URI realmOauthAction(URI baseUri, String realmId) { + return tokenBase(baseUri).path(TokenService.class, "processOAuth").build(realmId); + } + public static URI realmCode(URI baseUri, String realmId) { return tokenBase(baseUri).path(TokenService.class, "accessCodeToToken").build(realmId); } @@ -165,6 +171,10 @@ public class Urls { .build(realmId); } + public static URI themeRoot(URI baseUri) { + return themeBase(baseUri).build(); + } + private static UriBuilder requiredActionsBase(URI baseUri) { return tokenBase(baseUri).path(TokenService.class, "getRequiredActionsService"); } @@ -172,4 +182,8 @@ public class Urls { private static UriBuilder tokenBase(URI baseUri) { return realmBase(baseUri).path(RealmsResource.class, "getTokenService"); } + + private static UriBuilder themeBase(URI baseUri) { + return UriBuilder.fromUri(baseUri).path(ThemeResource.class); + } } diff --git a/services/src/main/resources/META-INF/mime.types b/services/src/main/resources/META-INF/mime.types new file mode 100644 index 00000000000..432f4c81292 --- /dev/null +++ b/services/src/main/resources/META-INF/mime.types @@ -0,0 +1 @@ +text/css css CSS \ No newline at end of file diff --git a/services/src/test/resources/META-INF/persistence.xml b/services/src/test/resources/META-INF/persistence.xml index 2fb76f2281a..a020d60d7d1 100755 --- a/services/src/test/resources/META-INF/persistence.xml +++ b/services/src/test/resources/META-INF/persistence.xml @@ -2,6 +2,34 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> + + org.hibernate.ejb.HibernatePersistence + + org.keycloak.models.jpa.entities.ApplicationEntity + org.keycloak.models.jpa.entities.CredentialEntity + org.keycloak.models.jpa.entities.OAuthClientEntity + org.keycloak.models.jpa.entities.RealmEntity + org.keycloak.models.jpa.entities.RequiredCredentialEntity + org.keycloak.models.jpa.entities.ApplicationRoleEntity + org.keycloak.models.jpa.entities.RealmRoleEntity + org.keycloak.models.jpa.entities.SocialLinkEntity + org.keycloak.models.jpa.entities.UserEntity + org.keycloak.models.jpa.entities.UserRoleMappingEntity + org.keycloak.models.jpa.entities.UserScopeMappingEntity + + true + + + + + + + + + + + + - - - org.hibernate.ejb.HibernatePersistence - - org.keycloak.models.jpa.entities.ApplicationEntity - org.keycloak.models.jpa.entities.CredentialEntity - org.keycloak.models.jpa.entities.OAuthClientEntity - org.keycloak.models.jpa.entities.RealmEntity - org.keycloak.models.jpa.entities.RequiredCredentialEntity - org.keycloak.models.jpa.entities.ApplicationRoleEntity - org.keycloak.models.jpa.entities.RealmRoleEntity - org.keycloak.models.jpa.entities.SocialLinkEntity - org.keycloak.models.jpa.entities.UserEntity - org.keycloak.models.jpa.entities.UserRoleMappingEntity - org.keycloak.models.jpa.entities.UserScopeMappingEntity - - true - - - - - - - - - - - - - - diff --git a/social/core/pom.xml b/social/core/pom.xml index 99ade1ef8b0..41a9ec34a7e 100755 --- a/social/core/pom.xml +++ b/social/core/pom.xml @@ -13,6 +13,11 @@ + + org.keycloak + keycloak-core + ${project.version} + org.json json diff --git a/social/core/src/main/java/org/keycloak/social/SocialLoader.java b/social/core/src/main/java/org/keycloak/social/SocialLoader.java index 5dfb44a7e49..6d245355ac0 100644 --- a/social/core/src/main/java/org/keycloak/social/SocialLoader.java +++ b/social/core/src/main/java/org/keycloak/social/SocialLoader.java @@ -1,5 +1,7 @@ package org.keycloak.social; +import org.keycloak.util.ProviderLoader; + import java.util.ServiceLoader; /** @@ -25,7 +27,7 @@ public class SocialLoader { } public static Iterable load() { - return ServiceLoader.load(SocialProvider.class); + return ProviderLoader.load(SocialProvider.class); } } diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java index 40eb086ccbe..0789e2376c1 100644 --- a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java @@ -59,26 +59,6 @@ public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitP WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); addJSONData(service.getJSON(deploymentName), warMetaData); - //addJSONData(getJSON(), warMetaData); - } - - // TODO: remove this. - private String getJSON() { - return "{\n" + -" \"realm\": \"demo\",\n" + -" \"realm-public-key\": \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB\",\n" + -" \"auth-server-url\": \"http://localhost:8080/auth\",\n" + -" \"ssl-not-required\": true,\n" + -" \"resource\": \"customer-portal-subsys\",\n" + -" \"credentials\": {\n" + -" \"password\": \"password\"\n" + -" },\n" + -" \"use-resource-role-mappings\": false,\n" + -" \"enable-cors\": false,\n" + -" \"cors-max-age\": -1,\n" + -" \"expose-token\": false,\n" + -" \"bearer-only\": false\n" + -"}"; } private void addJSONData(String json, WarMetaData warMetaData) { diff --git a/testsuite/integration/README.md b/testsuite/integration/README.md index 6eb01f160b9..642865706d3 100644 --- a/testsuite/integration/README.md +++ b/testsuite/integration/README.md @@ -24,6 +24,22 @@ or run org.keycloak.testutils.KeycloakServer from your favourite IDE! When starting the server it can also import a realm from a json file: mvn exec:java -Pkeycloak-server -Dimport=testrealm.json + +### Live edit of html and styles + +The Keycloak test server can load resources directly from the filesystem instead of the classpath. This allows editing html, styles and updating images without restarting the server. To make the server use resources from the filesystem start with: + + mvn exec:java -Pkeycloak-server -Dresources + +You can also specify the theme directory used by the server with: + + mvn exec:java -Pkeycloak-server -Dkeycloak.theme.dir= + +For example to use the example themes run the server with: + + mvn exec:java -Pkeycloak-server -Dkeycloak.theme.dir=examples/themes + +**NOTE:** If `keycloak.theme.dir` is specified the default themes (base, rcue and keycloak) are loaded from the classpath TOTP codes ---------- diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index e3fffc5ed99..e228bbe91d4 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -90,7 +90,32 @@ org.keycloak - keycloak-forms + keycloak-forms-common-freemarker + ${project.version} + + + org.keycloak + keycloak-forms-common-themes + ${project.version} + + + org.keycloak + keycloak-account-api + ${project.version} + + + org.keycloak + keycloak-account-freemarker + ${project.version} + + + org.keycloak + keycloak-login-api + ${project.version} + + + org.keycloak + keycloak-login-freemarker ${project.version} diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java index c3edfe93c17..60433a2ed67 100755 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java @@ -37,6 +37,7 @@ import io.undertow.servlet.api.ServletInfo; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import org.jboss.resteasy.spi.ResteasyDeployment; +import org.keycloak.theme.DefaultLoginThemeProvider; import org.keycloak.services.tmp.TmpAdminRedirectServlet; import org.keycloak.util.JsonSerialization; import org.keycloak.models.Constants; @@ -136,6 +137,11 @@ public class KeycloakServer { throw new RuntimeException("Invalid resources directory"); } + if (!System.getProperties().containsKey("keycloak.theme.dir")) { + System.setProperty(DefaultLoginThemeProvider.class.getName() + ".disabled", ""); + System.setProperty("keycloak.theme.dir", file(dir.getAbsolutePath(), "forms", "common-themes", "src", "main", "resources", "theme").getAbsolutePath()); + } + config.setResourcesHome(dir.getAbsolutePath()); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 8df0a1e3e7c..c49e8a90b76 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -87,7 +87,7 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("Success!", resetPasswordPage.getMessage()); + Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getMessage()); Assert.assertEquals(1, greenMail.getReceivedMessages().length); @@ -146,7 +146,7 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); - Assert.assertEquals("Success!", resetPasswordPage.getMessage()); + Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getMessage()); Assert.assertEquals(1, greenMail.getReceivedMessages().length); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java index 0a94649c736..080112f6e6f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java @@ -53,7 +53,7 @@ public class AccountPasswordPage extends AbstractAccountPage { } public boolean isCurrent() { - return driver.getPageSource().contains("Change Password"); + return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/password"); } public void open() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java index 171e6efa4fd..292a99f4272 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java @@ -54,7 +54,7 @@ public class AccountTotpPage extends AbstractAccountPage { } public boolean isCurrent() { - return driver.getTitle().contains("Edit Account - Google Authenticator"); + return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/totp"); } public void open() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java index d19ca29c1b6..0c88078882e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java @@ -75,7 +75,7 @@ public class AccountUpdateProfilePage extends AbstractAccountPage { } public boolean isCurrent() { - return driver.getPageSource().contains("Edit Account"); + return driver.getTitle().contains("Account Management") && driver.getPageSource().contains("Edit Account"); } public void open() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java index 3a1f72e0d0e..532a7d822f7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java @@ -59,7 +59,7 @@ public class LoginPage extends AbstractPage { @FindBy(linkText = "Username") private WebElement recoverUsernameLink; - @FindBy(id = "loginError") + @FindBy(className = "error") private WebElement loginErrorMessage; public void login(String username, String password) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java index 2a7ed578caf..516f2d6ed8b 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java @@ -36,7 +36,7 @@ public class LoginTotpPage extends AbstractPage { @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; - @FindBy(id = "loginError") + @FindBy(className = "error") private WebElement loginErrorMessage; public void login(String totp) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java index 9fe02ef3fb3..17f058dd329 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -41,7 +41,7 @@ public class LoginUpdateProfilePage extends AbstractPage { @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; - @FindBy(id = "loginError") + @FindBy(className = "error") private WebElement loginErrorMessage; public void update(String firstName, String lastName, String email) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java index c67d6927bfe..c84f1ac566a 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -50,7 +50,7 @@ public class RegisterPage extends AbstractPage { @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; - @FindBy(id = "loginError") + @FindBy(className = "error") private WebElement loginErrorMessage; public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) { diff --git a/testsuite/performance/src/test/resources/META-INF/persistence.xml b/testsuite/performance/src/test/resources/META-INF/persistence.xml index 2f67fde9244..a020d60d7d1 100755 --- a/testsuite/performance/src/test/resources/META-INF/persistence.xml +++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml @@ -1,7 +1,35 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" + version="1.0"> + + org.hibernate.ejb.HibernatePersistence + + org.keycloak.models.jpa.entities.ApplicationEntity + org.keycloak.models.jpa.entities.CredentialEntity + org.keycloak.models.jpa.entities.OAuthClientEntity + org.keycloak.models.jpa.entities.RealmEntity + org.keycloak.models.jpa.entities.RequiredCredentialEntity + org.keycloak.models.jpa.entities.ApplicationRoleEntity + org.keycloak.models.jpa.entities.RealmRoleEntity + org.keycloak.models.jpa.entities.SocialLinkEntity + org.keycloak.models.jpa.entities.UserEntity + org.keycloak.models.jpa.entities.UserRoleMappingEntity + org.keycloak.models.jpa.entities.UserScopeMappingEntity + + true + + + + + + + + + + + + - - + + + + + - --> - - org.hibernate.ejb.HibernatePersistence - - org.keycloak.models.jpa.entities.ApplicationEntity - org.keycloak.models.jpa.entities.CredentialEntity - org.keycloak.models.jpa.entities.OAuthClientEntity - org.keycloak.models.jpa.entities.RealmEntity - org.keycloak.models.jpa.entities.RequiredCredentialEntity - org.keycloak.models.jpa.entities.ApplicationRoleEntity - org.keycloak.models.jpa.entities.RealmRoleEntity - org.keycloak.models.jpa.entities.SocialLinkEntity - org.keycloak.models.jpa.entities.UserEntity - org.keycloak.models.jpa.entities.UserRoleMappingEntity - org.keycloak.models.jpa.entities.UserScopeMappingEntity - - true - - - - - - - - - - - - - - - - \ No newline at end of file + --> +