├── client ├── src │ ├── main │ │ └── webapp │ │ │ ├── scss │ │ │ ├── custom │ │ │ │ ├── view │ │ │ │ │ ├── _login.scss │ │ │ │ │ ├── _main.scss │ │ │ │ │ └── _customerSearch.scss │ │ │ │ └── common │ │ │ │ │ ├── _error.scss │ │ │ │ │ ├── _bootstrap.scss │ │ │ │ │ ├── _global.scss │ │ │ │ │ ├── _layout.scss │ │ │ │ │ └── _footer.scss │ │ │ ├── _config.scss │ │ │ ├── _custom.scss │ │ │ └── main.scss │ │ │ ├── js │ │ │ └── custom │ │ │ │ ├── constants.js │ │ │ │ ├── service.js │ │ │ │ ├── controller.js │ │ │ │ ├── directive.js │ │ │ │ ├── constants │ │ │ │ ├── propertiesConstant.js │ │ │ │ └── storageConstant.js │ │ │ │ ├── app.js │ │ │ │ ├── services │ │ │ │ ├── titleService.js │ │ │ │ ├── userService.js │ │ │ │ ├── messageService.js │ │ │ │ ├── storageService.js │ │ │ │ ├── customerService.js │ │ │ │ ├── authenticationService.js │ │ │ │ └── base64Service.js │ │ │ │ ├── listener.js │ │ │ │ ├── directives │ │ │ │ └── exampleFocus.js │ │ │ │ ├── controllers │ │ │ │ ├── MenuController.js │ │ │ │ ├── LoginController.js │ │ │ │ └── CustomerController.js │ │ │ │ └── config.js │ │ │ ├── img │ │ │ └── favicon.png │ │ │ ├── html │ │ │ └── partials │ │ │ │ ├── common │ │ │ │ ├── header.html │ │ │ │ ├── footer.html │ │ │ │ ├── error.html │ │ │ │ ├── alert.html │ │ │ │ └── menu.html │ │ │ │ └── view │ │ │ │ ├── main.html │ │ │ │ ├── login.html │ │ │ │ └── customer_search.html │ │ │ └── index.html │ └── test │ │ └── webapp │ │ ├── specs │ │ ├── jasmine │ │ │ ├── jasmine_favicon.png │ │ │ ├── console.js │ │ │ ├── boot.js │ │ │ ├── jasmine-html.js │ │ │ └── jasmine.css │ │ ├── custom │ │ │ ├── base64Service.spec.js │ │ │ ├── authenticationService.spec.js │ │ │ ├── customerService.spec.js │ │ │ └── customerController.spec.js │ │ └── angular │ │ │ └── angular-jasmine.js │ │ └── html │ │ └── jasmine-index.html ├── .gitignore ├── package.json ├── pom.xml └── gulpfile.babel.js ├── .gitignore ├── .travis.yml ├── api ├── .gitignore └── src │ ├── main │ ├── java │ │ └── au │ │ │ └── com │ │ │ └── example │ │ │ ├── service │ │ │ ├── user │ │ │ │ ├── UserService.java │ │ │ │ ├── model │ │ │ │ │ ├── MembershipDetail.java │ │ │ │ │ ├── SpringMembershipDetail.java │ │ │ │ │ ├── SpringUserDetail.java │ │ │ │ │ └── UserDetail.java │ │ │ │ └── UserServiceImpl.java │ │ │ ├── CustomerService.java │ │ │ ├── authentication │ │ │ │ ├── TokenAuthenticationService.java │ │ │ │ ├── TokenAuthenticationServiceImpl.java │ │ │ │ └── TokenHandler.java │ │ │ └── CustomerServiceImpl.java │ │ │ ├── persistence │ │ │ ├── dao │ │ │ │ ├── user │ │ │ │ │ ├── entity │ │ │ │ │ │ ├── MembershipType.java │ │ │ │ │ │ ├── MembershipEntity.java │ │ │ │ │ │ └── UserEntity.java │ │ │ │ │ ├── UserDAO.java │ │ │ │ │ ├── query │ │ │ │ │ │ ├── SelectUser.java │ │ │ │ │ │ └── UpdatePassword.java │ │ │ │ │ └── UserDAOImpl.java │ │ │ │ ├── customer │ │ │ │ │ ├── CustomerDao.java │ │ │ │ │ ├── query │ │ │ │ │ │ └── SelectCustomer.java │ │ │ │ │ ├── entity │ │ │ │ │ │ └── CustomerEntity.java │ │ │ │ │ └── CustomerDaoImpl.java │ │ │ │ └── base │ │ │ │ │ ├── query │ │ │ │ │ ├── QueryString.java │ │ │ │ │ └── QueryParameter.java │ │ │ │ │ └── BaseDao.java │ │ │ ├── exceptions │ │ │ │ ├── CreateUserException.java │ │ │ │ ├── DeleteUserException.java │ │ │ │ ├── UpdateDeleteException.java │ │ │ │ ├── UpdateUserException.java │ │ │ │ └── ChangePasswordException.java │ │ │ └── provider │ │ │ │ └── CustomDaoAuthenticationProvider.java │ │ │ ├── exception │ │ │ └── UpdateDeleteException.java │ │ │ ├── api │ │ │ ├── model │ │ │ │ └── customer │ │ │ │ │ └── Customer.java │ │ │ └── controller │ │ │ │ ├── user │ │ │ │ └── UserController.java │ │ │ │ ├── authentication │ │ │ │ └── AuthenticationController.java │ │ │ │ └── customer │ │ │ │ └── CustomerController.java │ │ │ ├── spring │ │ │ ├── security │ │ │ │ ├── UnauthorisedEntryPoint.java │ │ │ │ ├── StatelessAuthenticationFilter.java │ │ │ │ └── StatelessTokenAuthenticationFilter.java │ │ │ ├── PersistenceConfig.java │ │ │ ├── AppConfig.java │ │ │ └── SecurityConfig.java │ │ │ ├── constant │ │ │ └── Constants.java │ │ │ └── utils │ │ │ ├── CopyUtils.java │ │ │ └── AuthenticationUtils.java │ ├── resources │ │ ├── log4j.properties │ │ ├── import.sql │ │ └── META-INF │ │ │ └── persistence.xml │ └── webapp │ │ └── WEB-INF │ │ ├── web.xml │ │ └── resources │ │ └── properties │ │ └── clientMessages.properties │ ├── test │ ├── resources │ │ ├── import.sql │ │ └── META-INF │ │ │ └── persistence.xml │ └── java │ │ └── au │ │ └── com │ │ └── example │ │ └── user │ │ ├── UserControllerEndpointUnitTest.java │ │ └── UserDetailsManagerUnitTest.java │ └── integration-test │ └── java │ └── au │ └── com │ └── speak │ └── persistence │ └── dao │ └── user │ └── UserDaoIntegrationTest.java ├── pom.xml ├── LICENSE └── README.md /client/src/main/webapp/scss/custom/view/_login.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/view/_main.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | angular_bootstrap_spring.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: openjdk16 3 | sudo: false 4 | script: mvn clean verify 5 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | target 3 | .idea 4 | api.iml -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | node 4 | src/main/webapp/css/style.css 5 | client.iml -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.constants', []); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services', ['app.constants']); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.controllers', ['app.services']); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.directives', ['app.services']); 4 | -------------------------------------------------------------------------------- /client/src/main/webapp/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rob-Leggett/angular_bootstrap_spring/HEAD/client/src/main/webapp/img/favicon.png -------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/common/_error.scss: -------------------------------------------------------------------------------- 1 | div.help-block { 2 | color: red; 3 | } 4 | 5 | input.invalid { 6 | border-color: red; 7 | } -------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/view/_customerSearch.scss: -------------------------------------------------------------------------------- 1 | .customer-panel { 2 | width: 300px; 3 | display: inline-block; 4 | margin: 5px; 5 | } -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/common/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/main/webapp/scss/_config.scss: -------------------------------------------------------------------------------- 1 | // bootstrap variable overrides 2 | 3 | // font awesome variable overrides 4 | $fa-font-path: "../fonts/font-awesome"; -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/constants/propertiesConstant.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.constants').constant('propertiesConstant', { 4 | API_URL: '/api' 5 | }); -------------------------------------------------------------------------------- /client/src/test/webapp/specs/jasmine/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rob-Leggett/angular_bootstrap_spring/HEAD/client/src/test/webapp/specs/jasmine/jasmine_favicon.png -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app', ['ngRoute', 'ngMessages', 'ngAria', 'xeditable', 'app.constants', 'app.directives', 'app.controllers', 'app.services']); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/constants/storageConstant.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.constants').constant('storageConstant', { 4 | USER: 'user', 5 | AUTH_TOKEN: 'authToken' 6 | }); -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/view/main.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome

