diff --git a/quartz-manager/pom.xml b/quartz-manager/pom.xml index b12053b..deb2dad 100644 --- a/quartz-manager/pom.xml +++ b/quartz-manager/pom.xml @@ -40,6 +40,10 @@ org.springframework.boot spring-boot-starter-thymeleaf + + org.thymeleaf.extras + thymeleaf-extras-springsecurity4 + org.springframework.boot spring-boot-starter-velocity diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/MVCConfig.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/MVCConfig.java index 826a74e..a1ec413 100644 --- a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/MVCConfig.java +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/MVCConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect; import org.thymeleaf.spring4.SpringTemplateEngine; import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring4.view.ThymeleafViewResolver; @@ -19,12 +20,10 @@ public class MVCConfig extends WebMvcConfigurerAdapter { registry.addViewController("/login").setViewName("login"); registry.addViewController("/").setViewName("redirect:/manager"); - registry.addViewController("/templates/manager/config-form.html") - .setViewName("manager/config-form"); + registry.addViewController("/templates/manager/config-form.html").setViewName("manager/config-form"); registry.addViewController("/templates/manager/progress-panel.html") .setViewName("manager/progress-panel"); - registry.addViewController("/templates/manager/logs-panel.html") - .setViewName("manager/logs-panel"); + registry.addViewController("/templates/manager/logs-panel.html").setViewName("manager/logs-panel"); } @@ -33,6 +32,7 @@ public class MVCConfig extends WebMvcConfigurerAdapter { final SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine(); springTemplateEngine.addTemplateResolver(templateResolver()); springTemplateEngine.addDialect(new LayoutDialect()); + springTemplateEngine.addDialect(new SpringSecurityDialect()); return springTemplateEngine; } diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java index 71ccc6b..bbf562f 100644 --- a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/configuration/WebSecurityConfig.java @@ -1,5 +1,7 @@ package it.fabioformosa.quartzmanager.configuration; +import javax.annotation.Resource; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -8,21 +10,24 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import it.fabioformosa.quartzmanager.security.AjaxAuthenticationFilter; +import it.fabioformosa.quartzmanager.security.ComboEntryPoint; + @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Configuration @Order(1) - public static class ApiWebSecurityConfig - extends WebSecurityConfigurerAdapter { + public static class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // - .antMatcher("/notifications").authorizeRequests() - .anyRequest().hasAnyRole("ADMIN").and().httpBasic(); + .antMatcher("/notifications").authorizeRequests().anyRequest().hasAnyRole("ADMIN").and() + .httpBasic(); // http.antMatcher("/logs/**").authorizeRequests().anyRequest() // .permitAll(); @@ -31,30 +36,30 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Configuration @Order(2) - public static class FormWebSecurityConfig - extends WebSecurityConfigurerAdapter { + public static class FormWebSecurityConfig extends WebSecurityConfigurerAdapter { - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", - "/lib/**"); - } + @Resource + private ComboEntryPoint comboEntryPoint; @Override protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable().authorizeRequests().anyRequest() - .authenticated().and().formLogin().loginPage("/login") - .permitAll().and().logout() - .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) - .logoutSuccessUrl("/manager"); + http.exceptionHandling().authenticationEntryPoint(comboEntryPoint).and().csrf().disable()// + .authorizeRequests().anyRequest().authenticated().and()// + .addFilterBefore(new AjaxAuthenticationFilter(authenticationManager()), + UsernamePasswordAuthenticationFilter.class)// + .formLogin().loginPage("/login").permitAll().and().logout() + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/manager"); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**"); } } @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) - throws Exception { - auth.inMemoryAuthentication().withUser("admin").password("admin") - .roles("ADMIN"); + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); } } diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/controllers/SessionController.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/controllers/SessionController.java new file mode 100644 index 0000000..757e6e3 --- /dev/null +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/controllers/SessionController.java @@ -0,0 +1,34 @@ +package it.fabioformosa.quartzmanager.controllers; + +import javax.servlet.http.HttpSession; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/session") +public class SessionController { + + private final Logger log = LoggerFactory.getLogger(SessionController.class); + + @RequestMapping("/invalidate") + @PreAuthorize("hasAuthority('ADMIN')") + public String invalidateSession(HttpSession session) { + session.invalidate(); + log.info("Invalidated current session!"); + return "redirect:/manager"; + } + + @RequestMapping("/refresh") + @PreAuthorize("hasAuthority('ADMIN')") + public HttpEntity refreshSession(HttpSession session) { + return new ResponseEntity<>(HttpStatus.OK); + } + +} diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/AjaxAuthenticationFilter.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/AjaxAuthenticationFilter.java new file mode 100644 index 0000000..1cd4bd2 --- /dev/null +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/AjaxAuthenticationFilter.java @@ -0,0 +1,55 @@ +package it.fabioformosa.quartzmanager.security; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class AjaxAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + public class AjaxLoginAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler + implements AuthenticationSuccessHandler { + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + response.setStatus(HttpServletResponse.SC_OK); + clearAuthenticationAttributes(request); + return; + } + + } + + public AjaxAuthenticationFilter(AuthenticationManager authenticationManager) { + setAuthenticationManager(authenticationManager); + setUsernameParameter("ajaxUsername"); + setPasswordParameter("ajaxPassword"); + setPostOnly(true); + setFilterProcessesUrl("/ajaxLogin"); + + setAuthenticationSuccessHandler(new AjaxLoginAuthSuccessHandler()); + } + + /** + * Removes temporary authentication-related data which may have been stored + * in the session during the authentication process. + */ + protected final void clearAuthenticationAttributes(HttpServletRequest request) { + HttpSession session = request.getSession(false); + + if (session == null) + return; + + session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + } + +} diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/ComboEntryPoint.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/ComboEntryPoint.java new file mode 100644 index 0000000..2c05117 --- /dev/null +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/ComboEntryPoint.java @@ -0,0 +1,33 @@ +package it.fabioformosa.quartzmanager.security; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class ComboEntryPoint extends LoginUrlAuthenticationEntryPoint { + + private static final String LOGIN_FORM_URL = "/login"; + + public ComboEntryPoint() { + super(LOGIN_FORM_URL); + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + if (RESTRequestMatcher.isRestRequest(request) + || WebsocketRequestMatcher.isWebsocketConnectionRequest(request)) + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + else + super.commence(request, response, authException); + } + +} diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/RESTRequestMatcher.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/RESTRequestMatcher.java new file mode 100644 index 0000000..899d5ad --- /dev/null +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/RESTRequestMatcher.java @@ -0,0 +1,26 @@ +package it.fabioformosa.quartzmanager.security; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.util.matcher.ELRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +public class RESTRequestMatcher { + + static private final Logger log = LoggerFactory.getLogger(RESTRequestMatcher.class); + + static public RequestMatcher matcherRequestedWith = new ELRequestMatcher( + "hasHeader('X-Requested-With','XMLHttpRequest')"); + static public RequestMatcher matcherAccept = new ELRequestMatcher( + "hasHeader('accept','application/json, text/plain, */*')"); + + static public boolean isRestRequest(HttpServletRequest request) { + log.trace("Detecting if it's an AJAX Request: " + request.getRequestURL() + " accept: " + + request.getHeader("accept") + " " + " X-Requested-With: " + + request.getHeader("X-Requested-With")); + return matcherRequestedWith.matches(request) || matcherAccept.matches(request); + } + +} \ No newline at end of file diff --git a/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/WebsocketRequestMatcher.java b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/WebsocketRequestMatcher.java new file mode 100644 index 0000000..28f64db --- /dev/null +++ b/quartz-manager/src/main/java/it/fabioformosa/quartzmanager/security/WebsocketRequestMatcher.java @@ -0,0 +1,18 @@ +package it.fabioformosa.quartzmanager.security; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WebsocketRequestMatcher { + + static private final Logger log = LoggerFactory.getLogger(WebsocketRequestMatcher.class); + + static public boolean isWebsocketConnectionRequest(HttpServletRequest request) { + log.trace("Detecting if it's a Websocket Connection Request: " + request.getRequestURL()); + return request.getServletPath().equals("/progress/info") + || request.getServletPath().equals("/logs/info"); + } + +} \ No newline at end of file diff --git a/quartz-manager/src/main/resources/application.properties b/quartz-manager/src/main/resources/application.properties index 046b2ac..a8bae81 100644 --- a/quartz-manager/src/main/resources/application.properties +++ b/quartz-manager/src/main/resources/application.properties @@ -1,5 +1,6 @@ server.context-path=/quartz-manager server.port=9000 +server.session.timeout=28800 spring.thymeleaf.cache=false spring.thymeleaf.mode=LEGACYHTML5 diff --git a/quartz-manager/src/main/resources/static/js/app/app.js b/quartz-manager/src/main/resources/static/js/app/app.js index 164a866..c1ee36e 100644 --- a/quartz-manager/src/main/resources/static/js/app/app.js +++ b/quartz-manager/src/main/resources/static/js/app/app.js @@ -1 +1 @@ -var schedulerApp = angular.module('schedulerApp', ['starter', 'configurator', 'ff-websocket', 'progress']); +var schedulerApp = angular.module('schedulerApp', ['starter', 'configurator', 'ff-websocket', 'progress', 'http-auth-interceptor', 'authenticationComp']); diff --git a/quartz-manager/src/main/resources/static/js/app/components/authentication/authentication.js b/quartz-manager/src/main/resources/static/js/app/components/authentication/authentication.js new file mode 100644 index 0000000..ccef49c --- /dev/null +++ b/quartz-manager/src/main/resources/static/js/app/components/authentication/authentication.js @@ -0,0 +1,134 @@ +'use strict'; +var authenticationComp = angular.module('authenticationComp', ['http-auth-interceptor']); + + +/**********************************************************************************/ +//SESSION REFRESH +authenticationComp.directive('sessionRefresh', + ['authService','$rootScope', '$http', '$location', + 'LoginService', function(authService, $rootScope, $http, $location, LoginService) { +var openedLoginDialog = false; +return{ + restrict: 'AC', + link: function(scope, elem, attrs) { + + var lastRefreshTimeForAnonymous; + + $rootScope.$on('event:auth-loginRequired', function(e, arg) { + //show login dialog + arg.ajaxLoginError = arg.ajaxLoginError || ""; + + attrs.title = attrs.title || "Session Expired"; + attrs.commonErrorLabel = attrs.commonErrorLabel || "Authenticaion failed:"; + attrs.wrongPasswordMsg = attrs.wrongPasswordMsg || "Wrong Password! Please, re-try."; + attrs.serverConnectionFailedMsg = attrs.serverConnectionFailedMsg || "Server connection failed, please try later"; + + var username = attrs.username || "admin"; + + if(openedLoginDialog) + return; + + openedLoginDialog = true; + bootbox.dialog({ + message: "
" + + "

Login

"+ + "" + arg.ajaxLoginError + " "+ + "" + + "" + + "" + + "" + + "" + + ""+ + "" + + "
" + + "
", + title: attrs.title, + onEscape: function() { + openedLoginDialog = false; + }, + buttons:{ + main:{ + label: "Ok", + className: "btn-primary", + callback: function() { + openedLoginDialog = false; + var postData = {}; + postData.ajaxUsername = $('#ajaxLoginUsername').val(); + postData.ajaxPassword = $('#ajaxLoginPassword').val(); + + var loginCompleted = LoginService.doLogin(postData.ajaxUsername, postData.ajaxPassword, true); + + loginCompleted + .then(function(){ + authService.loginConfirmed(); + },function(msgError){ + $rootScope.$emit('event:auth-loginRequired', {ajaxLoginError: msgError}); + }); + } + }, + }, + }); + + $('#refreshSessionDialog').closest('.modal').css('z-index', '9001'); + }); + + $rootScope.$on('event:auth-loginConfirmed', function() { + //nothing + }); + + } + }; +}]); + +/**********************************************************************************/ +//LOGIN SERVICE +authenticationComp.service('LoginService', ['$q', '$http', '$window', '$log', function($q, $http, $window, $log){ + + this.doLogin = function(username, password, ignoreAuthModule){ + ignoreAuthModule = ignoreAuthModule || false; + + var ajaxLoginDone = $q.defer(); + + var postData = {}; + postData.ajaxUsername = username; + postData.ajaxPassword = password; + + $http({ + method: 'POST', + url: 'ajaxLogin', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + transformRequest: function(obj) { + var str = []; + for(var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: postData, + ignoreAuthModule : ignoreAuthModule + }) + .then(function (res) { + if(res.status == 200){ + for(var i in res.cookies) + if(res.cookies[i] != null) + document.cookie = res.cookies[i]; + ajaxLoginDone.resolve(res.loggedUser); + } + else + ajaxLoginDone.reject(res.msgError); + }, function(data, status){ + ajaxLoginDone.reject(); + }); + + return ajaxLoginDone.promise; + }; + + +}]); diff --git a/quartz-manager/src/main/resources/static/js/app/components/progress/logs-directive.js b/quartz-manager/src/main/resources/static/js/app/components/progress/logs-directive.js index 3493948..4f64983 100644 --- a/quartz-manager/src/main/resources/static/js/app/components/progress/logs-directive.js +++ b/quartz-manager/src/main/resources/static/js/app/components/progress/logs-directive.js @@ -1,23 +1,15 @@ angular.module('progress') .directive('logsPanel', ['LogService', function(LogService){ - var appendLog = function(log){ - var logTable = document.getElementById('logTable'); - var row = document.createElement('tr'); - row.style.wordWrap = 'break-word'; - row.appendChild(document.createTextNode(log)); - logTable.appendChild(row); - } - var MAX_LOGS = 10; return{ restrict: 'E', - controller : ['$scope', function($scope){ + controller : ['$scope', '$rootScope', '$http', function($scope, $rootScope, $http){ $scope.logs = new Array(); - LogService.receive().then(null, null, function(logRecord){ + var _showNewLog = function(logRecord){ if($scope.logs.length > MAX_LOGS) $scope.logs.pop(); @@ -28,7 +20,31 @@ angular.module('progress') logItem.threadName = logRecord.threadName; $scope.logs.unshift(logItem); - }) + }; + + var _refreshSession = function(){ + $http({ + method : 'GET', + url : 'session/refresh' + }); + }; + + var _handleNewMsgFromLogWebsocket = function(receivedMsg){ + if(receivedMsg.type == 'SUCCESS') + _showNewLog(receivedMsg.message); + else if(receivedMsg.type == 'ERROR') + _refreshSession(); //if websocket has been closed for session expiration, try to refresh it + }; + + + LogService.receive().then(null, null, function(receivedMsg){ + _handleNewMsgFromLogWebsocket(receivedMsg); + }); + + $rootScope.$on('event:auth-loginConfirmed', function() { + //REST API login succeeded, now open websocket connection again + LogService.reconnectNow(); + }); }], templateUrl : 'templates/manager/logs-panel.html' }; diff --git a/quartz-manager/src/main/resources/static/js/app/components/progress/progress-directive.js b/quartz-manager/src/main/resources/static/js/app/components/progress/progress-directive.js index ea4a5f9..f204aa8 100644 --- a/quartz-manager/src/main/resources/static/js/app/components/progress/progress-directive.js +++ b/quartz-manager/src/main/resources/static/js/app/components/progress/progress-directive.js @@ -1,16 +1,25 @@ angular.module('progress') -.directive('progressPanel',['ProgressService', function(ProgressService){ - - return{ - restrict: 'E', - controller : ['$scope', function($scope){ + .directive('progressPanel', [ 'ProgressService', function(ProgressService) { + + return { + restrict : 'E', + controller : [ '$scope', '$rootScope', function($scope, $rootScope) { + + ProgressService.receive().then(null, null, function(receivedMsg) { + if (receivedMsg.type == 'SUCCESS') { + var newStatus = receivedMsg.message; + $scope.progress = newStatus; + $scope.percentageStr = $scope.progress.percentage + '%'; + } + }); + + $rootScope.$on('event:auth-loginConfirmed', function() { + //re-open websocket connection after REST API login + ProgressService.reconnectNow(); + }); + + }], - ProgressService.receive().then(null, null, function(newStatus){ - $scope.progress = newStatus; - $scope.percentageStr = $scope.progress.percentage + '%'; - }); - - }], - templateUrl : 'templates/manager/progress-panel.html' - }; -}]); \ No newline at end of file + templateUrl : 'templates/manager/progress-panel.html' + }; + } ]); \ No newline at end of file diff --git a/quartz-manager/src/main/resources/static/js/app/components/websocket/websocket-factory.js b/quartz-manager/src/main/resources/static/js/app/components/websocket/websocket-factory.js index af4f734..eba4749 100644 --- a/quartz-manager/src/main/resources/static/js/app/components/websocket/websocket-factory.js +++ b/quartz-manager/src/main/resources/static/js/app/components/websocket/websocket-factory.js @@ -29,6 +29,7 @@ angular.module('ff-websocket') var getMessage = function(data) { var out = {}; + out.type = 'SUCCESS'; out.message = JSON.parse(data.body); out.headers = {}; out.headers.messageId = data.headers["message-id"]; @@ -40,24 +41,42 @@ angular.module('ff-websocket') return out; }; - that.reconnect = function() { - $timeout(function() { - initialize(); + that.reconnectionPromise = null; + + that.scheduleReconnection = function() { + that.reconnectionPromise = $timeout(function() { + console.log("Socket reconnecting... (if it fails, next attempt in " + that.options.RECONNECT_TIMEOUT + " msec)"); + _initialize(); }, that.options.RECONNECT_TIMEOUT); }; + + that.reconnectNow = function(){ + _socket.stomp.disconnect(); + if(that.reconnectionPromise && that.reconnectionPromise.cancel) + that.reconnectionPromise.cancel(); + _initialize(); + }; + + var _errorCallback = function(errorMsg){ + var out = {}; + out.type = 'ERROR'; + out.message = errorMsg; + _deferred.notify(out); + that.scheduleReconnection(); + }; var _startListener = function(frame){ console.log('Connected: ' + frame); _socket.stomp.subscribe(that.options.TOPIC_NAME, function(data){ - _deferred.notify(getMessage(data).message); + _deferred.notify(getMessage(data)); }); }; var _initialize = function(){ _socket.client = new SockJS(that.options.SOCKET_URL); _socket.stomp = Stomp.over(_socket.client); - _socket.stomp.connect({}, _startListener); - _socket.stomp.onclose = that.reconnect; + _socket.stomp.connect({}, _startListener, _errorCallback); + _socket.stomp.onclose = that.scheduleReconnection; }; that.receive = function(){ diff --git a/quartz-manager/src/main/resources/static/js/lib/bootbox.min.js b/quartz-manager/src/main/resources/static/js/lib/bootbox.min.js new file mode 100644 index 0000000..0dc0cbd --- /dev/null +++ b/quartz-manager/src/main/resources/static/js/lib/bootbox.min.js @@ -0,0 +1,6 @@ +/** + * bootbox.js v4.4.0 + * + * http://bootboxjs.com/license.txt + */ +!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error("Please pass an array of input options");if(!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),a.maxlength&&h.attr("maxlength",a.maxlength),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(".modal-dialog"),i=d.find(".modal-body"),j=a.buttons,k="",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error("$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.");if(g(j,function(a,b){k+="",l[a]=b.callback}),i.find(".bootbox-body").html(a.message),a.animate===!0&&d.addClass("fade"),a.className&&d.addClass(a.className),"large"===a.size?f.addClass("modal-lg"):"small"===a.size&&f.addClass("modal-sm"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(".modal-header").prepend(m):m.css("margin-top","-10px").prependTo(i)}return a.title&&d.find(".modal-title").html(a.title),k.length&&(i.after(n.footer),d.find(".modal-footer").html(k)),d.on("hidden.bs.modal",function(a){a.target===this&&d.remove()}),d.on("shown.bs.modal",function(){d.find(".btn-primary:first").focus()}),"static"!==a.backdrop&&d.on("click.dismiss.bs.modal",function(a){d.children(".modal-backdrop").length&&(a.currentTarget=d.children(".modal-backdrop").get(0)),a.target===a.currentTarget&&d.trigger("escape.close.bb")}),d.on("escape.close.bb",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on("click",".modal-footer button",function(a){var c=b(this).data("bb-handler");e(a,d,l[c])}),d.on("click",".bootbox-close-button",function(a){e(a,d,l.onEscape)}),d.on("keyup",function(a){27===a.which&&d.trigger("escape.close.bb")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?"static":!1,keyboard:!1,show:!1}),a.show&&d.modal("show"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={bg_BG:{OK:"Ок",CANCEL:"Отказ",CONFIRM:"Потвърждавам"},br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fa:{OK:"قبول",CANCEL:"لغو",CONFIRM:"تایید"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},hu:{OK:"OK",CANCEL:"Mégsem",CONFIRM:"Megerősít"},hr:{OK:"OK",CANCEL:"Odustani",CONFIRM:"Potvrdi"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sq:{OK:"OK",CANCEL:"Anulo",CONFIRM:"Prano"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},th:{OK:"ตกลง",CANCEL:"ยกเลิก",CONFIRM:"ยืนยัน"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.addLocale=function(a,c){return b.each(["OK","CANCEL","CONFIRM"],function(a,b){if(!c[b])throw new Error("Please supply a translation for '"+b+"'")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults("locale",a)},p.init=function(c){return a(c||b)},p}); \ No newline at end of file diff --git a/quartz-manager/src/main/resources/static/js/lib/http-auth-interceptor.js b/quartz-manager/src/main/resources/static/js/lib/http-auth-interceptor.js new file mode 100644 index 0000000..76a3181 --- /dev/null +++ b/quartz-manager/src/main/resources/static/js/lib/http-auth-interceptor.js @@ -0,0 +1,141 @@ +/*global angular:true, browser:true */ + +/** + * @license HTTP Auth Interceptor Module for AngularJS + * (c) 2012 Witold Szczerba + * License: MIT + */ + +(function () { + 'use strict'; + + angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer']) + + .factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) { + return { + /** + * Call this function to indicate that authentication was successful and trigger a + * retry of all deferred requests. + * @param data an optional argument to pass on to $broadcast which may be useful for + * example if you need to pass through details of the user that was logged in + * @param configUpdater an optional transformation function that can modify the + * requests that are retried after having logged in. This can be used for example + * to add an authentication token. It must return the request. + */ + loginConfirmed: function(data, configUpdater) { + var updater = configUpdater || function(config) {return config;}; + $rootScope.$broadcast('event:auth-loginConfirmed', data); + httpBuffer.retryAll(updater); + }, + + /** + * Call this function to indicate that authentication should not proceed. + * All deferred requests will be abandoned or rejected (if reason is provided). + * @param data an optional argument to pass on to $broadcast. + * @param reason if provided, the requests are rejected; abandoned otherwise. + */ + loginCancelled: function(data, reason) { + httpBuffer.rejectAll(reason); + $rootScope.$broadcast('event:auth-loginCancelled', data); + } + }; + }]) + + /** + * $http interceptor. + * On 401 response (without 'ignoreAuthModule' option) stores the request + * and broadcasts 'event:auth-loginRequired'. + * On 403 response (without 'ignoreAuthModule' option) discards the request + * and broadcasts 'event:auth-forbidden'. + */ + .config(['$httpProvider', function($httpProvider) { + $httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) { + return { + responseError: function(rejection) { + var config = rejection.config || {}; + if (!config.ignoreAuthModule) { + switch (rejection.status) { + case 401: + var deferred = $q.defer(); + var bufferLength = httpBuffer.append(config, deferred); + if (bufferLength === 1) + $rootScope.$broadcast('event:auth-loginRequired', rejection); + return deferred.promise; + case 403: + $rootScope.$broadcast('event:auth-forbidden', rejection); + break; + } + } + // otherwise, default behaviour + return $q.reject(rejection); + } + }; + }]); + }]); + + /** + * Private module, a utility, required internally by 'http-auth-interceptor'. + */ + angular.module('http-auth-interceptor-buffer', []) + + .factory('httpBuffer', ['$injector', function($injector) { + /** Holds all the requests, so they can be re-requested in future. */ + var buffer = []; + + /** Service initialized later because of circular dependency problem. */ + var $http; + + function retryHttpRequest(config, deferred) { + function successCallback(response) { + deferred.resolve(response); + } + function errorCallback(response) { + deferred.reject(response); + } + $http = $http || $injector.get('$http'); + $http(config).then(successCallback, errorCallback); + } + + return { + /** + * Appends HTTP request configuration object with deferred response attached to buffer. + * @return {Number} The new length of the buffer. + */ + append: function(config, deferred) { + return buffer.push({ + config: config, + deferred: deferred + }); + }, + + /** + * Abandon or reject (if reason provided) all the buffered requests. + */ + rejectAll: function(reason) { + if (reason) { + for (var i = 0; i < buffer.length; ++i) { + buffer[i].deferred.reject(reason); + } + } + buffer = []; + }, + + /** + * Retries all the buffered requests clears the buffer. + */ + retryAll: function(updater) { + for (var i = 0; i < buffer.length; ++i) { + var _cfg = updater(buffer[i].config); + if (_cfg !== false) + retryHttpRequest(_cfg, buffer[i].deferred); + } + buffer = []; + } + }; + }]); +})(); + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = 'http-auth-interceptor'; +} \ No newline at end of file diff --git a/quartz-manager/src/main/resources/static/js/lib/http-auth-interceptor.min.js b/quartz-manager/src/main/resources/static/js/lib/http-auth-interceptor.min.js new file mode 100644 index 0000000..167529a --- /dev/null +++ b/quartz-manager/src/main/resources/static/js/lib/http-auth-interceptor.min.js @@ -0,0 +1 @@ +!function(){"use strict";angular.module("http-auth-interceptor",["http-auth-interceptor-buffer"]).factory("authService",["$rootScope","httpBuffer",function($rootScope,httpBuffer){return{loginConfirmed:function(data,configUpdater){var updater=configUpdater||function(config){return config};$rootScope.$broadcast("event:auth-loginConfirmed",data),httpBuffer.retryAll(updater)},loginCancelled:function(data,reason){httpBuffer.rejectAll(reason),$rootScope.$broadcast("event:auth-loginCancelled",data)}}}]).config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$rootScope","$q","httpBuffer",function($rootScope,$q,httpBuffer){return{responseError:function(rejection){var config=rejection.config||{};if(!config.ignoreAuthModule)switch(rejection.status){case 401:var deferred=$q.defer(),bufferLength=httpBuffer.append(config,deferred);return 1===bufferLength&&$rootScope.$broadcast("event:auth-loginRequired",rejection),deferred.promise;case 403:$rootScope.$broadcast("event:auth-forbidden",rejection)}return $q.reject(rejection)}}}])}]),angular.module("http-auth-interceptor-buffer",[]).factory("httpBuffer",["$injector",function($injector){function retryHttpRequest(config,deferred){function successCallback(response){deferred.resolve(response)}function errorCallback(response){deferred.reject(response)}$http=$http||$injector.get("$http"),$http(config).then(successCallback,errorCallback)}var $http,buffer=[];return{append:function(config,deferred){return buffer.push({config:config,deferred:deferred})},rejectAll:function(reason){if(reason)for(var i=0;i - @@ -15,10 +15,16 @@ + + + + + + @@ -33,6 +39,9 @@ + + + diff --git a/quartz-manager/src/main/resources/templates/layouts/standard.html b/quartz-manager/src/main/resources/templates/layouts/standard.html index 706653f..b3a2d4d 100644 --- a/quartz-manager/src/main/resources/templates/layouts/standard.html +++ b/quartz-manager/src/main/resources/templates/layouts/standard.html @@ -57,5 +57,21 @@
+ + + \ No newline at end of file