├── 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 |
3 | × Close
4 | {{error.message}}
5 |
6 |
--------------------------------------------------------------------------------
/client/src/main/webapp/html/partials/common/alert.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | × Close
4 | {{alert.message}}
5 |
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 |
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 |
--------------------------------------------------------------------------------
/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 extends GrantedAuthority> 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 |
2 |
3 |
12 |
13 |
39 |
--------------------------------------------------------------------------------
/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 extends GrantedAuthority> 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 |
18 |
19 |
20 | Filtering by: {{ search.lastName }}
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/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 | [](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 | [](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 |
--------------------------------------------------------------------------------