3 |
4 | 5 |
6 | This is the customer library, it is used for maintaining your customer library. 7 |
-------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/common/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | cursor: pointer !important; 3 | 4 | .dropdown-menu { 5 | min-width: 200px; 6 | 7 | .glyphicon { 8 | padding-right: 10px; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/user/UserService.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.user; 2 | 3 | import org.springframework.security.provisioning.UserDetailsManager; 4 | 5 | public interface UserService extends UserDetailsManager { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/common/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/common/_global.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin-left: auto; 3 | margin-right: auto; 4 | } 5 | 6 | @media screen { 7 | body { 8 | max-width: 1024px; 9 | } 10 | } 11 | 12 | @media tablet { 13 | body { 14 | max-width: 768px !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/entity/MembershipType.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user.entity; 2 | 3 | public enum MembershipType { 4 | ADMIN, 5 | 6 | CASUAL, 7 | 8 | STUDENT, 9 | 10 | TEACHER_LITE, 11 | 12 | TEACHER_PROFESSIONAL; 13 | } 14 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/exceptions/CreateUserException.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.exceptions; 2 | 3 | public class CreateUserException extends RuntimeException { 4 | private static final long serialVersionUID = -8712337910120088658L; 5 | 6 | public CreateUserException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/exceptions/DeleteUserException.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.exceptions; 2 | 3 | public class DeleteUserException extends RuntimeException { 4 | private static final long serialVersionUID = 5506033991116551044L; 5 | 6 | public DeleteUserException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/exceptions/UpdateDeleteException.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.exceptions; 2 | 3 | public class UpdateDeleteException extends Exception { 4 | private static final long serialVersionUID = -2751999676085328962L; 5 | 6 | public UpdateDeleteException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/exceptions/UpdateUserException.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.exceptions; 2 | 3 | public class UpdateUserException extends RuntimeException { 4 | private static final long serialVersionUID = -8819614967396644975L; 5 | 6 | public UpdateUserException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/titleService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('titleService', ['$route', '$window', 4 | function ($route, $window) { 5 | var title = $window.document.title; 6 | 7 | this.changeTitle = function () { 8 | $window.document.title = title + " - " + $route.current.title; 9 | }; 10 | }]); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/CustomerService.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service; 2 | 3 | import au.com.example.api.model.customer.Customer; 4 | 5 | import java.util.List; 6 | 7 | public interface CustomerService { 8 | List getCustomers(); 9 | 10 | boolean deleteCustomer(Long id); 11 | 12 | boolean saveCustomer(Customer customer); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/exceptions/ChangePasswordException.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.exceptions; 2 | 3 | public class ChangePasswordException extends RuntimeException { 4 | private static final long serialVersionUID = 2978320980462051681L; 5 | 6 | public ChangePasswordException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/common/error.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
-------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/common/alert.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
-------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/customer/CustomerDao.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.customer; 2 | 3 | import au.com.example.api.model.customer.Customer; 4 | 5 | import java.util.List; 6 | 7 | public interface CustomerDao { 8 | List getCustomers(); 9 | 10 | boolean deleteCustomer(Long id); 11 | 12 | boolean saveCustomer(Customer customer); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/base/query/QueryString.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.base.query; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * QueryString 7 | * 8 | * @author Robert Leggett, O-I 9 | * @version $Id$ 10 | */ 11 | public interface QueryString { 12 | public String getStatement(); 13 | 14 | public List getParameters(); 15 | } 16 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/exception/UpdateDeleteException.java: -------------------------------------------------------------------------------- 1 | package au.com.example.exception; 2 | 3 | public class UpdateDeleteException extends Exception { 4 | private static final long serialVersionUID = -2751999676085328962L; 5 | 6 | public UpdateDeleteException(String message) { 7 | super(message); 8 | } 9 | 10 | public UpdateDeleteException(String message, Throwable e) { 11 | super(message, e); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app').run(['$rootScope', '$http', '$location', 'titleService', 4 | function ($rootScope, $http, $location, titleService) { 5 | $rootScope.navigateTo = "/main"; 6 | 7 | $rootScope.$on('$routeChangeSuccess', function(event, next, current) { 8 | titleService.changeTitle(); 9 | 10 | if ($location.path().indexOf("/login") == -1) { 11 | $rootScope.navigateTo = $location.path(); 12 | } 13 | }); 14 | }]); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/directives/exampleFocus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.directives').directive('exampleFocus', 4 | function ($timeout) { 5 | return { 6 | scope: { 7 | trigger: '@exampleFocus' 8 | }, 9 | link: function (scope, element) { 10 | scope.$watch('trigger', function () { 11 | $timeout(function () { 12 | element[0].focus(); 13 | }); 14 | }); 15 | } 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/authentication/TokenAuthenticationService.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | public interface TokenAuthenticationService { 9 | 10 | void addAuthentication(HttpServletResponse response, Authentication authentication); 11 | 12 | Authentication getAuthentication(HttpServletRequest request); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, consoleAppender, fileAppender 2 | 3 | log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender 4 | log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.consoleAppender.layout.ConversionPattern=[%t] %-5p %c %x - %m%n 6 | 7 | log4j.appender.fileAppender=org.apache.log4j.RollingFileAppender 8 | log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout 9 | log4j.appender.fileAppender.layout.ConversionPattern=[%t] %-5p %c %x - %m%n 10 | log4j.appender.fileAppender.File=application.log -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/userService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('userService', ['$http', '$q', 'propertiesConstant', function ($http, $q, propertiesConstant) { 4 | this.retrieve = function retrieve() { 5 | var d = $q.defer(); 6 | 7 | $http.get(propertiesConstant.API_URL + '/user') 8 | .then(function success(response) { 9 | d.resolve(response.data); 10 | }, function error() { 11 | d.reject(); 12 | }); 13 | 14 | return d.promise; 15 | }; 16 | }]); -------------------------------------------------------------------------------- /api/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | insert into user (account_non_expired, account_non_locked, credentials_non_expired, enabled, first_name, last_name, alias, password, email) values (true, true, true, true, 'Test', 'User', 'Test User', '$2a$10$xT/t.6abkjaRpAkNOrt43OD9Cn2aaS3vgxQsnLtEN7mOi6RpACvbm', 'user@tester.com.au'); 2 | 3 | insert into Customer (first_name, last_name) values ('Foo', 'Bar'); 4 | insert into Customer (first_name, last_name) values ('Jim', 'Sunny'); 5 | insert into Customer (first_name, last_name) values ('Peter', 'Prone'); 6 | insert into Customer (first_name, last_name) values ('Sam', 'Sully'); -------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/common/_layout.scss: -------------------------------------------------------------------------------- 1 | div.section { 2 | border: 1px solid #eee; 3 | border-radius: 15px; 4 | padding: 5px; 5 | } 6 | 7 | div.page-wrapper { 8 | padding-left: 30px; 9 | max-width: 650px; 10 | min-height: 400px; 11 | 12 | div.filter-label { 13 | margin-top: -15px; 14 | } 15 | 16 | div.content { 17 | min-height: 500px; 18 | } 19 | 20 | div.panel-content { 21 | width: 150px; 22 | display: inline; 23 | } 24 | 25 | div.inline { 26 | display: inline-block; 27 | margin-right: 5px; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /api/src/test/resources/import.sql: -------------------------------------------------------------------------------- 1 | insert into user (account_non_expired, account_non_locked, credentials_non_expired, enabled, first_name, last_name, alias, password, email) values (true, true, true, true, 'Test', 'User', 'Test User', '$2a$10$xT/t.6abkjaRpAkNOrt43OD9Cn2aaS3vgxQsnLtEN7mOi6RpACvbm', 'test-user-db@tester.com.au'); 2 | 3 | delete from Customer; 4 | 5 | insert into Customer (first_name, last_name) values ('Foo', 'Bar'); 6 | insert into Customer (first_name, last_name) values ('Jim', 'Sunny'); 7 | insert into Customer (first_name, last_name) values ('Peter', 'Prone'); 8 | insert into Customer (first_name, last_name) values ('Sam', 'Sully'); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/controllers/MenuController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.controllers').controller('MenuController', ['$location', '$scope', 'authenticationService', 'messageService', 4 | function ($location, $scope, authenticationService, messageService) { 5 | $scope.logout = function logout() { 6 | authenticationService.logout() 7 | .then(function() { 8 | $location.path("/logout"); 9 | }) 10 | .catch(function () { 11 | messageService.error("LOGOUT_FAILURE", "We were unable to log you out, please try again."); 12 | }); 13 | }; 14 | }]); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/messageService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('messageService', ['$rootScope', 4 | function ($rootScope) { 5 | $rootScope.errors = []; 6 | $rootScope.alerts = []; 7 | 8 | this.error = function (code, message) { 9 | $rootScope.errors.push({ code: code, message: message }); 10 | }; 11 | 12 | this.info = function (code, message) { 13 | $rootScope.alerts.push({ code: code, message: message }); 14 | }; 15 | 16 | this.clearError = function () { 17 | $rootScope.errors = []; 18 | }; 19 | 20 | this.clearInfo = function () { 21 | $rootScope.alerts = []; 22 | } 23 | }]); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/controllers/LoginController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.controllers').controller('LoginController', ['$location', '$rootScope', '$scope', 'authenticationService', 'messageService', 4 | function ($location, $rootScope, $scope, authenticationService, messageService) { 5 | $scope.login = function (credentials) { 6 | authenticationService.login(credentials) 7 | .then(function() { 8 | $location.path($rootScope.navigateTo); 9 | }) 10 | .catch(function () { 11 | messageService.error("LOGIN_FAILURE", "We were unable to log you in, please try again."); 12 | }); 13 | }; 14 | }]); -------------------------------------------------------------------------------- /client/src/test/webapp/html/jasmine-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v2.0.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/base/query/QueryParameter.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.base.query; 2 | 3 | /** 4 | * QueryParameter 5 | * 6 | * @author Robert Leggett, O-I 7 | * @version $Id$ 8 | */ 9 | public class QueryParameter { 10 | private String name; 11 | 12 | private Object value; 13 | 14 | public QueryParameter(String name, Object value) { 15 | this.name = name; 16 | this.value = value; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | 27 | public Object getValue() { 28 | return value; 29 | } 30 | 31 | public void setValue(Object value) { 32 | this.value = value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/src/main/webapp/scss/custom/common/_footer.scss: -------------------------------------------------------------------------------- 1 | div.page-footer { 2 | display: inline-flex; 3 | padding-top: 9px; 4 | margin: 10px 0 20px; 5 | border-radius: 15px; 6 | width: 100%; 7 | 8 | .page-footer-section { 9 | border-left: 1px solid #eee; 10 | border-right: 1px solid #eee; 11 | padding-left: 10px; 12 | width: 100%; 13 | 14 | .testimonials { 15 | 16 | } 17 | 18 | .company-links { 19 | a { 20 | font-size: 14pt; 21 | text-decoration: none; 22 | 23 | &:hover, &:link, &:active, &:visited { 24 | color: $link-color; 25 | } 26 | } 27 | 28 | .separator { 29 | padding-left: 5px; 30 | padding-right: 5px; 31 | } 32 | } 33 | 34 | .social-buttons { 35 | a { 36 | font-size: 20pt; 37 | text-decoration: none; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /client/src/main/webapp/scss/_custom.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | APPLICATION 3 | ========================================================================== */ 4 | 5 | @import "custom/common/global"; 6 | @import "custom/common/error"; 7 | @import "custom/common/bootstrap"; 8 | @import "custom/common/layout"; 9 | 10 | /* ============================================================================= 11 | CONTENT 12 | ========================================================================== */ 13 | 14 | @import "custom/common/footer"; 15 | 16 | /* ============================================================================= 17 | VIEWS 18 | ========================================================================== */ 19 | 20 | @import "custom/view/main"; 21 | @import "custom/view/login"; 22 | @import "custom/view/customerSearch"; -------------------------------------------------------------------------------- /client/src/main/webapp/scss/main.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | CONFIG 3 | ========================================================================== */ 4 | 5 | @import "config"; 6 | 7 | /* ============================================================================= 8 | BOOTSTRAP 9 | ========================================================================== */ 10 | 11 | @import "bootstrap"; 12 | 13 | /* ============================================================================= 14 | FONT AWESOME 15 | ========================================================================== */ 16 | 17 | @import "font-awesome"; 18 | 19 | /* ============================================================================= 20 | APPLICATION 21 | ========================================================================== */ 22 | 23 | @import "custom"; -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/customer/query/SelectCustomer.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.customer.query; 2 | 3 | import au.com.example.persistence.dao.base.query.QueryParameter; 4 | import au.com.example.persistence.dao.base.query.QueryString; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class SelectCustomer implements QueryString 10 | { 11 | private static final String QUERY = "SELECT c FROM " + "CustomerEntity" + " c WHERE (1=1) "; 12 | 13 | // === QueryString implementation 14 | 15 | public String getStatement() 16 | { 17 | StringBuffer statement = new StringBuffer(QUERY); 18 | 19 | return statement.toString(); 20 | } 21 | 22 | public List getParameters() 23 | { 24 | List parameters = new ArrayList(); 25 | 26 | return parameters; 27 | } 28 | } -------------------------------------------------------------------------------- /client/src/test/webapp/specs/custom/base64Service.spec.js: -------------------------------------------------------------------------------- 1 | describe('Base64Service Tests', function (){ 2 | var base64Service; 3 | 4 | // excuted before each "it" is run. 5 | beforeEach(function (){ 6 | 7 | // load the module. 8 | module('app.services'); 9 | 10 | // inject your service for testing. 11 | // The _underscores_ are a convenience thing 12 | // so you can have your variable name be the 13 | // same as your injected service. 14 | inject(function(_base64Service_) { 15 | base64Service = _base64Service_; 16 | }); 17 | }); 18 | 19 | it('should have an encode function', function () { 20 | expect(angular.isFunction(base64Service.encode)).toBe(true); 21 | }); 22 | 23 | it('should have an decode function', function () { 24 | expect(angular.isFunction(base64Service.decode)).toBe(true); 25 | }); 26 | }); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/CustomerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service; 2 | 3 | import au.com.example.api.model.customer.Customer; 4 | import au.com.example.persistence.dao.customer.CustomerDao; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service(value = "CustomerService") 11 | public class CustomerServiceImpl implements CustomerService { 12 | 13 | @Autowired 14 | private CustomerDao customerDao; 15 | 16 | @Override 17 | public List getCustomers() { 18 | return customerDao.getCustomers(); 19 | } 20 | 21 | @Override 22 | public boolean deleteCustomer(Long id) { 23 | return customerDao.deleteCustomer(id); 24 | } 25 | 26 | @Override 27 | public boolean saveCustomer(Customer customer) { return customerDao.saveCustomer(customer); } 28 | } 29 | -------------------------------------------------------------------------------- /client/src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Customer Library 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/storageService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('storageService', ['$rootScope', '$window', 4 | function ($rootScope, $window) { 5 | this.getLocalItem = function (key) { 6 | return JSON.parse($window.localStorage.getItem(key)); 7 | }; 8 | 9 | this.setLocalItem = function (key, item) { 10 | $window.localStorage.setItem(key, JSON.stringify(item)); 11 | }; 12 | 13 | this.removeLocalItem = function(key) { 14 | $window.localStorage.removeItem(key); 15 | }; 16 | 17 | this.getSessionItem = function (key) { 18 | return JSON.parse($window.sessionStorage.getItem(key)); 19 | }; 20 | 21 | this.setSessionItem = function (key, item) { 22 | $window.sessionStorage.setItem(key, JSON.stringify(item)); 23 | }; 24 | 25 | this.removeSessionItem = function(key) { 26 | $window.sessionStorage.removeItem(key); 27 | } 28 | }]); -------------------------------------------------------------------------------- /client/src/test/webapp/specs/custom/authenticationService.spec.js: -------------------------------------------------------------------------------- 1 | describe('AuthenticationService Tests', function (){ 2 | var authenticationService; 3 | 4 | // excuted before each "it" is run. 5 | beforeEach(function (){ 6 | 7 | // load the module. 8 | module('app.services'); 9 | 10 | // inject your service for testing. 11 | // The _underscores_ are a convenience thing 12 | // so you can have your variable name be the 13 | // same as your injected service. 14 | inject(function(_authenticationService_) { 15 | authenticationService = _authenticationService_; 16 | }); 17 | }); 18 | 19 | it('should have a login function', function () { 20 | expect(angular.isFunction(authenticationService.login)).toBe(true); 21 | }); 22 | 23 | it('should have a logout function', function () { 24 | expect(angular.isFunction(authenticationService.logout)).toBe(true); 25 | }); 26 | }); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/api/model/customer/Customer.java: -------------------------------------------------------------------------------- 1 | package au.com.example.api.model.customer; 2 | 3 | public class Customer { 4 | 5 | private Long id; 6 | private String firstName; 7 | private String lastName; 8 | 9 | public Customer() { 10 | this(null, null, null); 11 | } 12 | 13 | public Customer(Long id, String firstName, String lastName) { 14 | this.id = id; 15 | this.firstName = firstName; 16 | this.lastName = lastName; 17 | } 18 | 19 | public Long getId() { 20 | return id; 21 | } 22 | 23 | public void setId(Long id) { 24 | this.id = id; 25 | } 26 | 27 | public String getFirstName() { 28 | return firstName; 29 | } 30 | 31 | public void setFirstName(String firstName) { 32 | this.firstName = firstName; 33 | } 34 | 35 | public String getLastName() { 36 | return lastName; 37 | } 38 | 39 | public void setLastName(String lastName) { 40 | this.lastName = lastName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/spring/security/UnauthorisedEntryPoint.java: -------------------------------------------------------------------------------- 1 | package au.com.example.spring.security; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Just return 401-unauthorized for every unauthorized request. The client side 14 | * catches this and handles login itself. 15 | */ 16 | @Component(value = "unauthorisedEntryPoint") 17 | public class UnauthorisedEntryPoint implements AuthenticationEntryPoint { 18 | 19 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { 20 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/provider/CustomDaoAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.provider; 2 | 3 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 4 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 5 | import org.springframework.security.core.AuthenticationException; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.provisioning.UserDetailsManager; 8 | 9 | public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider { 10 | 11 | @Override 12 | protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws 13 | AuthenticationException { 14 | 15 | if (getUserDetailsService() instanceof UserDetailsManager) { 16 | // TODO: custom checks if you wish 17 | } 18 | 19 | super.additionalAuthenticationChecks(userDetails, authentication); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/api/controller/user/UserController.java: -------------------------------------------------------------------------------- 1 | package au.com.example.api.controller.user; 2 | 3 | import au.com.example.service.user.model.SpringUserDetail; 4 | import au.com.example.service.user.model.UserDetail; 5 | import au.com.example.utils.AuthenticationUtils; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RestController 14 | @RequestMapping(value = "/user") 15 | public class UserController { 16 | 17 | @PreAuthorize("isAuthenticated()") 18 | @RequestMapping(method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}) 19 | public UserDetail retrieveUser(@AuthenticationPrincipal SpringUserDetail user) { 20 | return AuthenticationUtils.toUserDetail(user); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | au.com.example 6 | angular_bootstrap_spring 7 | pom 8 | 0.0.1-SNAPSHOT 9 | Angular JS with Bootstrap and Spring Maven Webapp 10 | http://maven.apache.org 11 | 12 | 13 | api 14 | client 15 | 16 | 17 | 18 | 19 | Robert Leggett 20 | 21 | Solution Architect 22 | Technical Team Lead 23 | Senior Developer 24 | 25 | http://robertleggett.wordpress.com/ 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/view/login.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /api/src/main/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | org.hibernate.jpa.HibernatePersistenceProvider 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rob Leggett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/UserDAO.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user; 2 | 3 | 4 | import au.com.example.persistence.exceptions.ChangePasswordException; 5 | import au.com.example.persistence.exceptions.CreateUserException; 6 | import au.com.example.persistence.exceptions.DeleteUserException; 7 | import au.com.example.persistence.exceptions.UpdateUserException; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | 11 | public interface UserDAO { 12 | public abstract UserDetails loadUser(String email) 13 | throws UsernameNotFoundException; 14 | 15 | public abstract void changePassword(String username, String password) throws ChangePasswordException; 16 | 17 | public abstract boolean userExists(String username); 18 | 19 | public abstract void createUser(UserDetails user) throws CreateUserException; 20 | 21 | public abstract void updateUser(UserDetails user) throws UpdateUserException; 22 | 23 | public abstract void deleteUser(String username) throws DeleteUserException; 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/user/model/MembershipDetail.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.user.model; 2 | 3 | import au.com.example.persistence.dao.user.entity.MembershipType; 4 | 5 | import java.io.Serializable; 6 | import java.util.Date; 7 | 8 | public class MembershipDetail implements Serializable { 9 | 10 | private Long id; 11 | private MembershipType type; 12 | private Date expire; 13 | 14 | public MembershipDetail() { 15 | } 16 | 17 | public MembershipDetail(Long id, MembershipType type, Date expire) { 18 | this.id = id; 19 | this.type = type; 20 | this.expire = expire; 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | 27 | public void setId(Long id) { 28 | this.id = id; 29 | } 30 | 31 | public MembershipType getType() { 32 | return type; 33 | } 34 | 35 | public void setType(MembershipType type) { 36 | this.type = type; 37 | } 38 | 39 | public Date getExpire() { 40 | return expire; 41 | } 42 | 43 | public void setExpire(Date expire) { 44 | this.expire = expire; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/src/test/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | org.hibernate.jpa.HibernatePersistenceProvider 9 | 10 | au.com.example.persistence.dao.customer.entity.CustomerEntity 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/api/controller/authentication/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package au.com.example.api.controller.authentication; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping(value = "/auth") 14 | public class AuthenticationController { 15 | 16 | private static Logger log = LoggerFactory.getLogger(AuthenticationController.class); 17 | 18 | @PreAuthorize("isAuthenticated()") 19 | @RequestMapping(value = "/login", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE }) 20 | public void login() { } 21 | 22 | @RequestMapping(value = "/logout/validate", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE }) 23 | public void logout(@RequestParam(value = "status") String status) { 24 | if(log.isDebugEnabled()) { 25 | log.debug("Logout status " + status); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/query/SelectUser.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user.query; 2 | 3 | 4 | import au.com.example.constant.Constants; 5 | import au.com.example.persistence.dao.base.query.QueryParameter; 6 | import au.com.example.persistence.dao.base.query.QueryString; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class SelectUser implements QueryString { 12 | private static final String QUERY = "SELECT u FROM " + Constants.ENTITY_USER + " u WHERE (1=1) "; 13 | 14 | private String email; 15 | 16 | public SelectUser(String email) { 17 | this.email = email; 18 | } 19 | 20 | // === QueryString implementation 21 | 22 | @Override 23 | public String getStatement() { 24 | StringBuffer statement = new StringBuffer(QUERY); 25 | 26 | if (email != null) { 27 | statement.append("AND (u.email = :email) "); 28 | } 29 | 30 | return statement.toString(); 31 | } 32 | 33 | @Override 34 | public List getParameters() { 35 | List parameters = new ArrayList<>(); 36 | 37 | if (email != null) { 38 | parameters.add(new QueryParameter("email", email)); 39 | } 40 | 41 | return parameters; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/user/model/SpringMembershipDetail.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.user.model; 2 | 3 | 4 | import au.com.example.persistence.dao.user.entity.MembershipType; 5 | import org.springframework.security.core.GrantedAuthority; 6 | 7 | import java.util.Date; 8 | 9 | public class SpringMembershipDetail implements GrantedAuthority { 10 | 11 | private Long id; 12 | private MembershipType type; 13 | private Date expire; 14 | 15 | public SpringMembershipDetail(Long id, MembershipType type, Date expire) { 16 | this.id = id; 17 | this.type = type; 18 | this.expire = expire; 19 | } 20 | 21 | public Long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(Long id) { 26 | this.id = id; 27 | } 28 | 29 | public MembershipType getType() { 30 | return type; 31 | } 32 | 33 | public void setType(MembershipType type) { 34 | this.type = type; 35 | } 36 | 37 | public Date getExpire() { 38 | return expire; 39 | } 40 | 41 | public void setExpire(Date expire) { 42 | this.expire = expire; 43 | } 44 | 45 | // ========== Implementation ============== 46 | 47 | @Override public String getAuthority() { 48 | return (type == null) ? null : type.name(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/spring/PersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package au.com.example.spring; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.orm.jpa.JpaTransactionManager; 7 | import org.springframework.orm.jpa.LocalEntityManagerFactoryBean; 8 | import org.springframework.transaction.PlatformTransactionManager; 9 | import org.springframework.transaction.annotation.EnableTransactionManagement; 10 | 11 | import javax.persistence.EntityManagerFactory; 12 | 13 | @Configuration 14 | @EnableTransactionManagement 15 | @ComponentScan(basePackages = "au.com.example.persistence") 16 | public class PersistenceConfig { 17 | @Bean 18 | public LocalEntityManagerFactoryBean entityManagerFactory() { 19 | LocalEntityManagerFactoryBean em = new LocalEntityManagerFactoryBean(); 20 | em.setPersistenceUnitName("example-unit"); 21 | 22 | return em; 23 | } 24 | 25 | @Bean 26 | public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { 27 | JpaTransactionManager transactionManager = new JpaTransactionManager(); 28 | transactionManager.setEntityManagerFactory(emf); 29 | 30 | return transactionManager; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/spring/security/StatelessAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package au.com.example.spring.security; 2 | 3 | import au.com.example.constant.Constants; 4 | import au.com.example.service.authentication.TokenAuthenticationService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.filter.GenericFilterBean; 9 | 10 | import javax.servlet.FilterChain; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.ServletRequest; 13 | import javax.servlet.ServletResponse; 14 | import javax.servlet.http.HttpServletRequest; 15 | import java.io.IOException; 16 | 17 | @Component(value = Constants.SECURITY_STATELESS_AUTH_FILTER) 18 | public class StatelessAuthenticationFilter extends GenericFilterBean { 19 | 20 | @Autowired 21 | private TokenAuthenticationService tokenAuthenticationService; 22 | 23 | @Override 24 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 25 | SecurityContextHolder.getContext().setAuthentication( 26 | tokenAuthenticationService.getAuthentication((HttpServletRequest) request)); 27 | 28 | chain.doFilter(request, response); // continue always 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.1-SNAPSHOT", 4 | "description": "example", 5 | "babel": { 6 | "presets": [ 7 | "es2015" 8 | ] 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.7.0", 12 | "babel-preset-es2015": "^6.6.0", 13 | "babel-register": "^6.6.5", 14 | "connect-livereload": "^0.5.3", 15 | "date-format": "0.0.2", 16 | "event-stream": "^3.3.4", 17 | "express": "^4.13.3", 18 | "gulp": "^3.9.0", 19 | "gulp-clean": "^0.3.1", 20 | "gulp-if": "^1.2.5", 21 | "gulp-inject": "^1.5.0", 22 | "gulp-rename": "^1.2.2", 23 | "gulp-sass": "^2.0.4", 24 | "gulp-sourcemaps": "^1.5.2", 25 | "gulp-uglifyjs": "^0.6.2", 26 | "gulp-util": "^3.0.6", 27 | "gulp-zip": "^2.0.3", 28 | "http-proxy": "^1.11.1", 29 | "minimatch": "^3.0.2", 30 | "moment": "^2.10.6", 31 | "morgan": "^1.6.1", 32 | "tiny-lr": "^0.1.6", 33 | "yargs": "^3.20.0" 34 | }, 35 | "dependencies": { 36 | "angular": "~1.8.0", 37 | "angular-animate": "~1.6.1", 38 | "angular-aria": "~1.6.1", 39 | "angular-cookies": "~1.6.1", 40 | "angular-messages": "~1.6.1", 41 | "angular-mocks": "~1.6.1", 42 | "angular-resource": "~1.6.1", 43 | "angular-route": "~1.6.1", 44 | "angular-sanitize": "~1.6.1", 45 | "bootstrap-sass": "~3.4.1", 46 | "font-awesome": "~4.4.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/api/controller/customer/CustomerController.java: -------------------------------------------------------------------------------- 1 | package au.com.example.api.controller.customer; 2 | 3 | import au.com.example.api.model.customer.Customer; 4 | import au.com.example.service.CustomerService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | @RestController 12 | @RequestMapping(value = "/customer") 13 | public class CustomerController { 14 | 15 | @Autowired 16 | private CustomerService customerService; 17 | 18 | @PreAuthorize("isAuthenticated()") 19 | @RequestMapping(method = RequestMethod.GET, produces = "application/json") 20 | public List getCustomers() { 21 | return customerService.getCustomers(); 22 | } 23 | 24 | @PreAuthorize("isAuthenticated()") 25 | @RequestMapping(method = RequestMethod.POST, produces = "application/json") 26 | public boolean saveCustomer(@RequestBody Customer customer) { 27 | return customerService.saveCustomer(customer); 28 | } 29 | 30 | @PreAuthorize("isAuthenticated()") 31 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = "application/json") 32 | public boolean deleteCustomer(@PathVariable Long id) { 33 | return customerService.deleteCustomer(id); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/customerService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('customerService', [ '$http', '$q', 'propertiesConstant', 4 | function ($http, $q, propertiesConstant) { 5 | this.getCustomers = function () { 6 | var d = $q.defer(); 7 | 8 | $http.get(propertiesConstant.API_URL + '/customer') 9 | .then(function success(response) { 10 | d.resolve(response.data); 11 | }, function error(response) { 12 | d.reject(response.status); 13 | }); 14 | 15 | return d.promise; 16 | }; 17 | 18 | this.deleteCustomer = function (id) { 19 | var d = $q.defer(); 20 | 21 | $http.delete(propertiesConstant.API_URL + '/customer/' + id) 22 | .then(function success(response) { 23 | d.resolve(response.data); 24 | }, function error() { 25 | d.reject(); 26 | }); 27 | 28 | return d.promise; 29 | }; 30 | 31 | this.saveCustomer = function (customer) { 32 | var d = $q.defer(); 33 | 34 | $http.post(propertiesConstant.API_URL + '/customer', customer) 35 | .then(function success(response) { 36 | d.resolve(response.data); 37 | }, function error() { 38 | d.reject(); 39 | }); 40 | 41 | return d.promise; 42 | }; 43 | }]); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/constant/Constants.java: -------------------------------------------------------------------------------- 1 | package au.com.example.constant; 2 | 3 | public class Constants { 4 | 5 | // === Security === 6 | public static final String SECURITY_UNAUTH_ENTRY_POINT = "unauthorisedEntryPoint"; 7 | public static final String SECURITY_STATELESS_AUTH_FILTER = "statelessAuthFilter"; 8 | public static final String SECURITY_STATELESS_TOKEN_AUTH_FILTER = "statelessTokenAuthFilter"; 9 | 10 | // === Services === 11 | public static final String SERVICE_USER = "user"; 12 | public static final String SERVICE_TOKEN_AUTH = "tokenAuth"; 13 | public static final String SERVICE_NOTIFICATION = "notification"; 14 | public static final String SERVICE_PAYMENT = "payment"; 15 | public static final String SERVICE_PAY_PAL = "payPal"; 16 | public static final String SERVICE_TRANSLATION = "translation"; 17 | public static final String SERVICE_VERB = "verb"; 18 | 19 | // === Tables === 20 | public static final String TABLE_MEMBERSHIP = "membership"; 21 | public static final String TABLE_TRANSLATION = "translation"; 22 | public static final String TABLE_USER = "user"; 23 | public static final String TABLE_USER_MEMBERSHIP = "user_membership"; 24 | public static final String TABLE_VERB = "verb"; 25 | 26 | // === Entities === 27 | public static final String ENTITY_TRANSLATION = "TranslationEntity"; 28 | public static final String ENTITY_USER = "UserEntity"; 29 | public static final String ENTITY_VERB = "VerbEntity"; 30 | 31 | // === Markers === 32 | public static final String ALL = "all"; 33 | } 34 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/query/UpdatePassword.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user.query; 2 | 3 | import au.com.example.constant.Constants; 4 | import au.com.example.persistence.dao.base.query.QueryParameter; 5 | import au.com.example.persistence.dao.base.query.QueryString; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class UpdatePassword implements QueryString { 11 | private static final String QUERY = "UPDATE " + Constants.ENTITY_USER + " "; 12 | 13 | private String username; 14 | private String password; 15 | 16 | public UpdatePassword(String username, String password) { 17 | this.username = username; 18 | this.password = password; 19 | } 20 | 21 | // === QueryString implementation 22 | 23 | @Override 24 | public String getStatement() { 25 | StringBuffer statement = new StringBuffer(QUERY); 26 | 27 | if (username != null && password != null) { 28 | statement.append("SET password = :password "); 29 | statement.append("WHERE username = :username "); 30 | } 31 | 32 | return statement.toString(); 33 | } 34 | 35 | @Override 36 | public List getParameters() { 37 | List parameters = new ArrayList(); 38 | 39 | if (username != null && password != null) { 40 | parameters.add(new QueryParameter("password", password)); 41 | parameters.add(new QueryParameter("username", username)); 42 | } 43 | 44 | return parameters; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/authenticationService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('authenticationService', ['$rootScope', '$http', '$q', 'base64Service', 'storageService', 'storageConstant', 'propertiesConstant', 4 | function ($rootScope, $http, $q, base64Service, storageService, storageConstant, propertiesConstant) { 5 | this.login = function (credentials) { 6 | var d = $q.defer(); 7 | 8 | $http.defaults.headers.common.Authorization = 'Basic ' + base64Service.encode(credentials.email + ':' + credentials.password); 9 | 10 | $http.post(propertiesConstant.API_URL + '/auth/login', null) 11 | .then(function success(response) { 12 | storageService.setSessionItem(storageConstant.AUTH_TOKEN, response.headers('X-AUTH-TOKEN')); 13 | 14 | delete $http.defaults.headers.common.Authorization; 15 | 16 | d.resolve(); 17 | }, function error() { 18 | d.reject(); 19 | }); 20 | 21 | return d.promise; 22 | }; 23 | 24 | this.logout = function () { 25 | var d = $q.defer(); 26 | 27 | $http.post(propertiesConstant.API_URL + '/auth/logout', null) 28 | .then(function success() { 29 | storageService.removeSessionItem(storageConstant.AUTH_TOKEN); 30 | storageService.removeSessionItem(storageConstant.USER); 31 | 32 | delete $rootScope.user; 33 | 34 | d.resolve(); 35 | }, function error() { 36 | d.reject(); 37 | }); 38 | 39 | return d.promise; 40 | }; 41 | }]); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/user/model/SpringUserDetail.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.user.model; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.userdetails.User; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | 9 | public class SpringUserDetail extends User { 10 | private static final long serialVersionUID = 5148339247016106466L; 11 | 12 | private String firstName; 13 | private String lastName; 14 | private String alias; 15 | 16 | public SpringUserDetail() { 17 | this(null, null, null, null, null, new ArrayList(), false, false, false, false); 18 | } 19 | 20 | public SpringUserDetail( 21 | String email, 22 | String password, 23 | String firstName, 24 | String lastName, 25 | String alias, 26 | Collection authorities, 27 | boolean enabled, 28 | boolean accountNonExpired, 29 | boolean credentialsNonExpired, 30 | boolean accountNonLocked) { 31 | 32 | super(email, password, enabled, accountNonExpired, 33 | credentialsNonExpired, accountNonLocked, authorities); 34 | 35 | this.firstName = firstName; 36 | this.lastName = lastName; 37 | this.alias = alias; 38 | } 39 | 40 | public String getFirstName() { 41 | return firstName; 42 | } 43 | 44 | public String getLastName() { 45 | return lastName; 46 | } 47 | 48 | public String getAlias() { 49 | return alias; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/spring/security/StatelessTokenAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package au.com.example.spring.security; 2 | 3 | import au.com.example.constant.Constants; 4 | import au.com.example.service.authentication.TokenAuthenticationService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.filter.GenericFilterBean; 10 | 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.ServletRequest; 14 | import javax.servlet.ServletResponse; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | 18 | @Component(value = Constants.SECURITY_STATELESS_TOKEN_AUTH_FILTER) 19 | public class StatelessTokenAuthenticationFilter extends GenericFilterBean { 20 | 21 | @Autowired 22 | private TokenAuthenticationService tokenAuthenticationService; 23 | 24 | @Override 25 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 26 | throws IOException, ServletException { 27 | 28 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 29 | 30 | if(authentication != null) { 31 | tokenAuthenticationService 32 | .addAuthentication((HttpServletResponse) response, SecurityContextHolder.getContext().getAuthentication()); 33 | } 34 | 35 | chain.doFilter(request, response); // continue always 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/utils/CopyUtils.java: -------------------------------------------------------------------------------- 1 | package au.com.example.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.Collection; 7 | 8 | public class CopyUtils { 9 | private static Logger log = LoggerFactory.getLogger(CopyUtils.class); 10 | 11 | private static final String METHOD_CLONE = "clone"; 12 | 13 | @SuppressWarnings("unchecked") 14 | public static T clone(T orig) { 15 | T obj = null; 16 | 17 | if (orig != null) { 18 | try { 19 | obj = (T) orig.getClass().getMethod(METHOD_CLONE).invoke(orig); 20 | } catch (Exception e) { 21 | log.error("The clone failed: {}: {}", e.getClass().getName(), e.getMessage()); 22 | } 23 | } 24 | 25 | return obj; 26 | } 27 | 28 | @SuppressWarnings("unchecked") 29 | public static , U extends Cloneable> T cloneCollection( 30 | T orig) { 31 | return (T) cloneCollection(orig.getClass(), orig); 32 | } 33 | 34 | public static , O extends Collection, U extends Cloneable> O cloneCollection( 35 | Class outType, I orig) { 36 | O obj = null; 37 | 38 | if (orig != null) { 39 | try { 40 | obj = outType.getConstructor().newInstance(); 41 | 42 | for (U element : orig) { 43 | obj.add(clone(element)); 44 | } 45 | } catch (Exception e) { 46 | log.error("The cloneCollection failed: {}: {}", e.getClass().getName(), e.getMessage()); 47 | } 48 | } 49 | 50 | return obj; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/customer/entity/CustomerEntity.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.customer.entity; 2 | 3 | import javax.persistence.*; 4 | 5 | @Entity 6 | @Table(name = "Customer") 7 | public class CustomerEntity implements Cloneable { 8 | 9 | private Long id; 10 | private String firstName; 11 | private String lastName; 12 | 13 | public CustomerEntity() { 14 | this(null, null, null); 15 | } 16 | 17 | public CustomerEntity(Long id, String firstName, String lastName) { 18 | this.id = id; 19 | this.firstName = firstName; 20 | this.lastName = lastName; 21 | } 22 | 23 | @Id 24 | @Column(name = "customer_id") 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | public Long getId() { 27 | return id; 28 | } 29 | public void setId(Long id) { 30 | this.id = id; 31 | } 32 | 33 | @Column(name = "first_name", nullable = false) 34 | public String getFirstName() { 35 | return firstName; 36 | } 37 | public void setFirstName(String firstName) { 38 | this.firstName = firstName; 39 | } 40 | 41 | @Column(name = "last_name", nullable = false) 42 | public String getLastName() { 43 | return lastName; 44 | } 45 | public void setLastName(String lastName) { 46 | this.lastName = lastName; 47 | } 48 | 49 | // ====== Cloneable Override ========= 50 | 51 | @Override 52 | public CustomerEntity clone() { 53 | return new CustomerEntity(id, firstName, lastName); 54 | } 55 | 56 | // ====== Overrides ======== 57 | 58 | 59 | @Override 60 | public String toString() { 61 | return "CustomerEntity{" + 62 | "id=" + id + 63 | ", firstName='" + firstName + '\'' + 64 | ", lastName='" + lastName + '\'' + 65 | '}'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/spring/AppConfig.java: -------------------------------------------------------------------------------- 1 | package au.com.example.spring; 2 | 3 | import org.springframework.context.MessageSource; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 8 | import org.springframework.web.servlet.LocaleResolver; 9 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 10 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 12 | import org.springframework.web.servlet.i18n.CookieLocaleResolver; 13 | import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; 14 | 15 | @Configuration 16 | @EnableWebMvc 17 | @ComponentScan(basePackages = { "au.com.example" }) 18 | public class AppConfig extends WebMvcConfigurerAdapter { 19 | 20 | // ========= Overrides =========== 21 | 22 | @Override 23 | public void addInterceptors(InterceptorRegistry registry) { 24 | registry.addInterceptor(new LocaleChangeInterceptor()); 25 | } 26 | 27 | // ========= Beans =========== 28 | 29 | @Bean(name = "localeResolver") 30 | public LocaleResolver getLocaleResolver() { 31 | return new CookieLocaleResolver(); 32 | } 33 | 34 | @Bean(name = "messageSource") 35 | public MessageSource getMessageSources() { 36 | ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); 37 | messageSource.setBasenames("/WEB-INF/resources/properties/clientMessages"); 38 | messageSource.setCacheSeconds(0); 39 | 40 | return messageSource; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/common/menu.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/controllers/CustomerController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.controllers').controller('CustomerController',['$rootScope', '$scope', '$location', 'customerService', 'messageService', 4 | function ($rootScope, $scope, $location, customerService, messageService) { 5 | customerService.getCustomers().then( 6 | function success(customers) { 7 | $scope.customers = customers; 8 | }, 9 | function error(status) { 10 | if(status === 401) { 11 | $location.path("/login"); 12 | } 13 | else { 14 | messageService.error("CUSTOMERS_GET_FAILURE", "Oooooops something went wrong, please try again"); 15 | } 16 | }); 17 | 18 | $scope.remove = function remove(id) { 19 | customerService.deleteCustomer(id).then( 20 | function success(response) { 21 | if (response) { 22 | angular.forEach($scope.customers, function (customer, index) { 23 | if (id == customer.id) { 24 | $scope.customers.splice(index, 1); 25 | } 26 | }); 27 | } 28 | }, 29 | function error() { 30 | messageService.error("CUSTOMER_DELETE_FAILURE", "Oooooops something went wrong, please try again"); 31 | }); 32 | }; 33 | 34 | $scope.save = function (id) { 35 | angular.forEach($scope.customers, function (customer) { 36 | if (id == customer.id) { 37 | customerService.saveCustomer(customer).then( 38 | function success(response) {}); 39 | } 40 | }, 41 | function error() { 42 | messageService.error("CUSTOMER_SAVE_FAILURE", "Oooooops something went wrong, please try again"); 43 | }); 44 | }; 45 | }]); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/user/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.user; 2 | 3 | import au.com.example.constant.Constants; 4 | import au.com.example.persistence.dao.user.UserDAO; 5 | import au.com.example.persistence.exceptions.ChangePasswordException; 6 | import au.com.example.persistence.exceptions.CreateUserException; 7 | import au.com.example.persistence.exceptions.DeleteUserException; 8 | import au.com.example.persistence.exceptions.UpdateUserException; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.dao.DataAccessException; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 | import org.springframework.stereotype.Service; 14 | 15 | @Service(Constants.SERVICE_USER) 16 | public class UserServiceImpl implements UserService { 17 | 18 | @Autowired 19 | private UserDAO userDAO; 20 | 21 | // === UserDetailsService implementation === 22 | 23 | @Override 24 | public UserDetails loadUserByUsername(String email) 25 | throws UsernameNotFoundException, DataAccessException { 26 | return userDAO.loadUser(email); 27 | } 28 | 29 | // === UserDetailsManager implementation === 30 | 31 | @Override 32 | public void changePassword(String email, String password) throws ChangePasswordException { 33 | userDAO.changePassword(email, password); 34 | } 35 | 36 | @Override 37 | public boolean userExists(String email) { 38 | return userDAO.userExists(email); 39 | } 40 | 41 | @Override 42 | public void createUser(UserDetails user) throws CreateUserException { 43 | userDAO.createUser(user); 44 | } 45 | 46 | @Override 47 | public void updateUser(UserDetails user) throws UpdateUserException { 48 | userDAO.updateUser(user); 49 | } 50 | 51 | @Override 52 | public void deleteUser(String email) throws DeleteUserException { 53 | userDAO.deleteUser(email); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /api/src/integration-test/java/au/com/speak/persistence/dao/user/UserDaoIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package au.com.speak.persistence.dao.user; 2 | 3 | import au.com.example.persistence.dao.user.UserDAO; 4 | import au.com.example.service.user.model.SpringUserDetail; 5 | import au.com.example.spring.PersistenceConfig; 6 | import au.com.example.spring.SecurityConfig; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 13 | 14 | import java.util.ArrayList; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | @RunWith(SpringJUnit4ClassRunner.class) 20 | @ContextConfiguration(classes = { SecurityConfig.class, PersistenceConfig.class }) 21 | public class UserDaoIntegrationTest { 22 | 23 | @Autowired 24 | private UserDAO userDao; 25 | 26 | @Test 27 | public void shouldCreateUserSuccessfully() { 28 | userDao.createUser(new SpringUserDetail("test_user1@tester.com.au", "password", "test", "user", "test user updated", new ArrayList(), true, true, true, true)); 29 | 30 | assertTrue(userDao.userExists("test_user1@tester.com.au")); 31 | } 32 | 33 | @Test 34 | public void shouldUpdateUserSuccessfully() { 35 | 36 | SpringUserDetail before = (SpringUserDetail)userDao.loadUser("test-user-db@tester.com.au"); 37 | 38 | assertEquals("Test User", before.getAlias()); 39 | 40 | userDao.updateUser( 41 | new SpringUserDetail("test-user-db@tester.com.au", "$2a$10$xT/t.6abkjaRpAkNOrt43OD9Cn2aaS3vgxQsnLtEN7mOi6RpACvbm", 42 | "Test", "User", "Test User Updated", new ArrayList(), true, true, true, true)); 43 | 44 | SpringUserDetail after = (SpringUserDetail)userDao.loadUser("test-user-db@tester.com.au"); 45 | 46 | assertEquals("Test User Updated", after.getAlias()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/test/webapp/specs/custom/customerService.spec.js: -------------------------------------------------------------------------------- 1 | describe('CustomerService Tests', function (){ 2 | 3 | beforeEach(module('app.services')); 4 | beforeEach(module(function ($provide) { 5 | $provide.value('$scope', { 6 | getCustomers: jasmine.createSpy('$scope.getCustomers'), 7 | deleteCustomer: jasmine.createSpy('$scope.deleteCustomer'), 8 | saveCustomer: jasmine.createSpy('$scope.saveCustomer') 9 | }); 10 | }) 11 | ); 12 | 13 | describe('CustomerService Structural Tests', function() { 14 | 15 | it('should have an get customers function', inject(function ($scope) { 16 | expect(angular.isFunction($scope.getCustomers)).toBe(true); 17 | })); 18 | 19 | it('should have an delete customer function', inject(function ($scope) { 20 | expect(angular.isFunction($scope.deleteCustomer)).toBe(true); 21 | })); 22 | 23 | it('should have an save customer function', inject(function ($scope) { 24 | expect(angular.isFunction($scope.saveCustomer)).toBe(true); 25 | })); 26 | }); 27 | 28 | describe('CustomerService Get Customers Tests', function() { 29 | it('should return results from get customers function call', inject(function ($httpBackend, customerService, propertiesConstant) { 30 | var customers = [ 31 | {"id": 1, "firstName": "Foo", "lastName": "Bar"}, 32 | {"id": 2, "firstName": "Jim", "lastName": "Sunny"}, 33 | {"id": 3, "firstName": "Peter", "lastName": "Prone"}, 34 | {"id": 4, "firstName": "Sam", "lastName": "Sully"} 35 | ]; 36 | 37 | $httpBackend.whenGET(propertiesConstant.API_URL + '/customer').respond(customers); 38 | 39 | // check result returned from service call 40 | customerService.getCustomers().then(function (customers) { 41 | expect(customers).toEqual(customers); 42 | }); 43 | 44 | $httpBackend.flush(); 45 | 46 | $httpBackend.expectGET(propertiesConstant.API_URL + '/customer').respond(customers); 47 | })); 48 | }); 49 | }); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/entity/MembershipEntity.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user.entity; 2 | 3 | import au.com.example.constant.Constants; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.EnumType; 8 | import javax.persistence.Enumerated; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.Id; 11 | import javax.persistence.SequenceGenerator; 12 | import javax.persistence.Table; 13 | import java.util.Date; 14 | 15 | @Entity 16 | @Table(name = Constants.TABLE_MEMBERSHIP) 17 | public class MembershipEntity implements Cloneable { 18 | private Long id; 19 | private MembershipType type; 20 | private Date expire; 21 | 22 | public MembershipEntity() { 23 | this(null, null, null); 24 | } 25 | 26 | public MembershipEntity(Long id, MembershipType type, Date expire) { 27 | this.id = id; 28 | this.type = type; 29 | this.expire = expire; 30 | } 31 | 32 | @Id 33 | @GeneratedValue(generator = "MembershipSeq") 34 | @SequenceGenerator(name = "MembershipSeq", sequenceName = "MEMBERSHIP_SEQ", allocationSize = 1, initialValue = 1) 35 | @Column(name = "membership_id") 36 | public Long getId() { 37 | return id; 38 | } 39 | 40 | public void setId(Long id) { 41 | this.id = id; 42 | } 43 | 44 | @Column(name = "type") 45 | @Enumerated(EnumType.STRING) 46 | public MembershipType getType() { 47 | return type; 48 | } 49 | 50 | public void setType(MembershipType type) { 51 | this.type = type; 52 | } 53 | 54 | @Column(name = "expire") 55 | public Date getExpire() { 56 | return expire; 57 | } 58 | 59 | public void setExpire(Date expire) { 60 | this.expire = expire; 61 | } 62 | 63 | // === Cloneable implementation 64 | 65 | @Override 66 | public MembershipEntity clone() { 67 | return new MembershipEntity(id, type, expire); 68 | } 69 | 70 | // === Overrides 71 | 72 | @Override 73 | public String toString() { 74 | return "MembershipEntity [id=" + id + ", type=" + type + ", expire=" + expire + "]"; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/user/model/UserDetail.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.user.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collection; 5 | 6 | public class UserDetail implements Serializable { 7 | 8 | private String email; 9 | private String firstName; 10 | private String lastName; 11 | private String alias; 12 | private Collection memberships; 13 | private boolean enabled; 14 | private boolean accountNonExpired; 15 | private boolean credentialsNonExpired; 16 | private boolean accountNonLocked; 17 | 18 | public UserDetail() { } 19 | 20 | public UserDetail( 21 | String email, 22 | String firstName, 23 | String lastName, 24 | String alias, 25 | Collection memberships, 26 | boolean enabled, 27 | boolean accountNonExpired, 28 | boolean credentialsNonExpired, 29 | boolean accountNonLocked) { 30 | 31 | this.email = email; 32 | this.firstName = firstName; 33 | this.lastName = lastName; 34 | this.alias = alias; 35 | this.memberships = memberships; 36 | this.enabled = enabled; 37 | this.accountNonExpired = accountNonExpired; 38 | this.credentialsNonExpired = credentialsNonExpired; 39 | this.accountNonLocked = accountNonLocked; 40 | } 41 | 42 | public String getEmail() { 43 | return email; 44 | } 45 | 46 | public String getFirstName() { 47 | return firstName; 48 | } 49 | 50 | public String getLastName() { 51 | return lastName; 52 | } 53 | 54 | public String getAlias() { 55 | return alias; 56 | } 57 | 58 | public Collection getMemberships() { 59 | return memberships; 60 | } 61 | 62 | public boolean getEnabled() { 63 | return enabled; 64 | } 65 | 66 | public boolean getAccountNonExpired() { 67 | return accountNonExpired; 68 | } 69 | 70 | public boolean getCredentialsNonExpired() { 71 | return credentialsNonExpired; 72 | } 73 | 74 | public boolean getAccountNonLocked() { 75 | return accountNonLocked; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/src/test/webapp/specs/custom/customerController.spec.js: -------------------------------------------------------------------------------- 1 | describe('CustomerController Tests', function () { 2 | beforeEach(module('app.controllers')); 3 | beforeEach(module(function ($provide) { 4 | 5 | $provide.value('$scope', { 6 | remove: jasmine.createSpy('$scope.remove'), 7 | save: jasmine.createSpy('$scope.save') 8 | }); 9 | 10 | $provide.factory('customerService', function ($jasmine) { 11 | return $jasmine.createPromiseSpyObj( 12 | 'customerService', [ 'getCustomers', 'deleteCustomer', 'saveCustomer' ] 13 | ); 14 | }); 15 | 16 | $provide.factory('messageService', function ($jasmine) { 17 | return $jasmine.createPromiseSpyObj( 18 | 'messageService', [ 'error' ] 19 | ); 20 | }); 21 | }) 22 | ); 23 | 24 | describe('CustomerController Structural Tests', function () { 25 | it('should have an remove function in scope', inject(function ($scope) { 26 | expect(angular.isFunction($scope.remove)).toBe(true); 27 | })); 28 | 29 | it('should have an save function in scope', inject(function ($scope) { 30 | expect(angular.isFunction($scope.save)).toBe(true); 31 | })); 32 | 33 | }); 34 | 35 | describe('CustomerController Save Tests', function () { 36 | it('should have not saved any data with service call returning true', inject(function ($controller, $scope, customerService, messageService) { 37 | $controller('CustomerController'); 38 | 39 | $scope.customers = [ 40 | {"id": 1, "firstName": "Foo", "lastName": "Bar"}, 41 | {"id": 2, "firstName": "Jim", "lastName": "Sunny"}, 42 | {"id": 3, "firstName": "Peter", "lastName": "Prone"}, 43 | {"id": 4, "firstName": "Sam", "lastName": "Sully"} 44 | ]; 45 | 46 | $scope.save(2); 47 | 48 | customerService.saveCustomer.$resolve(true); 49 | 50 | expect(customerService.saveCustomer).toHaveBeenCalledWith({"id": 2, "firstName": "Jim", "lastName": "Sunny"}); 51 | expect(messageService.error).not.toHaveBeenCalled(); 52 | })); 53 | 54 | }); 55 | }); -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/services/base64Service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app.services').service('base64Service', 4 | function () { 5 | var keyStr = "ABCDEFGHIJKLMNOP" + 6 | "QRSTUVWXYZabcdef" + 7 | "ghijklmnopqrstuv" + 8 | "wxyz0123456789+/" + 9 | "="; 10 | this.encode = function (input) { 11 | var output = "", 12 | chr1, chr2, chr3 = "", 13 | enc1, enc2, enc3, enc4 = "", 14 | i = 0; 15 | 16 | while (i < input.length) { 17 | chr1 = input.charCodeAt(i++); 18 | chr2 = input.charCodeAt(i++); 19 | chr3 = input.charCodeAt(i++); 20 | 21 | enc1 = chr1 >> 2; 22 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 23 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 24 | enc4 = chr3 & 63; 25 | 26 | if (isNaN(chr2)) { 27 | enc3 = enc4 = 64; 28 | } else if (isNaN(chr3)) { 29 | enc4 = 64; 30 | } 31 | 32 | output = output + 33 | keyStr.charAt(enc1) + 34 | keyStr.charAt(enc2) + 35 | keyStr.charAt(enc3) + 36 | keyStr.charAt(enc4); 37 | chr1 = chr2 = chr3 = ""; 38 | enc1 = enc2 = enc3 = enc4 = ""; 39 | } 40 | 41 | return output; 42 | }; 43 | 44 | this.decode = function (input) { 45 | var output = "", 46 | chr1, chr2, chr3 = "", 47 | enc1, enc2, enc3, enc4 = "", 48 | i = 0; 49 | 50 | // remove all characters that are not A-Z, a-z, 0-9, +, /, or = 51 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 52 | 53 | while (i < input.length) { 54 | enc1 = keyStr.indexOf(input.charAt(i++)); 55 | enc2 = keyStr.indexOf(input.charAt(i++)); 56 | enc3 = keyStr.indexOf(input.charAt(i++)); 57 | enc4 = keyStr.indexOf(input.charAt(i++)); 58 | 59 | chr1 = (enc1 << 2) | (enc2 >> 4); 60 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 61 | chr3 = ((enc3 & 3) << 6) | enc4; 62 | 63 | output = output + String.fromCharCode(chr1); 64 | 65 | if (enc3 != 64) { 66 | output = output + String.fromCharCode(chr2); 67 | } 68 | if (enc4 != 64) { 69 | output = output + String.fromCharCode(chr3); 70 | } 71 | 72 | chr1 = chr2 = chr3 = ""; 73 | enc1 = enc2 = enc3 = enc4 = ""; 74 | } 75 | }; 76 | }); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/utils/AuthenticationUtils.java: -------------------------------------------------------------------------------- 1 | package au.com.example.utils; 2 | 3 | import au.com.example.service.user.model.MembershipDetail; 4 | import au.com.example.service.user.model.SpringMembershipDetail; 5 | import au.com.example.service.user.model.SpringUserDetail; 6 | import au.com.example.service.user.model.UserDetail; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | 14 | public final class AuthenticationUtils { 15 | 16 | public static String getUsername() { 17 | Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 18 | 19 | String username = principal.toString(); 20 | 21 | if (principal instanceof UserDetails) { 22 | username = ((UserDetails) principal).getUsername(); 23 | } 24 | 25 | return username; 26 | } 27 | 28 | public static UserDetail toUserDetail(SpringUserDetail springUserDetail) { 29 | return (springUserDetail == null) ? null : new UserDetail( 30 | springUserDetail.getUsername(), 31 | springUserDetail.getFirstName(), 32 | springUserDetail.getLastName(), 33 | springUserDetail.getAlias(), 34 | toMembershipDetail(springUserDetail.getAuthorities()), 35 | springUserDetail.isEnabled(), 36 | springUserDetail.isAccountNonExpired(), 37 | springUserDetail.isCredentialsNonExpired(), 38 | springUserDetail.isAccountNonLocked()); 39 | } 40 | 41 | // ========== HELPERS ========= 42 | 43 | private static Collection toMembershipDetail(Collection grantedAuthorities) { 44 | Collection membershipDetails = new ArrayList<>(); 45 | 46 | for(GrantedAuthority authority : grantedAuthorities) { 47 | if(authority instanceof SpringMembershipDetail) { 48 | SpringMembershipDetail springmembershipDetail = (SpringMembershipDetail)authority; 49 | 50 | membershipDetails.add(new MembershipDetail( 51 | springmembershipDetail.getId(), 52 | springmembershipDetail.getType(), 53 | springmembershipDetail.getExpire())); 54 | } 55 | } 56 | 57 | return membershipDetails; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/src/test/webapp/specs/angular/angular-jasmine.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('ngJasmine', []).service('$jasmine', function $jasmine($q, $rootScope) { 4 | function createPromise(spy) { 5 | var deferred = $q.defer(); 6 | 7 | deferred.promise.$spy = spy; 8 | 9 | spy.$resolve = function $resolve(data) { 10 | deferred.resolve(data); 11 | 12 | $rootScope.$apply(); 13 | }; 14 | 15 | spy.$resolve.$alias = function $resolveAlias(name, aliasFn) { 16 | spy.$promise[name] = function (promiseFn) { 17 | spy.$promise.then(function (data) { 18 | aliasFn(data, promiseFn); 19 | }); 20 | 21 | return spy.$promise; 22 | }; 23 | }; 24 | 25 | spy.$resolve.$action = function $resolveAction() { 26 | Array.prototype.forEach.call(arguments, function (name) { 27 | spy.$resolve.$alias(name, function (data, promiseFn) { 28 | if (data === name) { 29 | promiseFn(); 30 | } 31 | }); 32 | }); 33 | }; 34 | 35 | spy.$reject = function $reject(error) { 36 | deferred.reject(error); 37 | 38 | $rootScope.$apply(); 39 | }; 40 | 41 | spy.$reject.$alias = function $rejectAlias(name, aliasFn) { 42 | spy.$promise[name] = function (promiseFn) { 43 | spy.$promise.catch(function (error) { 44 | aliasFn(error, promiseFn); 45 | }); 46 | 47 | return spy.$promise; 48 | }; 49 | }; 50 | 51 | spy.$reject.$action = function $rejectAction() { 52 | Array.prototype.forEach.call(arguments, function (name) { 53 | spy.$reject.$alias(name, function (error, promiseFn) { 54 | if (error === name) { 55 | promiseFn(); 56 | } 57 | }); 58 | }); 59 | }; 60 | 61 | return spy.and.returnValue(spy.$promise = deferred.promise); 62 | } 63 | 64 | this.createPromiseSpy = function createPromiseSpy(name) { 65 | return createPromise(jasmine.createSpy(name)); 66 | }; 67 | 68 | this.createPromiseSpyObj = function createPromiseSpyObj(baseName, methodNames) { 69 | var spyObj = jasmine.createSpyObj(baseName, methodNames); 70 | 71 | methodNames.forEach(function (methodName) { 72 | createPromise(spyObj[methodName]); 73 | }); 74 | 75 | return spyObj; 76 | }; 77 | }); 78 | 79 | beforeEach(module('ngJasmine')); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/authentication/TokenAuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.authentication; 2 | 3 | import au.com.example.constant.Constants; 4 | import au.com.example.persistence.dao.user.UserDAO; 5 | import au.com.example.service.user.model.SpringUserDetail; 6 | import au.com.example.service.user.model.UserDetail; 7 | import au.com.example.utils.AuthenticationUtils; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContextHolder; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | import org.springframework.stereotype.Service; 14 | 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import javax.xml.bind.DatatypeConverter; 18 | 19 | @Service(Constants.SERVICE_TOKEN_AUTH) 20 | public class TokenAuthenticationServiceImpl implements TokenAuthenticationService { 21 | 22 | private static final String AUTH_HEADER_NAME = "X-AUTH-TOKEN"; 23 | 24 | private final TokenHandler tokenHandler; 25 | 26 | @Autowired 27 | private UserDAO userDao; 28 | 29 | public TokenAuthenticationServiceImpl() { 30 | // TODO: parse this as a property 31 | tokenHandler = new TokenHandler(DatatypeConverter.parseBase64Binary("9SyECk96oDsTmXfogIfgdjhdsgvagHJLKNLvfdsfR8cbXTvoPjX+Pq/T/b1PqpHX0lYm0oCBjXWICA==")); 32 | } 33 | 34 | @Override 35 | public void addAuthentication(HttpServletResponse response, Authentication authentication) { 36 | final SpringUserDetail user = (SpringUserDetail)authentication.getPrincipal(); 37 | response.addHeader(AUTH_HEADER_NAME, tokenHandler.createTokenForUser(AuthenticationUtils.toUserDetail(user))); 38 | } 39 | 40 | @Override 41 | public Authentication getAuthentication(HttpServletRequest request) { 42 | final String token = request.getHeader(AUTH_HEADER_NAME); 43 | 44 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 45 | 46 | if(authentication == null) { 47 | if (token != null) { 48 | final UserDetail user = tokenHandler.parseUserFromToken(token); 49 | if (user != null) { 50 | UserDetails details = userDao.loadUser(user.getEmail()); 51 | authentication = new UsernamePasswordAuthenticationToken(details, null, details.getAuthorities()); 52 | } 53 | } 54 | } 55 | 56 | return authentication; 57 | } 58 | } -------------------------------------------------------------------------------- /client/src/main/webapp/html/partials/view/customer_search.html: -------------------------------------------------------------------------------- 1 |
2 |

Customer Search

3 |
4 | 5 |
6 | 17 |
18 | 19 |
20 | Filtering by: {{ search.lastName }} 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 |
29 |
{{customer.firstName}} {{customer.lastName}}
30 |
31 | 34 |
35 |
36 |
37 |
{{customer.id}}
38 | 39 | 40 |
41 | 56 |
57 |
58 |
-------------------------------------------------------------------------------- /api/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | angular_bootstrap_spring 5 | 6 | 7 | 8 | org.springframework.web.context.ContextLoaderListener 9 | 10 | 11 | 12 | 13 | 14 | org.springframework.security.web.session.HttpSessionEventPublisher 15 | 16 | 17 | 18 | 19 | contextClass 20 | 21 | org.springframework.web.context.support.AnnotationConfigWebApplicationContext 22 | 23 | 24 | 25 | 26 | contextConfigLocation 27 | 28 | au.com.example.spring.AppConfig 29 | au.com.example.spring.SecurityConfig 30 | au.com.example.spring.PersistenceConfig 31 | 32 | 33 | 34 | 35 | log4jConfigLocation 36 | classpath:log4j.xml 37 | 38 | 39 | 42 | 43 | characterEncodingFilter 44 | org.springframework.web.filter.CharacterEncodingFilter 45 | 46 | encoding 47 | UTF-8 48 | 49 | 50 | forceEncoding 51 | true 52 | 53 | 54 | 55 | 56 | characterEncodingFilter 57 | /* 58 | 59 | 60 | 63 | 64 | springSecurityFilterChain 65 | org.springframework.web.filter.DelegatingFilterProxy 66 | 67 | 68 | springSecurityFilterChain 69 | /* 70 | 71 | 72 | 75 | 76 | model 77 | org.springframework.web.servlet.DispatcherServlet 78 | 79 | contextConfigLocation 80 | * 81 | 82 | 1 83 | 84 | 85 | 86 | model 87 | / 88 | 89 | 90 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/customer/CustomerDaoImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.customer; 2 | 3 | import au.com.example.exception.UpdateDeleteException; 4 | import au.com.example.api.model.customer.Customer; 5 | import au.com.example.persistence.dao.base.BaseDao; 6 | import au.com.example.persistence.dao.customer.entity.CustomerEntity; 7 | import au.com.example.persistence.dao.customer.query.SelectCustomer; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.stereotype.Repository; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | 17 | @Repository 18 | public class CustomerDaoImpl extends BaseDao implements CustomerDao { 19 | 20 | private static Logger log = LoggerFactory.getLogger(CustomerDaoImpl.class); 21 | 22 | @Transactional(readOnly = true) 23 | @Override 24 | public List getCustomers() { 25 | List customers = new ArrayList(); 26 | 27 | Collection customerEntities = loadData(CustomerEntity.class, new SelectCustomer()); 28 | 29 | if (customerEntities == null || customerEntities.isEmpty()) { 30 | log.info("No customers found"); 31 | } 32 | else 33 | { 34 | for(CustomerEntity customerEntity : customerEntities) { 35 | customers.add(toCustomer(customerEntity)); 36 | } 37 | } 38 | 39 | return customers; 40 | } 41 | 42 | @Transactional 43 | @Override 44 | public boolean deleteCustomer(Long id) { 45 | boolean success = false; 46 | 47 | try { 48 | success = deleteSingleData(CustomerEntity.class, id); 49 | } 50 | catch(UpdateDeleteException e) { 51 | log.error("Error deleting customer with id: " + id); 52 | } 53 | 54 | return success; 55 | } 56 | 57 | @Transactional 58 | @Override 59 | public boolean saveCustomer(Customer customer) { 60 | boolean success = false; 61 | 62 | try { 63 | success = updateSingleData(toCustomerEntity(customer)); 64 | } 65 | catch(UpdateDeleteException e) { 66 | log.error("Error saving customer with id: " + customer.getId()); 67 | } 68 | 69 | return success; 70 | } 71 | 72 | // ======== Helpers ========= 73 | 74 | private Customer toCustomer(CustomerEntity customerEntity) { 75 | Customer customer = null; 76 | 77 | if(customerEntity != null) { 78 | customer = new Customer(customerEntity.getId(), customerEntity.getFirstName(), customerEntity.getLastName()); 79 | } 80 | 81 | return customer; 82 | } 83 | 84 | private CustomerEntity toCustomerEntity(Customer customer) { 85 | CustomerEntity entity = null; 86 | 87 | if(customer != null) { 88 | entity = new CustomerEntity(customer.getId(), customer.getFirstName(), customer.getLastName()); 89 | } 90 | 91 | return entity; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/src/main/webapp/js/custom/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('app').config([ '$routeProvider', '$httpProvider', '$locationProvider', 4 | function($routeProvider, $httpProvider, $locationProvider) { 5 | $locationProvider.html5Mode(true); 6 | 7 | var user = ['$rootScope', 'userService','storageService', 'storageConstant', function ($rootScope, userService, storageService, storageConstant) { 8 | var user = storageService.getSessionItem(storageConstant.USER); 9 | 10 | if(user === null || user === undefined) { 11 | userService.retrieve() 12 | .then(function (user) { 13 | storageService.setSessionItem(storageConstant.USER, user); 14 | 15 | $rootScope.user = user; 16 | }); 17 | } else { 18 | $rootScope.user = user; 19 | } 20 | }]; 21 | 22 | var clear = ['$rootScope', 'storageService', 'storageConstant', function ($rootScope, storageService, storageConstant) { 23 | storageService.removeSessionItem(storageConstant.USER); 24 | 25 | delete $rootScope.user; 26 | }]; 27 | 28 | // ======= router configuration ============= 29 | 30 | $routeProvider 31 | .when('/main', { 32 | title: 'Main', 33 | templateUrl: 'html/partials/view/main.html', 34 | resolve: { 35 | user: user 36 | } 37 | }) 38 | .when('/customer/search', { 39 | title: 'Customer Search', 40 | controller: 'CustomerController', 41 | templateUrl: 'html/partials/view/customer_search.html', 42 | resolve: { 43 | user: user 44 | } 45 | }) 46 | .when('/login', { 47 | title: 'Login', 48 | templateUrl: 'html/partials/view/login.html', 49 | controller: 'LoginController', 50 | resolve: { 51 | clear: clear 52 | } 53 | }) 54 | .otherwise({ redirectTo : "/main"}); 55 | 56 | // ======== http configuration =============== 57 | 58 | $httpProvider.interceptors.push(function ($q, $location, messageService, storageService, storageConstant) { 59 | return { 60 | 'request': function(request) { 61 | messageService.clearError(); 62 | 63 | var authToken = storageService.getSessionItem(storageConstant.AUTH_TOKEN); 64 | 65 | if (authToken) { 66 | request.headers['X-AUTH-TOKEN'] = authToken; 67 | } 68 | 69 | return request; 70 | }, 71 | 'response': function (response) { 72 | return response; 73 | }, 74 | 'responseError': function (rejection) { 75 | switch (rejection.status) { 76 | case 400: 77 | case 401: 78 | case 403: 79 | case 500: { 80 | break; 81 | } 82 | default : { 83 | messageService.error("UNKNOWN_ERROR", "An error has occurred, please try again."); 84 | 85 | break; 86 | } 87 | } 88 | 89 | return $q.reject(rejection); 90 | } 91 | }; 92 | }); 93 | }]); -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/service/authentication/TokenHandler.java: -------------------------------------------------------------------------------- 1 | package au.com.example.service.authentication; 2 | 3 | import au.com.example.service.user.model.UserDetail; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import javax.crypto.Mac; 8 | import javax.crypto.spec.SecretKeySpec; 9 | import javax.xml.bind.DatatypeConverter; 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.security.InvalidKeyException; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.util.Arrays; 15 | 16 | public final class TokenHandler { 17 | 18 | private static final String HMAC_ALGO = "HmacSHA256"; 19 | private static final String SEPARATOR = "."; 20 | private static final String SEPARATOR_SPLITTER = "\\."; 21 | 22 | private final Mac hmac; 23 | 24 | public TokenHandler(byte[] secretKey) { 25 | try { 26 | hmac = Mac.getInstance(HMAC_ALGO); 27 | hmac.init(new SecretKeySpec(secretKey, HMAC_ALGO)); 28 | } catch (NoSuchAlgorithmException | InvalidKeyException e) { 29 | throw new IllegalStateException("failed to initialize HMAC: " + e.getMessage(), e); 30 | } 31 | } 32 | 33 | public UserDetail parseUserFromToken(String token) { 34 | final String[] parts = token.split(SEPARATOR_SPLITTER); 35 | if (parts.length == 2 && parts[0].length() > 0 && parts[1].length() > 0) { 36 | try { 37 | final byte[] userBytes = fromBase64(parts[0]); 38 | final byte[] hash = fromBase64(parts[1]); 39 | 40 | boolean validHash = Arrays.equals(createHmac(userBytes), hash); 41 | if (validHash) { 42 | return fromJSON(userBytes); 43 | } 44 | } catch (IllegalArgumentException e) { 45 | //log tempering attempt here 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | public String createTokenForUser(UserDetail user) { 52 | byte[] userBytes = toJSON(user); 53 | byte[] hash = createHmac(userBytes); 54 | final StringBuilder sb = new StringBuilder(170); 55 | sb.append(toBase64(userBytes)); 56 | sb.append(SEPARATOR); 57 | sb.append(toBase64(hash)); 58 | return sb.toString(); 59 | } 60 | 61 | private UserDetail fromJSON(final byte[] userBytes) { 62 | try { 63 | return new ObjectMapper().readValue(new ByteArrayInputStream(userBytes), UserDetail.class); 64 | } catch (IOException e) { 65 | throw new IllegalStateException(e); 66 | } 67 | } 68 | 69 | private byte[] toJSON(UserDetail user) { 70 | try { 71 | return new ObjectMapper().writeValueAsBytes(user); 72 | } catch (JsonProcessingException e) { 73 | throw new IllegalStateException(e); 74 | } 75 | } 76 | 77 | private String toBase64(byte[] content) { 78 | return DatatypeConverter.printBase64Binary(content); 79 | } 80 | 81 | private byte[] fromBase64(String content) { 82 | return DatatypeConverter.parseBase64Binary(content); 83 | } 84 | 85 | // synchronized to guard internal hmac object 86 | private synchronized byte[] createHmac(byte[] content) { 87 | return hmac.doFinal(content); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular_bootstrap_spring 2 | ======================== 3 | [![Build Status](https://travis-ci.org/Rob-Leggett/angular_bootstrap_spring.svg?branch=master)](https://travis-ci.org/Rob-Leggett/angular_bootstrap_spring) 4 | 5 | Angular JS with Bootstrap and Spring 4 and Spring Security. 6 | 7 | This example is an angular js single page application (SPA) with bootstrap for the widgets and styling. 8 | 9 | The application has been broken into two modules API and CLIENT, both are built separately and both are deployed separately. 10 | 11 | The API can run on any web server, but it has been tested against Tomcat 8, the server required http DELETE and PUT, so ensure your web server can support those http methods. 12 | 13 | The CLIENT currently is run via gulp, for a production release you could extract the .zip artefact and run the static client via Apache. 14 | 15 | Ensure that you proxy the API so that you have the same domain otherwise you will experience CORS related issues. (deployed artefacts only) 16 | 17 | ### Gulp: 18 | Used as the build tool for the client, this has been written using ES6 19 | 20 | ### Spring 4: 21 | Used to create RESTful controller interfaces which in turn gets called through ajax requests. 22 | 23 | ### Spring Security 4: 24 | Used for a stateless api that allows authentication via basic authentication or token authentication. 25 | 26 | Upon authentication a token is attached to the header response which can in turn be used for sequential requests to be authenticated against. 27 | 28 | When an authentication fails a 401 will always be returned. 29 | 30 | ### Login Details as per database inject.sql: 31 | **Username =** user@tester.com.au 32 | 33 | **Password =** password 34 | 35 | Testing 36 | ==================== 37 | Simply run on the parent pom to have node and modules auto install and execute all tests. **(REQUIRED FOR FIRST RUN)** 38 | 39 | Ensure you have Maven 3.2.0+ 40 | 41 | **mvn clean install** 42 | 43 | To run specific profiles please run mvn clean install and simple pass the profile you wish to execute. 44 | 45 | This will execute Java and Jasmine tests that will test both java classes and angular js files. 46 | 47 | You can also run jasmine only tests if you wish via the front end: 48 | 49 | **http://localhost:4444/test** 50 | 51 | Running 52 | ==================== 53 | 54 | ### Recommendations: 55 | 56 | Use IntelliJ 16+ to run the application. 57 | 58 | ### Run the API via Tomcat 8: 59 | 60 | Deploy exploded artefact to Tomcat 8 and ensure the root context is set to API. 61 | 62 | ### Run the CLIENT via gulp.babel.js: 63 | 64 | Where PATH is the directory to your checked out project. 65 | 66 | **Gulp File:** PATH\angular_bootstrap_spring\client\gulpfile.babel.js 67 | 68 | **Tasks:** run 69 | 70 | **Node Interpreter:** PATH\angular_bootstrap_spring\client\node\node.exe 71 | 72 | **Gulp package:** PATH\angular_bootstrap_spring\client\node_modules\gulp 73 | 74 | ### The application is set to run on 75 | 76 | **http://localhost:4444** 77 | 78 | Donations 79 | ==================== 80 | 81 | ### How you can help? 82 | 83 | Any donations received will be able to assist me provide more blog entries and examples via GitHub, any contributions provided is greatly appreciated. 84 | 85 | Thanks for your support. 86 | 87 | [![paypal](https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=EV2ZLZBABFJ34&lc=AU&item_name=Research%20%26%20Development¤cy_code=AUD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) 88 | -------------------------------------------------------------------------------- /api/src/test/java/au/com/example/user/UserControllerEndpointUnitTest.java: -------------------------------------------------------------------------------- 1 | package au.com.example.user; 2 | 3 | import au.com.example.spring.AppConfig; 4 | import au.com.example.spring.PersistenceConfig; 5 | import au.com.example.spring.SecurityConfig; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.security.crypto.codec.Base64; 12 | import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.TestExecutionListeners; 15 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 16 | import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; 17 | import org.springframework.test.context.support.DirtiesContextTestExecutionListener; 18 | import org.springframework.test.context.transaction.TransactionalTestExecutionListener; 19 | import org.springframework.test.context.web.ServletTestExecutionListener; 20 | import org.springframework.test.context.web.WebAppConfiguration; 21 | import org.springframework.test.web.servlet.MockMvc; 22 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 23 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 24 | import org.springframework.web.context.WebApplicationContext; 25 | 26 | import javax.servlet.Filter; 27 | 28 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext; 29 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 30 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 31 | 32 | @RunWith(SpringJUnit4ClassRunner.class) 33 | @ContextConfiguration(classes = {AppConfig.class, SecurityConfig.class, PersistenceConfig.class}) 34 | @WebAppConfiguration 35 | @TestExecutionListeners(listeners = {ServletTestExecutionListener.class, 36 | DependencyInjectionTestExecutionListener.class, 37 | DirtiesContextTestExecutionListener.class, 38 | TransactionalTestExecutionListener.class, 39 | WithSecurityContextTestExecutionListener.class}) 40 | public class UserControllerEndpointUnitTest { 41 | 42 | @Autowired 43 | private WebApplicationContext wac; 44 | 45 | @Autowired 46 | private Filter springSecurityFilterChain; 47 | 48 | private MockMvc mockMvc; 49 | 50 | @Before 51 | public void setup() { 52 | mockMvc = MockMvcBuilders 53 | .webAppContextSetup(wac) 54 | .addFilters(springSecurityFilterChain) 55 | .build(); 56 | } 57 | 58 | @Test 59 | public void shouldGetAuthenticatedUser() throws Exception { 60 | String basicDigestHeaderValue = "Basic " + new String(Base64.encode(("test-user-db@tester.com.au:password").getBytes())); 61 | 62 | mockMvc.perform( 63 | get("/user") 64 | .with(testSecurityContext()) 65 | .accept(MediaType.APPLICATION_JSON_VALUE) 66 | .header("Authorization", basicDigestHeaderValue)) 67 | .andExpect(MockMvcResultMatchers.status().isOk()) 68 | .andExpect(jsonPath("$.email").value("test-user-db@tester.com.au")); 69 | } 70 | 71 | @Test 72 | public void shouldNotGetAuthenticatedUserWithNoUserDetails() throws Exception { 73 | mockMvc.perform( 74 | get("/user") 75 | .accept(MediaType.APPLICATION_JSON)) 76 | .andExpect(MockMvcResultMatchers.status().isUnauthorized()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /api/src/test/java/au/com/example/user/UserDetailsManagerUnitTest.java: -------------------------------------------------------------------------------- 1 | package au.com.example.user; 2 | 3 | import au.com.example.persistence.dao.user.UserDAO; 4 | import au.com.example.persistence.exceptions.CreateUserException; 5 | import au.com.example.service.user.model.SpringUserDetail; 6 | import au.com.example.spring.PersistenceConfig; 7 | import au.com.example.spring.SecurityConfig; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.MockitoAnnotations; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.security.core.GrantedAuthority; 16 | import org.springframework.security.core.userdetails.UserDetails; 17 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 18 | import org.springframework.security.provisioning.UserDetailsManager; 19 | import org.springframework.test.context.ContextConfiguration; 20 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 21 | 22 | import java.util.ArrayList; 23 | 24 | import static org.junit.Assert.assertEquals; 25 | import static org.junit.Assert.assertNotNull; 26 | import static org.junit.Assert.assertNull; 27 | import static org.mockito.Matchers.any; 28 | import static org.mockito.Matchers.anyString; 29 | import static org.mockito.Mockito.doNothing; 30 | import static org.mockito.Mockito.doThrow; 31 | import static org.mockito.Mockito.when; 32 | 33 | @RunWith(SpringJUnit4ClassRunner.class) 34 | @ContextConfiguration(classes = { SecurityConfig.class, PersistenceConfig.class }) 35 | public class UserDetailsManagerUnitTest { 36 | 37 | @Autowired 38 | @InjectMocks 39 | private UserDetailsManager userDetailsManager; 40 | 41 | @Mock 42 | private UserDAO mockUserDAO; 43 | 44 | @Mock 45 | private SpringUserDetail mockUserDetail; 46 | 47 | @Before 48 | public void setup() { 49 | MockitoAnnotations.initMocks(this); 50 | } 51 | 52 | @Test 53 | public void shouldLoadUserWithEmailValid() { 54 | when(mockUserDAO.loadUser(anyString())).thenReturn(mockUserDetail); 55 | 56 | UserDetails user = userDetailsManager.loadUserByUsername("test-user-valid@tester.com.au"); 57 | 58 | assertNotNull(user); 59 | assertEquals(mockUserDetail.getUsername(), user.getUsername()); 60 | } 61 | 62 | @Test(expected = UsernameNotFoundException.class) 63 | public void shouldNotLoadUserWithUsernameNotFoundExceptionThrown() { 64 | when(mockUserDAO.loadUser(anyString())).thenThrow(UsernameNotFoundException.class); 65 | 66 | UserDetails user = userDetailsManager.loadUserByUsername("test-user-not-found@tester.com.au"); 67 | 68 | assertNull(user); 69 | } 70 | 71 | @Test 72 | public void shouldCreateUserWithUserDetailsValid() { 73 | doNothing().when(mockUserDAO).createUser(any(UserDetails.class)); 74 | 75 | userDetailsManager.createUser( 76 | new SpringUserDetail("test-user-valid@tester.com.au", "password", "test", "user", "test user", 77 | new ArrayList(), true, true, true, true)); 78 | } 79 | 80 | @Test(expected = CreateUserException.class) 81 | public void shouldNotCreateUserWithCreateUserExceptionThrown() { 82 | doThrow(CreateUserException.class).when(mockUserDAO).createUser(any(UserDetails.class)); 83 | 84 | userDetailsManager.createUser( 85 | new SpringUserDetail("test-user-valid@tester.com.au", "password", "test", "user", "test user", 86 | new ArrayList(), true, true, true, true)); 87 | } 88 | 89 | @Test(expected = IllegalArgumentException.class) 90 | public void shouldNotCreateUserWithIllegalArgumentExceptionThrown() { 91 | doThrow(IllegalArgumentException.class).when(mockUserDAO).createUser(any(UserDetails.class)); 92 | 93 | userDetailsManager.createUser(new SpringUserDetail()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /api/src/main/webapp/WEB-INF/resources/properties/clientMessages.properties: -------------------------------------------------------------------------------- 1 | # Errors 2 | error.login.failed = Your login attempt was not successful, try again. 3 | error.access.denied = You do not have access to this page. 4 | 5 | # Titles 6 | title.view.summary = About 7 | title.view.activity = Activity 8 | title.view.consultant = Consultant 9 | title.view.access.denied = Access Denied 10 | title.view.login = Login 11 | 12 | # Dialog Titles 13 | title.dialog.learning.focus.header = Learning Focus 14 | title.dialog.learning.goal.header = Learning Goal 15 | title.dialog.learning.checklist.header = Learning Checklist 16 | 17 | # Menu 18 | menu.summary = About 19 | menu.activities = My Activities 20 | menu.search = Search 21 | menu.consultant = Consultant 22 | 23 | # Messages 24 | msg.access.denied = You cannot perform that action 25 | 26 | # Labels 27 | label.actions = Actions 28 | label.banner.title = Learning & Innovation Center 29 | label.login.header = Login 30 | label.username = Username 31 | label.password = Password 32 | label.login.header = Login 33 | label.summary.header = About 34 | label.summary.instructions = This portal has been designed & developed by consultants for consultants.

It has been designed to provide an easy to use mechanism for consultants to set & keep track of learning objectives whilst we are in between client project work.

Whilst on the LAIC - it is expected that we use this time to develop our skills - be it learning a new development language, honing a skill or acquainting ourselves to a new project management or testing methodology.

You can use this portal to set your own learning objectives, set learning goals and track your development progress - It is a time to improve our skills and showcase these improvements to everyone.

There is also a checklist to help you keep track of your compliance training requirements as outlined by our parent company - UXC Limited.

Setting development goals and measuring the progress of development allows you to show your Managing Consultant or People Leader and the P&C team the self-development you have done in preparation for your next Performance Review.

Please ensure you keep up your development progress up to date in this system whilst assigned to the LAIC. 35 | label.activities.header = My Activities 36 | label.activities.header.user = {0} Activities 37 | label.activity.checklist.item = Item 38 | label.activity.checklist.comment = Comment 39 | label.activity.checklist.percentage = % Complete 40 | label.activity.from.date = From 41 | label.activity.to.date = To 42 | label.activity.description = Description 43 | label.activity.status = Status 44 | label.activity.complete = Complete 45 | label.activity.commenced = Commenced 46 | label.activity.dueby = Due By 47 | label.activity.duedate = Due Date 48 | label.activity.percentage = % Complete 49 | label.activity.comment = Comment 50 | label.activity.learning.focuses = Learning Focuses 51 | label.activity.learning.history = Learning History 52 | label.activity.checklist = L&I Checklist 53 | label.activity.learning.goal = Learning Goals 54 | label.activity.title = Title 55 | label.consultant.header = Consultant 56 | label.consultants = Consultants 57 | label.consultant.details = Details 58 | label.consultant.employee.id = Id 59 | label.consultant.display.name = Name 60 | label.consultant.title = Title 61 | label.consultant.mail = Email 62 | label.consultant.mobile = Mobile 63 | label.reason = Reason {0} 64 | label.login = Login 65 | label.logout = Logout 66 | label.welcome = Welcome {0} 67 | label.no.data = No Data 68 | 69 | # Enum 70 | enumeration.IN_PROGRESS = In Progress 71 | enumeration.ON_HOLD = On Hold 72 | enumeration.COMPLETED = Completed 73 | enumeration.CANCELLED = Cancelled 74 | 75 | # Buttons 76 | button.add = Add 77 | button.delete = Delete 78 | button.login = Login 79 | button.edit = Edit 80 | button.submit = Submit 81 | button.view.activities = View Activities 82 | 83 | # Dates 84 | date.time.long.format = dd/MM/yyyy HH:mm 85 | date.time.short.format = dd/MM/yy HH:mm 86 | date.long.format = dd/MM/yyyy 87 | date.short.format = dd-MMM 88 | date.day.long.format = EEEE, dd-MMM-yyyy 89 | date.day.short.format = EEE, dd-MMM 90 | time.short.format = HH:mm -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/spring/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package au.com.example.spring; 2 | 3 | import au.com.example.persistence.provider.CustomDaoAuthenticationProvider; 4 | import au.com.example.service.user.UserService; 5 | import au.com.example.spring.security.StatelessAuthenticationFilter; 6 | import au.com.example.spring.security.StatelessTokenAuthenticationFilter; 7 | import au.com.example.spring.security.UnauthorisedEntryPoint; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.security.authentication.AuthenticationManager; 13 | import org.springframework.security.authentication.AuthenticationProvider; 14 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 15 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 16 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 17 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 18 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 19 | import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; 20 | import org.springframework.security.config.http.SessionCreationPolicy; 21 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 22 | import org.springframework.security.crypto.password.PasswordEncoder; 23 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 24 | 25 | @Configuration 26 | @EnableWebSecurity 27 | @EnableGlobalMethodSecurity(prePostEnabled = true) 28 | @ComponentScan(basePackages = { "au.com.example.service", "au.com.example.spring.security" }) 29 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 30 | 31 | private static final String LOGOUT_URL = "/auth/logout"; 32 | private static final String LOGOUT_SUCCESS_URL = "/auth/logout/validate?status=success"; 33 | 34 | @Autowired 35 | private UnauthorisedEntryPoint unauthorisedEntryPoint; 36 | 37 | @Autowired 38 | private StatelessAuthenticationFilter statelessAuthenticationFilter; 39 | 40 | @Autowired 41 | private StatelessTokenAuthenticationFilter statelessTokenAuthenticationFilter; 42 | 43 | @Autowired 44 | private UserService userService; 45 | 46 | // ========= Overrides =========== 47 | 48 | @Override 49 | protected void configure(HttpSecurity http) throws Exception { 50 | http 51 | .csrf().disable() 52 | .httpBasic() 53 | .authenticationEntryPoint(unauthorisedEntryPoint) 54 | .and() 55 | .authorizeRequests() 56 | .antMatchers("/**").permitAll() 57 | .and() 58 | .sessionManagement() 59 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 60 | .and() 61 | .logout() 62 | .logoutUrl(LOGOUT_URL) 63 | .logoutSuccessUrl(LOGOUT_SUCCESS_URL) 64 | .invalidateHttpSession(true) 65 | .deleteCookies("JSESSIONID") 66 | .and() 67 | .addFilterBefore(statelessAuthenticationFilter, BasicAuthenticationFilter.class) 68 | .addFilterAfter(statelessTokenAuthenticationFilter, BasicAuthenticationFilter.class); 69 | } 70 | 71 | @Override 72 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 73 | auth.authenticationProvider(getDaoAuthenticationProvider()); 74 | } 75 | 76 | // =========== Beans ============ 77 | 78 | @Override 79 | @Bean 80 | public AuthenticationManager authenticationManagerBean() throws Exception { 81 | return super.authenticationManagerBean(); 82 | } 83 | 84 | @Bean(name = "passwordEncoder") 85 | public PasswordEncoder getPasswordEncoder() { 86 | return new BCryptPasswordEncoder(); 87 | } 88 | 89 | // =========== Helpers ============ 90 | 91 | private AuthenticationProvider getDaoAuthenticationProvider() { 92 | CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(); 93 | authenticationProvider.setUserDetailsService(userService); 94 | authenticationProvider.setPasswordEncoder(getPasswordEncoder()); 95 | 96 | return authenticationProvider; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /client/src/test/webapp/specs/jasmine/console.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2014 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | function getJasmineRequireObj() { 24 | if (typeof module !== 'undefined' && module.exports) { 25 | return exports; 26 | } else { 27 | window.jasmineRequire = window.jasmineRequire || {}; 28 | return window.jasmineRequire; 29 | } 30 | } 31 | 32 | getJasmineRequireObj().console = function(jRequire, j$) { 33 | j$.ConsoleReporter = jRequire.ConsoleReporter(); 34 | }; 35 | 36 | getJasmineRequireObj().ConsoleReporter = function() { 37 | 38 | var noopTimer = { 39 | start: function(){}, 40 | elapsed: function(){ return 0; } 41 | }; 42 | 43 | function ConsoleReporter(options) { 44 | var print = options.print, 45 | showColors = options.showColors || false, 46 | onComplete = options.onComplete || function() {}, 47 | timer = options.timer || noopTimer, 48 | specCount, 49 | failureCount, 50 | failedSpecs = [], 51 | pendingCount, 52 | ansi = { 53 | green: '\x1B[32m', 54 | red: '\x1B[31m', 55 | yellow: '\x1B[33m', 56 | none: '\x1B[0m' 57 | }; 58 | 59 | this.jasmineStarted = function() { 60 | specCount = 0; 61 | failureCount = 0; 62 | pendingCount = 0; 63 | print('Started'); 64 | printNewline(); 65 | timer.start(); 66 | }; 67 | 68 | this.jasmineDone = function() { 69 | printNewline(); 70 | for (var i = 0; i < failedSpecs.length; i++) { 71 | specFailureDetails(failedSpecs[i]); 72 | } 73 | 74 | if(specCount > 0) { 75 | printNewline(); 76 | 77 | var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' + 78 | failureCount + ' ' + plural('failure', failureCount); 79 | 80 | if (pendingCount) { 81 | specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount); 82 | } 83 | 84 | print(specCounts); 85 | } else { 86 | print('No specs found'); 87 | } 88 | 89 | printNewline(); 90 | var seconds = timer.elapsed() / 1000; 91 | print('Finished in ' + seconds + ' ' + plural('second', seconds)); 92 | 93 | printNewline(); 94 | 95 | onComplete(failureCount === 0); 96 | }; 97 | 98 | this.specDone = function(result) { 99 | specCount++; 100 | 101 | if (result.status == 'pending') { 102 | pendingCount++; 103 | print(colored('yellow', '*')); 104 | return; 105 | } 106 | 107 | if (result.status == 'passed') { 108 | print(colored('green', '.')); 109 | return; 110 | } 111 | 112 | if (result.status == 'failed') { 113 | failureCount++; 114 | failedSpecs.push(result); 115 | print(colored('red', 'F')); 116 | } 117 | }; 118 | 119 | return this; 120 | 121 | function printNewline() { 122 | print('\n'); 123 | } 124 | 125 | function colored(color, str) { 126 | return showColors ? (ansi[color] + str + ansi.none) : str; 127 | } 128 | 129 | function plural(str, count) { 130 | return count == 1 ? str : str + 's'; 131 | } 132 | 133 | function repeat(thing, times) { 134 | var arr = []; 135 | for (var i = 0; i < times; i++) { 136 | arr.push(thing); 137 | } 138 | return arr; 139 | } 140 | 141 | function indent(str, spaces) { 142 | var lines = (str || '').split('\n'); 143 | var newArr = []; 144 | for (var i = 0; i < lines.length; i++) { 145 | newArr.push(repeat(' ', spaces).join('') + lines[i]); 146 | } 147 | return newArr.join('\n'); 148 | } 149 | 150 | function specFailureDetails(result) { 151 | printNewline(); 152 | print(result.fullName); 153 | 154 | for (var i = 0; i < result.failedExpectations.length; i++) { 155 | var failedExpectation = result.failedExpectations[i]; 156 | printNewline(); 157 | print(indent(failedExpectation.stack, 2)); 158 | } 159 | 160 | printNewline(); 161 | } 162 | } 163 | 164 | return ConsoleReporter; 165 | }; 166 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user.entity; 2 | 3 | import au.com.example.constant.Constants; 4 | import au.com.example.utils.CopyUtils; 5 | 6 | import javax.persistence.CascadeType; 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.FetchType; 10 | import javax.persistence.Id; 11 | import javax.persistence.JoinColumn; 12 | import javax.persistence.JoinTable; 13 | import javax.persistence.OneToMany; 14 | import javax.persistence.Table; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | @Entity 19 | @Table(name = Constants.TABLE_USER) 20 | public class UserEntity implements Cloneable { 21 | private String email; 22 | private String password; 23 | private String firstName; 24 | private String lastName; 25 | private String alias; 26 | private List memberships; 27 | private boolean enabled; 28 | private boolean accountNonExpired; 29 | private boolean credentialsNonExpired; 30 | private boolean accountNonLocked; 31 | 32 | public UserEntity() { 33 | this(null, null, null, null, null, null, false, false, false, false); 34 | } 35 | 36 | public UserEntity( 37 | String email, 38 | String password, 39 | String firstName, 40 | String lastName, 41 | String alias, 42 | List memberships, 43 | boolean enabled, 44 | boolean accountNonExpired, 45 | boolean credentialsNonExpired, 46 | boolean accountNonLocked) { 47 | 48 | this.email = email; 49 | this.password = password; 50 | this.firstName = firstName; 51 | this.lastName = lastName; 52 | this.alias = alias; 53 | this.memberships = memberships; 54 | this.enabled = enabled; 55 | this.accountNonExpired = accountNonExpired; 56 | this.credentialsNonExpired = credentialsNonExpired; 57 | this.accountNonLocked = accountNonLocked; 58 | } 59 | 60 | @Id 61 | @Column(name = "email") 62 | public String getEmail() { 63 | return email; 64 | } 65 | 66 | public void setEmail(String email) { 67 | this.email = email; 68 | } 69 | 70 | @Column(name = "password") 71 | public String getPassword() { 72 | return password; 73 | } 74 | 75 | public void setPassword(String password) { 76 | this.password = password; 77 | } 78 | 79 | @Column(name = "first_name") 80 | public String getFirstName() { 81 | return firstName; 82 | } 83 | 84 | public void setFirstName(String firstName) { 85 | this.firstName = firstName; 86 | } 87 | 88 | @Column(name = "last_name") 89 | public String getLastName() { 90 | return lastName; 91 | } 92 | 93 | public void setLastName(String lastName) { 94 | this.lastName = lastName; 95 | } 96 | 97 | @Column(name = "alias") 98 | public String getAlias() { 99 | return alias; 100 | } 101 | 102 | public void setAlias(String alias) { 103 | this.alias = alias; 104 | } 105 | 106 | @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) 107 | @JoinTable(name = Constants.TABLE_USER_MEMBERSHIP, joinColumns = { @JoinColumn(name = "email") }, 108 | inverseJoinColumns = { @JoinColumn(name = "membership_id") }) 109 | public List getMemberships() { 110 | return memberships; 111 | } 112 | 113 | public void setMemberships(List memberships) { 114 | this.memberships = memberships; 115 | } 116 | 117 | @Column(name = "enabled") 118 | public boolean isEnabled() { 119 | return enabled; 120 | } 121 | 122 | public void setEnabled(boolean enabled) { 123 | this.enabled = enabled; 124 | } 125 | 126 | @Column(name = "account_non_expired") 127 | public boolean isAccountNonExpired() { 128 | return accountNonExpired; 129 | } 130 | 131 | public void setAccountNonExpired(boolean accountNonExpired) { 132 | this.accountNonExpired = accountNonExpired; 133 | } 134 | 135 | @Column(name = "credentials_non_expired") 136 | public boolean isCredentialsNonExpired() { 137 | return credentialsNonExpired; 138 | } 139 | 140 | public void setCredentialsNonExpired(boolean credentialsNonExpired) { 141 | this.credentialsNonExpired = credentialsNonExpired; 142 | } 143 | 144 | @Column(name = "account_non_locked") 145 | public boolean isAccountNonLocked() { 146 | return accountNonLocked; 147 | } 148 | 149 | public void setAccountNonLocked(boolean accountNonLocked) { 150 | this.accountNonLocked = accountNonLocked; 151 | } 152 | 153 | // === Cloneable implementation 154 | 155 | @SuppressWarnings("unchecked") 156 | @Override 157 | public UserEntity clone() { 158 | return new UserEntity( 159 | email, 160 | password, 161 | firstName, 162 | lastName, 163 | alias, 164 | CopyUtils.cloneCollection(ArrayList.class, memberships), 165 | enabled, 166 | accountNonExpired, 167 | credentialsNonExpired, 168 | accountNonLocked); 169 | } 170 | 171 | // === Overrides 172 | 173 | @Override public String toString() { 174 | return "UserEntity{" + 175 | "email='" + email + '\'' + 176 | ", password='" + password + '\'' + 177 | ", firstName='" + firstName + '\'' + 178 | ", lastName='" + lastName + '\'' + 179 | ", alias='" + alias + '\'' + 180 | ", memberships=" + memberships + 181 | ", enabled=" + enabled + 182 | ", accountNonExpired=" + accountNonExpired + 183 | ", credentialsNonExpired=" + credentialsNonExpired + 184 | ", accountNonLocked=" + accountNonLocked + 185 | '}'; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/base/BaseDao.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.base; 2 | 3 | import au.com.example.exception.UpdateDeleteException; 4 | import au.com.example.persistence.dao.base.query.QueryParameter; 5 | import au.com.example.persistence.dao.base.query.QueryString; 6 | import au.com.example.utils.CopyUtils; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import javax.persistence.EntityManager; 11 | import javax.persistence.EntityManagerFactory; 12 | import javax.persistence.EntityTransaction; 13 | import javax.persistence.NoResultException; 14 | import javax.persistence.PersistenceUnit; 15 | import javax.persistence.Query; 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.List; 19 | 20 | public class BaseDao { 21 | private static Logger log = LoggerFactory.getLogger(BaseDao.class); 22 | 23 | @PersistenceUnit 24 | private EntityManagerFactory emf; 25 | 26 | // === Getters & Setters === 27 | 28 | protected EntityManagerFactory getEmf() { 29 | return emf; 30 | } 31 | 32 | // === Methods === 33 | 34 | protected T loadDataSingle(Class oType, QueryString queryString) { 35 | EntityManager entityManager = emf.createEntityManager(); 36 | 37 | T data = null; 38 | 39 | try { 40 | Query query = entityManager.createQuery(queryString.getStatement()); 41 | 42 | for (QueryParameter parameter : queryString.getParameters()) { 43 | query.setParameter(parameter.getName(), parameter.getValue()); 44 | } 45 | 46 | T result = (T) query.getSingleResult(); 47 | 48 | if (result != null) { 49 | data = CopyUtils.clone(result); 50 | } 51 | } catch (NoResultException nre) { 52 | log.warn("No results found: " + nre.getMessage()); 53 | } finally { 54 | entityManager.close(); 55 | } 56 | 57 | return data; 58 | } 59 | 60 | @SuppressWarnings("unchecked") 61 | protected Collection loadData(Class oType, QueryString queryString) { 62 | EntityManager em = emf.createEntityManager(); 63 | 64 | try { 65 | Query query = em.createQuery(queryString.getStatement()); 66 | 67 | for (QueryParameter parameter : queryString.getParameters()) { 68 | query.setParameter(parameter.getName(), parameter.getValue()); 69 | } 70 | 71 | List data = new ArrayList(); 72 | 73 | Collection resultList = query.getResultList(); 74 | if (resultList != null) { 75 | for (T element : resultList) { 76 | // clone (lazy load collections and detach objects) 77 | data.add(CopyUtils.clone(element)); 78 | } 79 | } 80 | 81 | return data; 82 | } finally { 83 | em.close(); 84 | } 85 | } 86 | 87 | protected boolean deleteSingleData(Class oType, Long id) throws UpdateDeleteException { 88 | EntityManager entityManager = getEmf().createEntityManager(); 89 | 90 | try { 91 | EntityTransaction tx = null; 92 | 93 | try { 94 | tx = entityManager.getTransaction(); 95 | 96 | tx.begin(); 97 | 98 | entityManager.remove(entityManager.find(oType, id)); 99 | 100 | tx.commit(); 101 | } 102 | catch (Exception e) { 103 | log.error("Exception during deleting " + id + ": " + e.getMessage()); 104 | 105 | throw new UpdateDeleteException("Unable to delete data " + id, e); 106 | } 107 | finally { 108 | if (tx != null && tx.isActive()) { 109 | tx.rollback(); 110 | } 111 | } 112 | } 113 | finally { 114 | entityManager.close(); 115 | } 116 | 117 | return true; 118 | } 119 | 120 | protected boolean updateSingleData(Object entity) throws UpdateDeleteException { 121 | EntityManager entityManager = getEmf().createEntityManager(); 122 | 123 | try { 124 | EntityTransaction tx = null; 125 | 126 | try { 127 | tx = entityManager.getTransaction(); 128 | 129 | tx.begin(); 130 | 131 | entityManager.merge(entity); 132 | 133 | tx.commit(); 134 | } 135 | catch (Exception e) { 136 | log.error("Exception during merge: " + e.getMessage()); 137 | 138 | throw new UpdateDeleteException("Unable to merge data", e); 139 | } 140 | finally { 141 | if (tx != null && tx.isActive()) { 142 | tx.rollback(); 143 | } 144 | } 145 | } 146 | finally { 147 | entityManager.close(); 148 | } 149 | 150 | return true; 151 | } 152 | 153 | protected int updateDeleteDataSingle(QueryString queryString) throws UpdateDeleteException { 154 | EntityManager entityManager = emf.createEntityManager(); 155 | 156 | int rowsModified = 0; 157 | 158 | try { 159 | EntityTransaction tx = null; 160 | 161 | try { 162 | tx = entityManager.getTransaction(); 163 | 164 | tx.begin(); 165 | 166 | Query query = entityManager.createQuery(queryString.getStatement()); 167 | 168 | for (QueryParameter parameter : queryString.getParameters()) { 169 | query.setParameter(parameter.getName(), parameter.getValue()); 170 | } 171 | 172 | rowsModified = query.executeUpdate(); 173 | 174 | tx.commit(); 175 | } catch (Exception e) { 176 | log.error("Exception during update or delete query: " + e.getMessage()); 177 | 178 | throw new UpdateDeleteException(e.getMessage()); 179 | } finally { 180 | if (tx != null && tx.isActive()) { 181 | tx.rollback(); 182 | } 183 | } 184 | } finally { 185 | entityManager.close(); 186 | } 187 | 188 | return rowsModified; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /client/src/test/webapp/specs/jasmine/boot.js: -------------------------------------------------------------------------------- 1 | /** 2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. 3 | 4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. 5 | 6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. 7 | 8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem 9 | */ 10 | 11 | (function() { 12 | 13 | /** 14 | * ## Require & Instantiate 15 | * 16 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. 17 | */ 18 | window.jasmine = jasmineRequire.core(jasmineRequire); 19 | 20 | /** 21 | * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. 22 | */ 23 | jasmineRequire.html(jasmine); 24 | 25 | /** 26 | * Create the Jasmine environment. This is used to run all specs in a project. 27 | */ 28 | var env = jasmine.getEnv(); 29 | 30 | /** 31 | * ## The Global Interface 32 | * 33 | * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. 34 | */ 35 | var jasmineInterface = { 36 | describe: function(description, specDefinitions) { 37 | return env.describe(description, specDefinitions); 38 | }, 39 | 40 | xdescribe: function(description, specDefinitions) { 41 | return env.xdescribe(description, specDefinitions); 42 | }, 43 | 44 | it: function(desc, func) { 45 | return env.it(desc, func); 46 | }, 47 | 48 | xit: function(desc, func) { 49 | return env.xit(desc, func); 50 | }, 51 | 52 | beforeEach: function(beforeEachFunction) { 53 | return env.beforeEach(beforeEachFunction); 54 | }, 55 | 56 | afterEach: function(afterEachFunction) { 57 | return env.afterEach(afterEachFunction); 58 | }, 59 | 60 | expect: function(actual) { 61 | return env.expect(actual); 62 | }, 63 | 64 | pending: function() { 65 | return env.pending(); 66 | }, 67 | 68 | spyOn: function(obj, methodName) { 69 | return env.spyOn(obj, methodName); 70 | }, 71 | 72 | jsApiReporter: new jasmine.JsApiReporter({ 73 | timer: new jasmine.Timer() 74 | }) 75 | }; 76 | 77 | /** 78 | * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. 79 | */ 80 | if (typeof window == "undefined" && typeof exports == "object") { 81 | extend(exports, jasmineInterface); 82 | } else { 83 | extend(window, jasmineInterface); 84 | } 85 | 86 | /** 87 | * Expose the interface for adding custom equality testers. 88 | */ 89 | jasmine.addCustomEqualityTester = function(tester) { 90 | env.addCustomEqualityTester(tester); 91 | }; 92 | 93 | /** 94 | * Expose the interface for adding custom expectation matchers 95 | */ 96 | jasmine.addMatchers = function(matchers) { 97 | return env.addMatchers(matchers); 98 | }; 99 | 100 | /** 101 | * Expose the mock interface for the JavaScript timeout functions 102 | */ 103 | jasmine.clock = function() { 104 | return env.clock; 105 | }; 106 | 107 | /** 108 | * ## Runner Parameters 109 | * 110 | * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. 111 | */ 112 | 113 | var queryString = new jasmine.QueryString({ 114 | getWindowLocation: function() { return window.location; } 115 | }); 116 | 117 | var catchingExceptions = queryString.getParam("catch"); 118 | env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); 119 | 120 | /** 121 | * ## Reporters 122 | * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). 123 | */ 124 | var htmlReporter = new jasmine.HtmlReporter({ 125 | env: env, 126 | onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); }, 127 | getContainer: function() { return document.body; }, 128 | createElement: function() { return document.createElement.apply(document, arguments); }, 129 | createTextNode: function() { return document.createTextNode.apply(document, arguments); }, 130 | timer: new jasmine.Timer() 131 | }); 132 | 133 | /** 134 | * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. 135 | */ 136 | env.addReporter(jasmineInterface.jsApiReporter); 137 | env.addReporter(htmlReporter); 138 | 139 | /** 140 | * Filter which specs will be run by matching the start of the full name against the `spec` query param. 141 | */ 142 | var specFilter = new jasmine.HtmlSpecFilter({ 143 | filterString: function() { return queryString.getParam("spec"); } 144 | }); 145 | 146 | env.specFilter = function(spec) { 147 | return specFilter.matches(spec.getFullName()); 148 | }; 149 | 150 | /** 151 | * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. 152 | */ 153 | window.setTimeout = window.setTimeout; 154 | window.setInterval = window.setInterval; 155 | window.clearTimeout = window.clearTimeout; 156 | window.clearInterval = window.clearInterval; 157 | 158 | /** 159 | * ## Execution 160 | * 161 | * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. 162 | */ 163 | var currentWindowOnload = window.onload; 164 | 165 | window.onload = function() { 166 | if (currentWindowOnload) { 167 | currentWindowOnload(); 168 | } 169 | htmlReporter.initialize(); 170 | env.execute(); 171 | }; 172 | 173 | /** 174 | * Helper function for readability above. 175 | */ 176 | function extend(destination, source) { 177 | for (var property in source) destination[property] = source[property]; 178 | return destination; 179 | } 180 | 181 | }()); 182 | -------------------------------------------------------------------------------- /client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | angular_bootstrap_spring 7 | au.com.example 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | client 13 | pom 14 | 15 | 16 | 16 17 | 18 | UTF-8 19 | 20 | v6.9.1 21 | 3.10.8 22 | 23 | 0.7 24 | 2.1.1 25 | 2.0 26 | 1.3 27 | 28 | 29 | 30 | client 31 | 32 | 33 | 34 | 35 | com.github.klieber 36 | phantomjs-maven-plugin 37 | ${phantomjs.maven.plugin.version} 38 | 39 | 40 | com.github.searls 41 | jasmine-maven-plugin 42 | ${jasmine.maven.plugin.version} 43 | 44 | 45 | com.github.eirslett 46 | frontend-maven-plugin 47 | ${frontend.maven.plugin} 48 | 49 | 50 | 51 | 52 | 53 | 54 | com.github.klieber 55 | phantomjs-maven-plugin 56 | ${phantomjs.maven.plugin.version} 57 | 58 | 59 | 60 | install 61 | 62 | 63 | 64 | 65 | ${phantomjs.configuration.version} 66 | 67 | 68 | 69 | com.github.searls 70 | jasmine-maven-plugin 71 | 72 | 73 | 74 | test 75 | 76 | 77 | 78 | 79 | org.openqa.selenium.phantomjs.PhantomJSDriver 80 | 81 | 82 | phantomjs.binary.path 83 | ${phantomjs.binary} 84 | 85 | 86 | 87 | 88 | ${project.basedir}/src/main/webapp/js/jquery/jquery.js 89 | ${project.basedir}/node_modules/angular/angular.js 90 | ${project.basedir}/node_modules/angular-route/angular-route.js 91 | ${project.basedir}/node_modules/angular-messages/angular-messages.js 92 | ${project.basedir}/node_modules/angular-aria/angular-aria.js 93 | 94 | ${project.basedir}/node_modules/angular-mocks/angular-mocks.js 95 | ${project.basedir}/src/test/webapp/specs/angular/angular-jasmine.js 96 | 97 | ${project.basedir}/src/main/webapp/js/custom/*.js 98 | ${project.basedir}/src/main/webapp/js/custom/**/*.js 99 | 100 | ${project.basedir}/src/main/webapp/js 101 | ${project.basedir}/src/test/webapp/specs/custom 102 | 103 | **/*.spec.js 104 | 105 | 106 | 107 | 108 | com.github.eirslett 109 | frontend-maven-plugin 110 | ${frontend.maven.plugin} 111 | 112 | 113 | install node and npm 114 | 115 | install-node-and-npm 116 | 117 | generate-resources 118 | 119 | ${node.version} 120 | ${npm.version} 121 | 122 | 123 | 124 | npm install 125 | 126 | npm 127 | 128 | generate-resources 129 | 130 | 131 | gulp default 132 | 133 | gulp 134 | 135 | generate-resources 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | spring-milestones 145 | Spring Milestones 146 | http://repo.spring.io/milestone 147 | 148 | false 149 | 150 | 151 | 152 | 153 | 154 | 155 | Robert Leggett 156 | 157 | Solution Architect 158 | Technical Team Lead 159 | Senior Developer 160 | 161 | http://robertleggett.wordpress.com/ 162 | 163 | 164 | -------------------------------------------------------------------------------- /api/src/main/java/au/com/example/persistence/dao/user/UserDAOImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.example.persistence.dao.user; 2 | 3 | import au.com.example.persistence.dao.base.BaseDao; 4 | import au.com.example.persistence.dao.user.entity.MembershipEntity; 5 | import au.com.example.persistence.dao.user.entity.UserEntity; 6 | import au.com.example.persistence.dao.user.query.SelectUser; 7 | import au.com.example.persistence.dao.user.query.UpdatePassword; 8 | import au.com.example.persistence.exceptions.ChangePasswordException; 9 | import au.com.example.persistence.exceptions.CreateUserException; 10 | import au.com.example.persistence.exceptions.DeleteUserException; 11 | import au.com.example.persistence.exceptions.UpdateUserException; 12 | import au.com.example.service.user.model.SpringMembershipDetail; 13 | import au.com.example.service.user.model.SpringUserDetail; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.dao.DataAccessException; 18 | import org.springframework.security.core.GrantedAuthority; 19 | import org.springframework.security.core.userdetails.UserDetails; 20 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 21 | import org.springframework.security.crypto.password.PasswordEncoder; 22 | import org.springframework.stereotype.Repository; 23 | import org.springframework.transaction.annotation.Transactional; 24 | 25 | import javax.persistence.EntityManager; 26 | import javax.persistence.EntityTransaction; 27 | import java.util.ArrayList; 28 | import java.util.Collection; 29 | import java.util.List; 30 | 31 | @Repository 32 | public class UserDAOImpl extends BaseDao implements UserDAO { 33 | private static Logger log = LoggerFactory.getLogger(UserDAOImpl.class); 34 | 35 | @Autowired 36 | private PasswordEncoder passwordEncoder; 37 | 38 | @Override 39 | @Transactional(readOnly = true) 40 | public UserDetails loadUser(String email) 41 | throws UsernameNotFoundException, DataAccessException { 42 | UserEntity userEntity = loadDataSingle(UserEntity.class, new SelectUser(email)); 43 | 44 | if (userEntity == null) { 45 | throw new UsernameNotFoundException("User " + email + " could not be found."); 46 | } else { 47 | return toUserDetails(userEntity); 48 | } 49 | } 50 | 51 | @Override 52 | @Transactional(readOnly = false) 53 | public void changePassword(String username, String password) throws ChangePasswordException { 54 | try { 55 | String encodedPassword = passwordEncoder.encode(password); 56 | 57 | int rowsUpdated = updateDeleteDataSingle(new UpdatePassword(username, encodedPassword)); 58 | 59 | log.debug("Number of rows updated: " + rowsUpdated); 60 | } catch (Exception e) { 61 | log.error("Unable to change password for user: " + username + " because " + e.getMessage()); 62 | 63 | throw new ChangePasswordException("Unable to change password: " + e.getMessage()); 64 | } 65 | } 66 | 67 | @Override 68 | @Transactional(readOnly = true) 69 | public boolean userExists(String email) { 70 | boolean userExists = false; 71 | 72 | try { 73 | UserDetails user = loadUser(email); 74 | 75 | if (user != null) { 76 | userExists = true; 77 | } 78 | } catch (Exception e) { 79 | log.error("Unable to determine if user " + email + " exists: " + e.getMessage()); 80 | } 81 | 82 | return userExists; 83 | } 84 | 85 | @Override 86 | @Transactional(readOnly = false) 87 | public void createUser(UserDetails user) throws CreateUserException { 88 | EntityManager entityManager = getEmf().createEntityManager(); 89 | 90 | try { 91 | EntityTransaction tx = null; 92 | 93 | try { 94 | tx = entityManager.getTransaction(); 95 | 96 | tx.begin(); 97 | 98 | entityManager.persist(toUserEntity(user, true)); 99 | 100 | tx.commit(); 101 | } catch (Exception e) { 102 | log.error("Exception during creating user " + user + ": " + e.getMessage()); 103 | 104 | throw new CreateUserException(e.getMessage()); 105 | } finally { 106 | if (tx != null && tx.isActive()) { 107 | tx.rollback(); 108 | } 109 | } 110 | } finally { 111 | entityManager.close(); 112 | } 113 | } 114 | 115 | @Override 116 | @Transactional(readOnly = false) 117 | public void updateUser(UserDetails user) throws UpdateUserException { 118 | EntityManager entityManager = getEmf().createEntityManager(); 119 | 120 | try { 121 | EntityTransaction tx = null; 122 | 123 | try { 124 | tx = entityManager.getTransaction(); 125 | 126 | tx.begin(); 127 | 128 | entityManager.merge(toUserEntity(user, false)); 129 | 130 | tx.commit(); 131 | } catch (Exception e) { 132 | log.error("Exception during updating user " + user + ": " + e.getMessage()); 133 | 134 | throw new UpdateUserException(e.getMessage()); 135 | } finally { 136 | if (tx != null && tx.isActive()) { 137 | tx.rollback(); 138 | } 139 | } 140 | } finally { 141 | entityManager.close(); 142 | } 143 | } 144 | 145 | @Override 146 | @Transactional(readOnly = false) 147 | public void deleteUser(String email) throws DeleteUserException { 148 | EntityManager entityManager = getEmf().createEntityManager(); 149 | 150 | try { 151 | EntityTransaction tx = null; 152 | 153 | try { 154 | tx = entityManager.getTransaction(); 155 | 156 | tx.begin(); 157 | 158 | UserEntity userEntity = loadDataSingle(UserEntity.class, new SelectUser(email)); 159 | 160 | if (userEntity == null) { 161 | log.error("User with username " + email + " not found"); 162 | 163 | throw new DeleteUserException("User with username " + email + " not found"); 164 | } 165 | 166 | entityManager.remove(entityManager.find(UserEntity.class, userEntity.getEmail())); 167 | 168 | tx.commit(); 169 | } catch (Exception e) { 170 | log.error("Exception during deleting user " + email + ": " + e.getMessage()); 171 | 172 | throw new DeleteUserException(e.getMessage()); 173 | } finally { 174 | if (tx != null && tx.isActive()) { 175 | tx.rollback(); 176 | } 177 | } 178 | } finally { 179 | entityManager.close(); 180 | } 181 | } 182 | 183 | // === Helpers 184 | 185 | // === Service to Entity entity 186 | 187 | private UserEntity toUserEntity(UserDetails userDetails, boolean encodePassword) { 188 | UserEntity userEntity = null; 189 | 190 | if (userDetails != null) { 191 | if (userDetails instanceof SpringUserDetail) { 192 | SpringUserDetail user = (SpringUserDetail) userDetails; 193 | 194 | userEntity = new UserEntity( 195 | user.getUsername(), 196 | encodePassword ? passwordEncoder.encode(user.getPassword()) : user.getPassword(), 197 | user.getFirstName(), 198 | user.getLastName(), 199 | user.getAlias(), 200 | getSecurityRoles(user.getAuthorities()), 201 | user.isEnabled(), 202 | user.isAccountNonExpired(), 203 | user.isCredentialsNonExpired(), 204 | user.isAccountNonLocked()); 205 | } else { 206 | log.error("Only supports " + SpringUserDetail.class + " all other classes unsupported"); 207 | } 208 | } 209 | 210 | return userEntity; 211 | } 212 | 213 | private List getSecurityRoles(Collection authorities) { 214 | List securityRoles = new ArrayList<>(); 215 | 216 | if (authorities != null) { 217 | for (GrantedAuthority authority : authorities) { 218 | if (authority instanceof SpringMembershipDetail) { 219 | SpringMembershipDetail membership = (SpringMembershipDetail) authority; 220 | 221 | securityRoles.add(new MembershipEntity(membership.getId(), membership.getType(), membership.getExpire())); 222 | } 223 | } 224 | } 225 | 226 | return securityRoles; 227 | } 228 | 229 | // === Entity to Service entity 230 | 231 | private UserDetails toUserDetails(UserEntity userEntity) { 232 | UserDetails userDetails = null; 233 | 234 | if (userEntity != null) { 235 | userDetails = new SpringUserDetail( 236 | userEntity.getEmail(), 237 | userEntity.getPassword(), 238 | userEntity.getFirstName(), 239 | userEntity.getLastName(), 240 | userEntity.getAlias(), 241 | getGrantedAuthorities(userEntity.getMemberships()), 242 | userEntity.isEnabled(), 243 | userEntity.isAccountNonExpired(), 244 | userEntity.isCredentialsNonExpired(), 245 | userEntity.isAccountNonLocked()); 246 | } 247 | 248 | return userDetails; 249 | } 250 | 251 | private Collection getGrantedAuthorities(List securityRoles) { 252 | Collection authorities = new ArrayList<>(); 253 | 254 | if (securityRoles != null) { 255 | for (MembershipEntity role : securityRoles) { 256 | authorities.add(new SpringMembershipDetail(role.getId(), role.getType(), role.getExpire())); 257 | } 258 | } 259 | 260 | return authorities; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /client/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import gulp from "gulp"; 4 | import rename from "gulp-rename"; 5 | import gulpIf from "gulp-if"; 6 | import sass from "gulp-sass"; 7 | import sourcemaps from "gulp-sourcemaps"; 8 | import uglify from "gulp-uglifyjs"; 9 | import zip from "gulp-zip"; 10 | import inject from "gulp-inject"; 11 | import clean from "gulp-clean"; 12 | import util from "gulp-util"; 13 | import dateFormat from "date-format"; 14 | import path from "path"; 15 | import express from "express"; 16 | import morgan from "morgan"; 17 | import minimatch from "minimatch"; 18 | import http from "http"; 19 | import httpProxy from "http-proxy"; 20 | import tinyLiveReload from "tiny-lr"; 21 | import connectLiveReload from "connect-livereload"; 22 | import eventStream from "event-stream"; 23 | import yargs from "yargs"; 24 | 25 | const argv = yargs.argv; 26 | const timestamp = dateFormat(argv['timestamp-format'] || process.env['GULP_TIMESTAMP_FORMAT'] || 'yyyyMMddhhmmss'); 27 | const liveReload = tinyLiveReload(); 28 | 29 | const project = require('./package.json'); 30 | 31 | function proxy(logPrefix, options, proxyPort) { 32 | let httpServer = http.createServer((req, res) => { 33 | let option = options.find((option) => { 34 | return (minimatch(req.url, option.pattern)) 35 | }); 36 | 37 | option.proxy.web(req, res); 38 | }); 39 | 40 | httpServer.on('error', (err, req, res) => { 41 | res.status(503).end(); 42 | }); 43 | 44 | util.log(logPrefix, 'Proxy listening on port', util.colors.green(proxyPort)); 45 | 46 | httpServer.listen(proxyPort); 47 | } 48 | 49 | function proxyOptions(expressPort) { 50 | return [ 51 | { 52 | proxy: httpProxy.createProxyServer({ 53 | target: argv['api-url'] || process.env['GULP_API_URL'] || 'http://localhost:8080/' 54 | }), 55 | pattern: "/api/**" 56 | }, 57 | { 58 | proxy: httpProxy.createProxyServer({ 59 | target: argv['express-url'] || process.env['GULP_EXPRESS_URL'] || 'http://localhost:' + expressPort + '/' 60 | }), 61 | pattern: "/**" 62 | } 63 | ]; 64 | } 65 | 66 | function sendStream(res, stream) { 67 | return stream.pipe(eventStream.map((file, callback) => { 68 | let contentType = express.static.mime.lookup(file.path); 69 | let charset = express.static.mime.charsets.lookup(contentType); 70 | 71 | res.set('Content-Type', contentType + (charset ? '; charset=' + charset : '')); 72 | 73 | callback(null, file.contents); 74 | })).pipe(res); 75 | } 76 | 77 | function expressIndex() { 78 | return (req, res) => { 79 | sendStream(res, gulp.src('src/main/webapp/index.html') 80 | .pipe(inject(gulp.src(['target/gulp/main/js/app.js', 'target/gulp/main/css/style.css']), { 81 | ignorePath: 'target/gulp/main', 82 | addRootSlash: false 83 | }))); 84 | } 85 | } 86 | 87 | function expressJasmine() { 88 | return (req, res, next) => { 89 | let staticServer = express.static('src/test/webapp'); 90 | 91 | if (req.path == '/') { 92 | sendStream(res, gulp.src('src/test/webapp/html/jasmine-index.html') 93 | .pipe(inject(gulp.src('target/gulp/test/js/app.js'), { 94 | ignorePath: 'target/gulp', 95 | addRootSlash: false 96 | }))); 97 | } 98 | else { 99 | staticServer(req, res, next); 100 | } 101 | }; 102 | } 103 | 104 | function expressStatic(path) { 105 | return (req, res, next) => { 106 | let staticServer = express.static(path); 107 | 108 | return staticServer(req, res, () => { 109 | res.status(404).send('Not Found'); 110 | }); 111 | }; 112 | } 113 | 114 | function appJs() { 115 | return gulp.src([ 116 | 'src/main/webapp/js/jquery/jquery.js', 117 | require.resolve('angular/angular.js'), 118 | require.resolve('angular-aria/angular-aria.js'), 119 | require.resolve('angular-cookies/angular-cookies.js'), 120 | require.resolve('angular-messages/angular-messages.js'), 121 | require.resolve('angular-route/angular-route.js'), 122 | require.resolve('angular-sanitize/angular-sanitize.js'), 123 | require.resolve('bootstrap-sass/assets/javascripts/bootstrap.js'), 124 | 'src/main/webapp/js/angular-xeditable/*.js', 125 | 'src/main/webapp/js/custom/*.js', 126 | 'src/main/webapp/js/custom/**/*.js' 127 | ]); 128 | } 129 | 130 | function compileJs() { 131 | return appJs() 132 | .pipe(uglify('app.js', { 133 | basePath: 'js', 134 | mangle: false, 135 | outSourceMap: argv['uglify-out-source-maps'] || process.env['GULP_UGLIFY_OUT_SOURCE_MAPS'] !== undefined 136 | })) 137 | .pipe(gulp.dest('target/gulp/main/js')); 138 | } 139 | 140 | function compileScss(errLogToConsole) { 141 | let enableSassSourceMaps = argv['enable-sass-source-maps'] || process.env['GULP_ENABLE_SASS_SOURCE_MAPS'] !== undefined; 142 | 143 | return gulp.src('src/main/webapp/scss/main.scss') 144 | .pipe(gulpIf(enableSassSourceMaps, sourcemaps.init())) 145 | .pipe(sass({ 146 | errLogToConsole: errLogToConsole, 147 | outputStyle: argv['sass-output-style'] || process.env['GULP_SASS_OUTPUT_STYLE'] || 'compressed', 148 | includePaths: [ 149 | 'node_modules/bootstrap-sass/assets/stylesheets', 150 | 'node_modules/font-awesome/scss' 151 | ], 152 | sourceMap: '' // Required to prevent gulp-sass from crashing. 153 | })) 154 | .pipe(rename('style.css')) 155 | .pipe(gulpIf(enableSassSourceMaps, sourcemaps.write('.'))) 156 | .pipe(gulp.dest('target/gulp/main/css')); 157 | } 158 | 159 | function testJs() { 160 | return gulp.src([ 161 | require.resolve('angular/angular.js'), 162 | require.resolve('angular-route/angular-route.js'), 163 | require.resolve('angular-messages/angular-messages.js'), 164 | require.resolve('angular-aria/angular-aria.js'), 165 | require.resolve('angular-mocks/angular-mocks.js'), 166 | 'src/test/webapp/specs/angular/angular-jasmine.js', 167 | 'src/main/webapp/js/custom/*.js', 168 | 'src/main/webapp/js/custom/**/*.js', 169 | 'src/test/webapp/specs/custom/*.spec.js' 170 | ]); 171 | } 172 | 173 | function compileTestJs() { 174 | return testJs() 175 | .pipe(uglify('app.js', { 176 | basePath: 'js', 177 | mangle: false, 178 | outSourceMap: argv['uglify-out-source-maps'] || process.env['GULP_UGLIFY_OUT_SOURCE_MAPS'] !== undefined 179 | })) 180 | .pipe(gulp.dest('target/gulp/test/js')); 181 | } 182 | 183 | // ************************************ // 184 | // ************** TASKS *************** // 185 | // ************************************ // 186 | 187 | gulp.task('compile-font', () => { 188 | return gulp.src(['node_modules/bootstrap-sass/assets/fonts/**/**.*', 'node_modules/font-awesome/fonts/**.*']) 189 | .pipe(gulp.dest('target/gulp/main/fonts')); 190 | }); 191 | 192 | gulp.task('compile-js', () => { 193 | return compileJs(); 194 | }); 195 | 196 | gulp.task('watch-js', ['compile-js'], () => { 197 | let logPrefix = '[' + util.colors.blue('watch-js') + ']'; 198 | 199 | gulp.watch('src/main/webapp/js/**/*.js', () => { 200 | util.log(logPrefix, 'Recompiling JS'); 201 | 202 | compileJs(); 203 | }); 204 | 205 | gulp.watch('target/gulp/main/js/*.js', (event) => { 206 | util.log(logPrefix, 'Reloading', path.relative('target/gulp/main/js', event.path)); 207 | 208 | liveReload.changed({ 209 | body: { 210 | files: [ 211 | path.relative('target/gulp/main/js', event.path) 212 | ] 213 | } 214 | }); 215 | }); 216 | }); 217 | 218 | gulp.task('compile-scss', () => { 219 | return compileScss(false); 220 | }); 221 | 222 | gulp.task('watch-scss', ['compile-scss'], () => { 223 | let logPrefix = '[' + util.colors.blue('watch-scss') + ']'; 224 | 225 | gulp.watch('src/main/webapp/scss/**/*.scss', () => { 226 | util.log(logPrefix, 'Recompiling SCSS'); 227 | 228 | compileScss(true); 229 | }); 230 | 231 | gulp.watch('target/gulp/main/css/*.css', (event) => { 232 | util.log(logPrefix, 'Reloading', path.relative('target/gulp/main/css', event.path)); 233 | 234 | liveReload.changed({ 235 | body: { 236 | files: [ 237 | path.relative('target/gulp/main/css', event.path) 238 | ] 239 | } 240 | }); 241 | }); 242 | }); 243 | 244 | gulp.task('watch-html', () => { 245 | let logPrefix = '[' + util.colors.blue('watch-html') + ']'; 246 | 247 | gulp.watch(['src/main/webapp/index.html', 'src/main/webapp/html/**'], (event) => { 248 | util.log(logPrefix, 'Reloading', path.relative('src/main/webapp', event.path)); 249 | 250 | liveReload.changed({ 251 | body: { 252 | files: [ 253 | path.relative('src/main/webapp', event.path) 254 | ] 255 | } 256 | }); 257 | }); 258 | }); 259 | 260 | gulp.task('compile-test-js', () => { 261 | return compileTestJs(); 262 | }); 263 | 264 | gulp.task('watch-test-js', ['compile-test-js'], () => { 265 | let logPrefix = '[' + util.colors.blue('watch-test-js') + ']'; 266 | 267 | gulp.watch('src/test/webapp/specs/**/*.js', () => { 268 | util.log(logPrefix, 'Recompiling Test JS'); 269 | 270 | compileTestJs(); 271 | }); 272 | 273 | gulp.watch('target/gulp/test/js/*.js', (event) => { 274 | util.log(logPrefix, 'Reloading', path.relative('target/gulp/test/js', event.path)); 275 | 276 | liveReload.changed({ 277 | body: { 278 | files: [ 279 | path.relative('target/gulp/test/js', event.path) 280 | ] 281 | } 282 | }); 283 | }); 284 | }); 285 | 286 | gulp.task('zip', ['compile-font', 'compile-js', 'compile-scss'], () => { 287 | let buildNumber = argv['build-number'] || process.env['GULP_BUILD_NUMBER']; 288 | let filename = argv['zip-filename'] || process.env['GULP_ZIP_FILENAME'] || project.name + '-' + project.version + (buildNumber !== undefined ? '+build.' + buildNumber : '') + '.zip'; 289 | 290 | util.log('Creating', util.colors.magenta(filename)); 291 | 292 | return eventStream.merge( 293 | gulp.src([ 294 | argv['uglifyjs-out-source-maps'] || process.env['GULP_UGLIFYJS_OUT_SOURCE_MAPS'] ? 'src/main/webapp/js/**' : '', 295 | 'src/main/webapp/html/**', 296 | 'src/main/webapp/img/**' 297 | ], {base: 'src/main/webapp'}), 298 | 299 | gulp.src('target/gulp/main/**'), 300 | 301 | gulp.src('src/main/webapp/index.html') 302 | .pipe(inject(gulp.src(['target/gulp/main/js/app.js', 'target/gulp/main/css/style.css']), { 303 | ignorePath: 'target/gulp/main', 304 | addRootSlash: false 305 | }))) 306 | .pipe(zip(filename)) 307 | .pipe(gulp.dest('target')); 308 | }); 309 | 310 | gulp.task('run', ['compile-font', 'watch-html', 'watch-js', 'watch-scss', 'watch-test-js'], () => { 311 | let logPrefix = '[' + util.colors.blue('run') + ']'; 312 | 313 | let proxyPort = argv['proxy-port'] || process.env['GULP_PROXY_PORT'] || 4444; 314 | let expressPort = argv['express-port'] || process.env['GULP_EXPRESS_PORT'] || 7777; 315 | let liveReloadPort = argv['live-reload-port'] || process.env['GULP_LIVE_RELOAD_PORT'] || 35729; 316 | 317 | liveReload.listen(liveReloadPort); 318 | 319 | proxy(logPrefix, proxyOptions(expressPort), proxyPort); 320 | 321 | return express() 322 | .use(morgan('combined')) 323 | .use('/css', expressStatic('target/gulp/main/css')) 324 | .use('/fonts', expressStatic('target/gulp/main/fonts')) 325 | .use('/html', expressStatic('src/main/webapp/html')) 326 | .use('/img', expressStatic('src/main/webapp/img')) 327 | .use('/js', expressStatic('target/gulp/main/js')) 328 | .use(connectLiveReload({ 329 | port: liveReloadPort 330 | })) 331 | .use('/test/js', expressStatic('target/gulp/test/js')) 332 | .use('/test', expressJasmine()) 333 | .use('/specs', expressStatic('src/test/webapp/specs')) 334 | .use(expressIndex()) 335 | .listen(expressPort, function () { 336 | util.log(logPrefix, 'Express listening on port', util.colors.green(expressPort)); 337 | }); 338 | }); 339 | 340 | gulp.task('clean', () => { 341 | return gulp.src('target/gulp', {read: false}) 342 | .pipe(clean()); 343 | }); 344 | 345 | gulp.task('default', ['zip']); -------------------------------------------------------------------------------- /client/src/test/webapp/specs/jasmine/jasmine-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2014 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | jasmineRequire.html = function(j$) { 24 | j$.ResultsNode = jasmineRequire.ResultsNode(); 25 | j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); 26 | j$.QueryString = jasmineRequire.QueryString(); 27 | j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); 28 | }; 29 | 30 | jasmineRequire.HtmlReporter = function(j$) { 31 | 32 | var noopTimer = { 33 | start: function() {}, 34 | elapsed: function() { return 0; } 35 | }; 36 | 37 | function HtmlReporter(options) { 38 | var env = options.env || {}, 39 | getContainer = options.getContainer, 40 | createElement = options.createElement, 41 | createTextNode = options.createTextNode, 42 | onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {}, 43 | timer = options.timer || noopTimer, 44 | results = [], 45 | specsExecuted = 0, 46 | failureCount = 0, 47 | pendingSpecCount = 0, 48 | htmlReporterMain, 49 | symbols; 50 | 51 | this.initialize = function() { 52 | clearPrior(); 53 | htmlReporterMain = createDom('div', {className: 'jasmine_html-reporter'}, 54 | createDom('div', {className: 'banner'}, 55 | createDom('a', {className: 'title', href: 'http://jasmine.github.io/', target: '_blank'}), 56 | createDom('span', {className: 'version'}, j$.version) 57 | ), 58 | createDom('ul', {className: 'symbol-summary'}), 59 | createDom('div', {className: 'alert'}), 60 | createDom('div', {className: 'results'}, 61 | createDom('div', {className: 'failures'}) 62 | ) 63 | ); 64 | getContainer().appendChild(htmlReporterMain); 65 | 66 | symbols = find('.symbol-summary'); 67 | }; 68 | 69 | var totalSpecsDefined; 70 | this.jasmineStarted = function(options) { 71 | totalSpecsDefined = options.totalSpecsDefined || 0; 72 | timer.start(); 73 | }; 74 | 75 | var summary = createDom('div', {className: 'summary'}); 76 | 77 | var topResults = new j$.ResultsNode({}, '', null), 78 | currentParent = topResults; 79 | 80 | this.suiteStarted = function(result) { 81 | currentParent.addChild(result, 'suite'); 82 | currentParent = currentParent.last(); 83 | }; 84 | 85 | this.suiteDone = function(result) { 86 | if (currentParent == topResults) { 87 | return; 88 | } 89 | 90 | currentParent = currentParent.parent; 91 | }; 92 | 93 | this.specStarted = function(result) { 94 | currentParent.addChild(result, 'spec'); 95 | }; 96 | 97 | var failures = []; 98 | this.specDone = function(result) { 99 | if(noExpectations(result) && console && console.error) { 100 | console.error('Spec \'' + result.fullName + '\' has no expectations.'); 101 | } 102 | 103 | if (result.status != 'disabled') { 104 | specsExecuted++; 105 | } 106 | 107 | symbols.appendChild(createDom('li', { 108 | className: noExpectations(result) ? 'empty' : result.status, 109 | id: 'spec_' + result.id, 110 | title: result.fullName 111 | } 112 | )); 113 | 114 | if (result.status == 'failed') { 115 | failureCount++; 116 | 117 | var failure = 118 | createDom('div', {className: 'spec-detail failed'}, 119 | createDom('div', {className: 'description'}, 120 | createDom('a', {title: result.fullName, href: specHref(result)}, result.fullName) 121 | ), 122 | createDom('div', {className: 'messages'}) 123 | ); 124 | var messages = failure.childNodes[1]; 125 | 126 | for (var i = 0; i < result.failedExpectations.length; i++) { 127 | var expectation = result.failedExpectations[i]; 128 | messages.appendChild(createDom('div', {className: 'result-message'}, expectation.message)); 129 | messages.appendChild(createDom('div', {className: 'stack-trace'}, expectation.stack)); 130 | } 131 | 132 | failures.push(failure); 133 | } 134 | 135 | if (result.status == 'pending') { 136 | pendingSpecCount++; 137 | } 138 | }; 139 | 140 | this.jasmineDone = function() { 141 | var banner = find('.banner'); 142 | banner.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); 143 | 144 | var alert = find('.alert'); 145 | 146 | alert.appendChild(createDom('span', { className: 'exceptions' }, 147 | createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions'), 148 | createDom('input', { 149 | className: 'raise', 150 | id: 'raise-exceptions', 151 | type: 'checkbox' 152 | }) 153 | )); 154 | var checkbox = find('#raise-exceptions'); 155 | 156 | checkbox.checked = !env.catchingExceptions(); 157 | checkbox.onclick = onRaiseExceptionsClick; 158 | 159 | if (specsExecuted < totalSpecsDefined) { 160 | var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; 161 | alert.appendChild( 162 | createDom('span', {className: 'bar skipped'}, 163 | createDom('a', {href: '?', title: 'Run all specs'}, skippedMessage) 164 | ) 165 | ); 166 | } 167 | var statusBarMessage = ''; 168 | var statusBarClassName = 'bar '; 169 | 170 | if (totalSpecsDefined > 0) { 171 | statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount); 172 | if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); } 173 | statusBarClassName += (failureCount > 0) ? 'failed' : 'passed'; 174 | } else { 175 | statusBarClassName += 'skipped'; 176 | statusBarMessage += 'No specs found'; 177 | } 178 | 179 | alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage)); 180 | 181 | var results = find('.results'); 182 | results.appendChild(summary); 183 | 184 | summaryList(topResults, summary); 185 | 186 | function summaryList(resultsTree, domParent) { 187 | var specListNode; 188 | for (var i = 0; i < resultsTree.children.length; i++) { 189 | var resultNode = resultsTree.children[i]; 190 | if (resultNode.type == 'suite') { 191 | var suiteListNode = createDom('ul', {className: 'suite', id: 'suite-' + resultNode.result.id}, 192 | createDom('li', {className: 'suite-detail'}, 193 | createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description) 194 | ) 195 | ); 196 | 197 | summaryList(resultNode, suiteListNode); 198 | domParent.appendChild(suiteListNode); 199 | } 200 | if (resultNode.type == 'spec') { 201 | if (domParent.getAttribute('class') != 'specs') { 202 | specListNode = createDom('ul', {className: 'specs'}); 203 | domParent.appendChild(specListNode); 204 | } 205 | var specDescription = resultNode.result.description; 206 | if(noExpectations(resultNode.result)) { 207 | specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription; 208 | } 209 | specListNode.appendChild( 210 | createDom('li', { 211 | className: resultNode.result.status, 212 | id: 'spec-' + resultNode.result.id 213 | }, 214 | createDom('a', {href: specHref(resultNode.result)}, specDescription) 215 | ) 216 | ); 217 | } 218 | } 219 | } 220 | 221 | if (failures.length) { 222 | alert.appendChild( 223 | createDom('span', {className: 'menu bar spec-list'}, 224 | createDom('span', {}, 'Spec List | '), 225 | createDom('a', {className: 'failures-menu', href: '#'}, 'Failures'))); 226 | alert.appendChild( 227 | createDom('span', {className: 'menu bar failure-list'}, 228 | createDom('a', {className: 'spec-list-menu', href: '#'}, 'Spec List'), 229 | createDom('span', {}, ' | Failures '))); 230 | 231 | find('.failures-menu').onclick = function() { 232 | setMenuModeTo('failure-list'); 233 | }; 234 | find('.spec-list-menu').onclick = function() { 235 | setMenuModeTo('spec-list'); 236 | }; 237 | 238 | setMenuModeTo('failure-list'); 239 | 240 | var failureNode = find('.failures'); 241 | for (var i = 0; i < failures.length; i++) { 242 | failureNode.appendChild(failures[i]); 243 | } 244 | } 245 | }; 246 | 247 | return this; 248 | 249 | function find(selector) { 250 | return getContainer().querySelector('.jasmine_html-reporter ' + selector); 251 | } 252 | 253 | function clearPrior() { 254 | // return the reporter 255 | var oldReporter = find(''); 256 | 257 | if(oldReporter) { 258 | getContainer().removeChild(oldReporter); 259 | } 260 | } 261 | 262 | function createDom(type, attrs, childrenVarArgs) { 263 | var el = createElement(type); 264 | 265 | for (var i = 2; i < arguments.length; i++) { 266 | var child = arguments[i]; 267 | 268 | if (typeof child === 'string') { 269 | el.appendChild(createTextNode(child)); 270 | } else { 271 | if (child) { 272 | el.appendChild(child); 273 | } 274 | } 275 | } 276 | 277 | for (var attr in attrs) { 278 | if (attr == 'className') { 279 | el[attr] = attrs[attr]; 280 | } else { 281 | el.setAttribute(attr, attrs[attr]); 282 | } 283 | } 284 | 285 | return el; 286 | } 287 | 288 | function pluralize(singular, count) { 289 | var word = (count == 1 ? singular : singular + 's'); 290 | 291 | return '' + count + ' ' + word; 292 | } 293 | 294 | function specHref(result) { 295 | return '?spec=' + encodeURIComponent(result.fullName); 296 | } 297 | 298 | function setMenuModeTo(mode) { 299 | htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode); 300 | } 301 | 302 | function noExpectations(result) { 303 | return (result.failedExpectations.length + result.passedExpectations.length) === 0 && 304 | result.status === 'passed'; 305 | } 306 | } 307 | 308 | return HtmlReporter; 309 | }; 310 | 311 | jasmineRequire.HtmlSpecFilter = function() { 312 | function HtmlSpecFilter(options) { 313 | var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 314 | var filterPattern = new RegExp(filterString); 315 | 316 | this.matches = function(specName) { 317 | return filterPattern.test(specName); 318 | }; 319 | } 320 | 321 | return HtmlSpecFilter; 322 | }; 323 | 324 | jasmineRequire.ResultsNode = function() { 325 | function ResultsNode(result, type, parent) { 326 | this.result = result; 327 | this.type = type; 328 | this.parent = parent; 329 | 330 | this.children = []; 331 | 332 | this.addChild = function(result, type) { 333 | this.children.push(new ResultsNode(result, type, this)); 334 | }; 335 | 336 | this.last = function() { 337 | return this.children[this.children.length - 1]; 338 | }; 339 | } 340 | 341 | return ResultsNode; 342 | }; 343 | 344 | jasmineRequire.QueryString = function() { 345 | function QueryString(options) { 346 | 347 | this.setParam = function(key, value) { 348 | var paramMap = queryStringToParamMap(); 349 | paramMap[key] = value; 350 | options.getWindowLocation().search = toQueryString(paramMap); 351 | }; 352 | 353 | this.getParam = function(key) { 354 | return queryStringToParamMap()[key]; 355 | }; 356 | 357 | return this; 358 | 359 | function toQueryString(paramMap) { 360 | var qStrPairs = []; 361 | for (var prop in paramMap) { 362 | qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])); 363 | } 364 | return '?' + qStrPairs.join('&'); 365 | } 366 | 367 | function queryStringToParamMap() { 368 | var paramStr = options.getWindowLocation().search.substring(1), 369 | params = [], 370 | paramMap = {}; 371 | 372 | if (paramStr.length > 0) { 373 | params = paramStr.split('&'); 374 | for (var i = 0; i < params.length; i++) { 375 | var p = params[i].split('='); 376 | var value = decodeURIComponent(p[1]); 377 | if (value === 'true' || value === 'false') { 378 | value = JSON.parse(value); 379 | } 380 | paramMap[decodeURIComponent(p[0])] = value; 381 | } 382 | } 383 | 384 | return paramMap; 385 | } 386 | 387 | } 388 | 389 | return QueryString; 390 | }; 391 | -------------------------------------------------------------------------------- /client/src/test/webapp/specs/jasmine/jasmine.css: -------------------------------------------------------------------------------- 1 | body { overflow-y: scroll; } 2 | 3 | .jasmine_html-reporter { background-color: #eeeeee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | .jasmine_html-reporter a { text-decoration: none; } 5 | .jasmine_html-reporter a:hover { text-decoration: underline; } 6 | .jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; } 7 | .jasmine_html-reporter .banner, .jasmine_html-reporter .symbol-summary, .jasmine_html-reporter .summary, .jasmine_html-reporter .result-message, .jasmine_html-reporter .spec .description, .jasmine_html-reporter .spec-detail .description, .jasmine_html-reporter .alert .bar, .jasmine_html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; } 8 | .jasmine_html-reporter .banner { position: relative; } 9 | .jasmine_html-reporter .banner .title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -webkit-background-size: 100%; -moz-background-size: 100%; -o-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; } 10 | .jasmine_html-reporter .banner .version { margin-left: 14px; position: relative; top: 6px; } 11 | .jasmine_html-reporter .banner .duration { position: absolute; right: 14px; top: 6px; } 12 | .jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; } 13 | .jasmine_html-reporter .version { color: #aaaaaa; } 14 | .jasmine_html-reporter .banner { margin-top: 14px; } 15 | .jasmine_html-reporter .duration { color: #aaaaaa; float: right; } 16 | .jasmine_html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } 17 | .jasmine_html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; } 18 | .jasmine_html-reporter .symbol-summary li.passed { font-size: 14px; } 19 | .jasmine_html-reporter .symbol-summary li.passed:before { color: #007069; content: "\02022"; } 20 | .jasmine_html-reporter .symbol-summary li.failed { line-height: 9px; } 21 | .jasmine_html-reporter .symbol-summary li.failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; } 22 | .jasmine_html-reporter .symbol-summary li.disabled { font-size: 14px; } 23 | .jasmine_html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; } 24 | .jasmine_html-reporter .symbol-summary li.pending { line-height: 17px; } 25 | .jasmine_html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; } 26 | .jasmine_html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 27 | .jasmine_html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 28 | .jasmine_html-reporter .bar.failed { background-color: #ca3a11; } 29 | .jasmine_html-reporter .bar.passed { background-color: #007069; } 30 | .jasmine_html-reporter .bar.skipped { background-color: #bababa; } 31 | .jasmine_html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; } 32 | .jasmine_html-reporter .bar.menu a { color: #333333; } 33 | .jasmine_html-reporter .bar a { color: white; } 34 | .jasmine_html-reporter.spec-list .bar.menu.failure-list, .jasmine_html-reporter.spec-list .results .failures { display: none; } 35 | .jasmine_html-reporter.failure-list .bar.menu.spec-list, .jasmine_html-reporter.failure-list .summary { display: none; } 36 | .jasmine_html-reporter .running-alert { background-color: #666666; } 37 | .jasmine_html-reporter .results { margin-top: 14px; } 38 | .jasmine_html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 39 | .jasmine_html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 40 | .jasmine_html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 41 | .jasmine_html-reporter.showDetails .summary { display: none; } 42 | .jasmine_html-reporter.showDetails #details { display: block; } 43 | .jasmine_html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 44 | .jasmine_html-reporter .summary { margin-top: 14px; } 45 | .jasmine_html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } 46 | .jasmine_html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; } 47 | .jasmine_html-reporter .summary li.passed a { color: #007069; } 48 | .jasmine_html-reporter .summary li.failed a { color: #ca3a11; } 49 | .jasmine_html-reporter .summary li.empty a { color: #ba9d37; } 50 | .jasmine_html-reporter .summary li.pending a { color: #ba9d37; } 51 | .jasmine_html-reporter .description + .suite { margin-top: 0; } 52 | .jasmine_html-reporter .suite { margin-top: 14px; } 53 | .jasmine_html-reporter .suite a { color: #333333; } 54 | .jasmine_html-reporter .failures .spec-detail { margin-bottom: 28px; } 55 | .jasmine_html-reporter .failures .spec-detail .description { background-color: #ca3a11; } 56 | .jasmine_html-reporter .failures .spec-detail .description a { color: white; } 57 | .jasmine_html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; } 58 | .jasmine_html-reporter .result-message span.result { display: block; } 59 | .jasmine_html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 60 | --------------------------------------------------------------------------------